Added support for acknowledging code of conduct for using public Notebook Gallery (#117)
* minro code edits * Added support for acknowledging code of conduct - Added CodeOfConduct component that shows links and a checkbox that must be acknwledged before seeing the public galley - Added verbose message for notebook publish error (when another notebook with the same name exists in the gallery) - Added a feature flag for enabling code of conduct acknowledgement * Added Info Component * minor edit * fixed failign tests * publish tab displayed only when code of conduct accepted * added code of conduct fetch during publish * fixed bug * added test and addressed PR comments * changed line endings * added comment * addressed PR comments
This commit is contained in:
parent
3051961093
commit
7a3e54d43e
|
@ -7,6 +7,12 @@ export class AuthorizationEndpoints {
|
||||||
public static common: string = "https://login.windows.net/";
|
public static common: string = "https://login.windows.net/";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class CodeOfConductEndpoints {
|
||||||
|
public static privacyStatement: string = "https://aka.ms/ms-privacy-policy";
|
||||||
|
public static codeOfConduct: string = "https://aka.ms/cosmos-code-of-conduct";
|
||||||
|
public static termsOfUse: string = "https://aka.ms/ms-terms-of-use";
|
||||||
|
}
|
||||||
|
|
||||||
export class BackendEndpoints {
|
export class BackendEndpoints {
|
||||||
public static localhost: string = "https://localhost:12900";
|
public static localhost: string = "https://localhost:12900";
|
||||||
public static dev: string = "https://ext.documents-dev.windows-int.net";
|
public static dev: string = "https://ext.documents-dev.windows-int.net";
|
||||||
|
@ -116,6 +122,7 @@ export class Features {
|
||||||
public static readonly enableTtl = "enablettl";
|
public static readonly enableTtl = "enablettl";
|
||||||
public static readonly enableNotebooks = "enablenotebooks";
|
public static readonly enableNotebooks = "enablenotebooks";
|
||||||
public static readonly enableGalleryPublish = "enablegallerypublish";
|
public static readonly enableGalleryPublish = "enablegallerypublish";
|
||||||
|
public static readonly enableCodeOfConduct = "enablecodeofconduct";
|
||||||
public static readonly enableLinkInjection = "enablelinkinjection";
|
public static readonly enableLinkInjection = "enablelinkinjection";
|
||||||
public static readonly enableSpark = "enablespark";
|
public static readonly enableSpark = "enablespark";
|
||||||
public static readonly livyEndpoint = "livyendpoint";
|
public static readonly livyEndpoint = "livyendpoint";
|
||||||
|
|
|
@ -1,69 +1,69 @@
|
||||||
import * as DataModels from "../Contracts/DataModels";
|
import * as DataModels from "../Contracts/DataModels";
|
||||||
import * as ViewModels from "../Contracts/ViewModels";
|
import * as ViewModels from "../Contracts/ViewModels";
|
||||||
|
|
||||||
export function replaceKnownError(err: string): string {
|
export function replaceKnownError(err: string): string {
|
||||||
if (
|
if (
|
||||||
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
|
window.dataExplorer.subscriptionType() === ViewModels.SubscriptionType.Internal &&
|
||||||
err.indexOf("SharedOffer is Disabled for your account") >= 0
|
err.indexOf("SharedOffer is Disabled for your account") >= 0
|
||||||
) {
|
) {
|
||||||
return "Database throughput is not supported for internal subscriptions.";
|
return "Database throughput is not supported for internal subscriptions.";
|
||||||
} else if (err.indexOf("Partition key paths must contain only valid") >= 0) {
|
} else if (err.indexOf("Partition key paths must contain only valid") >= 0) {
|
||||||
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
return "Partition key paths must contain only valid characters and not contain a trailing slash or wildcard character.";
|
||||||
}
|
}
|
||||||
|
|
||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parse(err: any): DataModels.ErrorDataModel[] {
|
export function parse(err: any): DataModels.ErrorDataModel[] {
|
||||||
try {
|
try {
|
||||||
return _parse(err);
|
return _parse(err);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
|
return [<DataModels.ErrorDataModel>{ message: JSON.stringify(err) }];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _parse(err: any): DataModels.ErrorDataModel[] {
|
function _parse(err: any): DataModels.ErrorDataModel[] {
|
||||||
var normalizedErrors: DataModels.ErrorDataModel[] = [];
|
var normalizedErrors: DataModels.ErrorDataModel[] = [];
|
||||||
if (err.message && !err.code) {
|
if (err.message && !err.code) {
|
||||||
normalizedErrors.push(err);
|
normalizedErrors.push(err);
|
||||||
} else {
|
} else {
|
||||||
const innerErrors: any[] = _getInnerErrors(err.message);
|
const innerErrors: any[] = _getInnerErrors(err.message);
|
||||||
normalizedErrors = innerErrors.map(innerError =>
|
normalizedErrors = innerErrors.map(innerError =>
|
||||||
typeof innerError === "string" ? { message: innerError } : innerError
|
typeof innerError === "string" ? { message: innerError } : innerError
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return normalizedErrors;
|
return normalizedErrors;
|
||||||
}
|
}
|
||||||
|
|
||||||
function _getInnerErrors(message: string): any[] {
|
function _getInnerErrors(message: string): any[] {
|
||||||
/*
|
/*
|
||||||
The backend error message has an inner-message which is a stringified object.
|
The backend error message has an inner-message which is a stringified object.
|
||||||
|
|
||||||
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
|
For SQL errors, the "errors" property is an array of SqlErrorDataModel.
|
||||||
Example:
|
Example:
|
||||||
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
|
"Message: {"Errors":["Resource with specified id or name already exists"]}\r\nActivityId: 80005000008d40b6a, Request URI: /apps/19000c000c0a0005/services/mctestdocdbprod-MasterService-0-00066ab9937/partitions/900005f9000e676fb8/replicas/13000000000955p"
|
||||||
For non-SQL errors the "Errors" propery is an array of string.
|
For non-SQL errors the "Errors" propery is an array of string.
|
||||||
Example:
|
Example:
|
||||||
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
|
"Message: {"errors":[{"severity":"Error","location":{"start":7,"end":8},"code":"SC1001","message":"Syntax error, incorrect syntax near '.'."}]}\r\nActivityId: d3300016d4084e310a, Request URI: /apps/12401f9e1df77/services/dc100232b1f44545/partitions/f86f3bc0001a2f78/replicas/13085003638s"
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let innerMessage: any = null;
|
let innerMessage: any = null;
|
||||||
|
|
||||||
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
|
const singleLineMessage = message.replace(/[\r\n]|\r|\n/g, "");
|
||||||
try {
|
try {
|
||||||
// Multi-Partition error flavor
|
// Multi-Partition error flavor
|
||||||
const regExp = /^(.*)ActivityId: (.*)/g;
|
const regExp = /^(.*)ActivityId: (.*)/g;
|
||||||
const regString = regExp.exec(singleLineMessage);
|
const regString = regExp.exec(singleLineMessage);
|
||||||
const innerMessageString = regString[1];
|
const innerMessageString = regString[1];
|
||||||
innerMessage = JSON.parse(innerMessageString);
|
innerMessage = JSON.parse(innerMessageString);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Single-partition error flavor
|
// Single-partition error flavor
|
||||||
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
|
const regExp = /^Message: (.*)ActivityId: (.*), Request URI: (.*)/g;
|
||||||
const regString = regExp.exec(singleLineMessage);
|
const regString = regExp.exec(singleLineMessage);
|
||||||
const innerMessageString = regString[1];
|
const innerMessageString = regString[1];
|
||||||
innerMessage = JSON.parse(innerMessageString);
|
innerMessage = JSON.parse(innerMessageString);
|
||||||
}
|
}
|
||||||
|
|
||||||
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
|
return innerMessage.errors ? innerMessage.errors : innerMessage.Errors;
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ export const FeaturePanelComponent: React.FunctionComponent = () => {
|
||||||
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
{ key: "feature.hosteddataexplorerenabled", label: "Hosted Data Explorer (deprecated?)", value: "true" },
|
||||||
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
{ key: "feature.enablettl", label: "Enable TTL", value: "true" },
|
||||||
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
{ key: "feature.enablegallerypublish", label: "Enable Notebook Gallery Publishing", value: "true" },
|
||||||
|
{ key: "feature.enablecodeofconduct", label: "Enable Code Of Conduct Acknowledgement", value: "true" },
|
||||||
{
|
{
|
||||||
key: "feature.enableLinkInjection",
|
key: "feature.enableLinkInjection",
|
||||||
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
label: "Enable Injecting Notebook Viewer Link into the first cell",
|
||||||
|
|
|
@ -161,6 +161,12 @@ exports[`Feature panel renders all flags 1`] = `
|
||||||
label="Enable Notebook Gallery Publishing"
|
label="Enable Notebook Gallery Publishing"
|
||||||
onChange={[Function]}
|
onChange={[Function]}
|
||||||
/>
|
/>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
checked={false}
|
||||||
|
key="feature.enablecodeofconduct"
|
||||||
|
label="Enable Code Of Conduct Acknowledgement"
|
||||||
|
onChange={[Function]}
|
||||||
|
/>
|
||||||
<StyledCheckboxBase
|
<StyledCheckboxBase
|
||||||
checked={false}
|
checked={false}
|
||||||
key="feature.enableLinkInjection"
|
key="feature.enableLinkInjection"
|
||||||
|
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import * as sinon from "sinon";
|
||||||
|
import React from "react";
|
||||||
|
import { CodeOfConductComponent, CodeOfConductComponentProps } from "./CodeOfConductComponent";
|
||||||
|
import { IJunoResponse, JunoClient } from "../../../Juno/JunoClient";
|
||||||
|
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||||
|
|
||||||
|
describe("CodeOfConductComponent", () => {
|
||||||
|
let sandbox: sinon.SinonSandbox;
|
||||||
|
let codeOfConductProps: CodeOfConductComponentProps;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
sandbox.stub(JunoClient.prototype, "acceptCodeOfConduct").returns({
|
||||||
|
status: HttpStatusCodes.OK,
|
||||||
|
data: true
|
||||||
|
} as IJunoResponse<boolean>);
|
||||||
|
const junoClient = new JunoClient(undefined);
|
||||||
|
codeOfConductProps = {
|
||||||
|
junoClient: junoClient,
|
||||||
|
onAcceptCodeOfConduct: jest.fn()
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders", () => {
|
||||||
|
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("onAcceptedCodeOfConductCalled", async () => {
|
||||||
|
const wrapper = shallow(<CodeOfConductComponent {...codeOfConductProps} />);
|
||||||
|
wrapper
|
||||||
|
.find(".genericPaneSubmitBtn")
|
||||||
|
.first()
|
||||||
|
.simulate("click");
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(codeOfConductProps.onAcceptCodeOfConduct).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,112 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { JunoClient } from "../../../Juno/JunoClient";
|
||||||
|
import { HttpStatusCodes, CodeOfConductEndpoints } from "../../../Common/Constants";
|
||||||
|
import * as Logger from "../../../Common/Logger";
|
||||||
|
import { logConsoleError } from "../../../Utils/NotificationConsoleUtils";
|
||||||
|
import { Stack, Text, Checkbox, PrimaryButton, Link } from "office-ui-fabric-react";
|
||||||
|
|
||||||
|
export interface CodeOfConductComponentProps {
|
||||||
|
junoClient: JunoClient;
|
||||||
|
onAcceptCodeOfConduct: (result: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CodeOfConductComponentState {
|
||||||
|
readCodeOfConduct: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CodeOfConductComponent extends React.Component<CodeOfConductComponentProps, CodeOfConductComponentState> {
|
||||||
|
private descriptionPara1: string;
|
||||||
|
private descriptionPara2: string;
|
||||||
|
private descriptionPara3: string;
|
||||||
|
private link1: { label: string; url: string };
|
||||||
|
private link2: { label: string; url: string };
|
||||||
|
|
||||||
|
constructor(props: CodeOfConductComponentProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
readCodeOfConduct: false
|
||||||
|
};
|
||||||
|
|
||||||
|
this.descriptionPara1 = "Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement";
|
||||||
|
this.descriptionPara2 =
|
||||||
|
"Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.";
|
||||||
|
this.descriptionPara3 = "In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the ";
|
||||||
|
this.link1 = { label: "code of conduct", url: CodeOfConductEndpoints.codeOfConduct };
|
||||||
|
this.link2 = { label: "privacy statement", url: CodeOfConductEndpoints.privacyStatement };
|
||||||
|
}
|
||||||
|
|
||||||
|
private async acceptCodeOfConduct(): Promise<void> {
|
||||||
|
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`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.props.onAcceptCodeOfConduct(response.data);
|
||||||
|
} catch (error) {
|
||||||
|
const message = `Failed to accept code of conduct: ${error}`;
|
||||||
|
Logger.logError(message, "CodeOfConductComponent/acceptCodeOfConduct");
|
||||||
|
logConsoleError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onChangeCheckbox = (): void => {
|
||||||
|
this.setState({ readCodeOfConduct: !this.state.readCodeOfConduct });
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
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>
|
||||||
|
{" and "}
|
||||||
|
<Link href={this.link2.url} target="_blank">
|
||||||
|
{this.link2.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 accepted the code of conduct and privacy statement"
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -15,7 +15,7 @@ import {
|
||||||
} from "office-ui-fabric-react";
|
} from "office-ui-fabric-react";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as Logger from "../../../Common/Logger";
|
import * as Logger from "../../../Common/Logger";
|
||||||
import { IGalleryItem, JunoClient } from "../../../Juno/JunoClient";
|
import { IGalleryItem, JunoClient, IJunoResponse, IPublicGalleryData } 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";
|
||||||
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
|
||||||
|
@ -24,6 +24,8 @@ import { GalleryCardComponent, GalleryCardComponentProps } from "./Cards/Gallery
|
||||||
import "./GalleryViewerComponent.less";
|
import "./GalleryViewerComponent.less";
|
||||||
import { HttpStatusCodes } from "../../../Common/Constants";
|
import { HttpStatusCodes } from "../../../Common/Constants";
|
||||||
import Explorer from "../../Explorer";
|
import Explorer from "../../Explorer";
|
||||||
|
import { CodeOfConductComponent } from "./CodeOfConductComponent";
|
||||||
|
import { InfoComponent } from "./InfoComponent/InfoComponent";
|
||||||
|
|
||||||
export interface GalleryViewerComponentProps {
|
export interface GalleryViewerComponentProps {
|
||||||
container?: Explorer;
|
container?: Explorer;
|
||||||
|
@ -60,6 +62,7 @@ interface GalleryViewerComponentState {
|
||||||
sortBy: SortBy;
|
sortBy: SortBy;
|
||||||
searchText: string;
|
searchText: string;
|
||||||
dialogProps: DialogProps;
|
dialogProps: DialogProps;
|
||||||
|
isCodeOfConductAccepted: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GalleryTabInfo {
|
interface GalleryTabInfo {
|
||||||
|
@ -86,6 +89,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||||
private publicNotebooks: IGalleryItem[];
|
private publicNotebooks: IGalleryItem[];
|
||||||
private favoriteNotebooks: IGalleryItem[];
|
private favoriteNotebooks: IGalleryItem[];
|
||||||
private publishedNotebooks: IGalleryItem[];
|
private publishedNotebooks: IGalleryItem[];
|
||||||
|
private isCodeOfConductAccepted: boolean;
|
||||||
private columnCount: number;
|
private columnCount: number;
|
||||||
private rowCount: number;
|
private rowCount: number;
|
||||||
|
|
||||||
|
@ -100,7 +104,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||||
selectedTab: props.selectedTab,
|
selectedTab: props.selectedTab,
|
||||||
sortBy: props.sortBy,
|
sortBy: props.sortBy,
|
||||||
searchText: props.searchText,
|
searchText: props.searchText,
|
||||||
dialogProps: undefined
|
dialogProps: undefined,
|
||||||
|
isCodeOfConductAccepted: undefined
|
||||||
};
|
};
|
||||||
|
|
||||||
this.sortingOptions = [
|
this.sortingOptions = [
|
||||||
|
@ -134,9 +139,20 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||||
|
|
||||||
if (this.props.container?.isGalleryPublishEnabled()) {
|
if (this.props.container?.isGalleryPublishEnabled()) {
|
||||||
tabs.push(this.createTab(GalleryTab.PublicGallery, this.state.publicNotebooks));
|
tabs.push(
|
||||||
|
this.createPublicGalleryTab(
|
||||||
|
GalleryTab.PublicGallery,
|
||||||
|
this.state.publicNotebooks,
|
||||||
|
this.state.isCodeOfConductAccepted
|
||||||
|
)
|
||||||
|
);
|
||||||
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
tabs.push(this.createTab(GalleryTab.Favorites, this.state.favoriteNotebooks));
|
||||||
tabs.push(this.createTab(GalleryTab.Published, this.state.publishedNotebooks));
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pivotProps: IPivotProps = {
|
const pivotProps: IPivotProps = {
|
||||||
|
@ -167,6 +183,17 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createPublicGalleryTab(
|
||||||
|
tab: GalleryTab,
|
||||||
|
data: IGalleryItem[],
|
||||||
|
acceptedCodeOfConduct: boolean
|
||||||
|
): GalleryTabInfo {
|
||||||
|
return {
|
||||||
|
tab,
|
||||||
|
content: this.createPublicGalleryTabContent(data, acceptedCodeOfConduct)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
private createTab(tab: GalleryTab, data: IGalleryItem[]): GalleryTabInfo {
|
||||||
return {
|
return {
|
||||||
tab,
|
tab,
|
||||||
|
@ -174,6 +201,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private createPublicGalleryTabContent(data: IGalleryItem[], acceptedCodeOfConduct: boolean): JSX.Element {
|
||||||
|
return acceptedCodeOfConduct === false ? (
|
||||||
|
<CodeOfConductComponent
|
||||||
|
junoClient={this.props.junoClient}
|
||||||
|
onAcceptCodeOfConduct={(result: boolean) => {
|
||||||
|
this.setState({ isCodeOfConductAccepted: result });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
this.createTabContent(data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
||||||
return (
|
return (
|
||||||
<Stack tokens={{ childrenGap: 10 }}>
|
<Stack tokens={{ childrenGap: 10 }}>
|
||||||
|
@ -187,8 +227,12 @@ 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() && (
|
||||||
|
<Stack.Item>
|
||||||
|
<InfoComponent />
|
||||||
|
</Stack.Item>
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
{data && this.createCardsTabContent(data)}
|
{data && this.createCardsTabContent(data)}
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
@ -254,12 +298,19 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||||
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
private async loadPublicNotebooks(searchText: string, sortBy: SortBy, offline: boolean): Promise<void> {
|
||||||
if (!offline) {
|
if (!offline) {
|
||||||
try {
|
try {
|
||||||
const response = await this.props.junoClient.getPublicNotebooks();
|
let response: IJunoResponse<IPublicGalleryData> | IJunoResponse<IGalleryItem[]>;
|
||||||
|
if (this.props.container.isCodeOfConductEnabled()) {
|
||||||
|
response = await this.props.junoClient.fetchPublicNotebooks();
|
||||||
|
this.isCodeOfConductAccepted = response.data?.metadata.acceptedCodeOfConduct;
|
||||||
|
this.publicNotebooks = response.data?.notebooksData;
|
||||||
|
} else {
|
||||||
|
response = await this.props.junoClient.getPublicNotebooks();
|
||||||
|
this.publicNotebooks = response.data;
|
||||||
|
}
|
||||||
|
|
||||||
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
if (response.status !== HttpStatusCodes.OK && response.status !== HttpStatusCodes.NoContent) {
|
||||||
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
throw new Error(`Received HTTP ${response.status} when loading public notebooks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.publicNotebooks = response.data;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = `Failed to load public notebooks: ${error}`;
|
const message = `Failed to load public notebooks: ${error}`;
|
||||||
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
Logger.logError(message, "GalleryViewerComponent/loadPublicNotebooks");
|
||||||
|
@ -268,7 +319,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({
|
||||||
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))]
|
publicNotebooks: this.publicNotebooks && [...this.sort(sortBy, this.search(searchText, this.publicNotebooks))],
|
||||||
|
isCodeOfConductAccepted: this.isCodeOfConductAccepted
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
@import "../../../../../less/Common/Constants.less";
|
||||||
|
.infoPanel, .infoPanelMain {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoPanel {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel, .infoLabelMain {
|
||||||
|
padding-left: 5px
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoLabel {
|
||||||
|
font-weight: 400
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoIconMain {
|
||||||
|
color: @AccentMedium
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoIconMain:hover {
|
||||||
|
color: @BaseMedium
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { shallow } from "enzyme";
|
||||||
|
import React from "react";
|
||||||
|
import { InfoComponent } from "./InfoComponent";
|
||||||
|
|
||||||
|
describe("InfoComponent", () => {
|
||||||
|
it("renders", () => {
|
||||||
|
const wrapper = shallow(<InfoComponent />);
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,42 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import { Icon, Label, Stack, HoverCard, HoverCardType, Link } from "office-ui-fabric-react";
|
||||||
|
import { CodeOfConductEndpoints } from "../../../../Common/Constants";
|
||||||
|
import "./InfoComponent.less";
|
||||||
|
|
||||||
|
export class InfoComponent extends React.Component {
|
||||||
|
private getInfoPanel = (iconName: string, labelText: string, url: string): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Link href={url} target="_blank">
|
||||||
|
<div className="infoPanel">
|
||||||
|
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} />
|
||||||
|
<Label className="infoLabel">{labelText}</Label>
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
private onHover = (): JSX.Element => {
|
||||||
|
return (
|
||||||
|
<Stack tokens={{ childrenGap: 5, padding: 5 }}>
|
||||||
|
<Stack.Item>{this.getInfoPanel("Script", "Code of Conduct", CodeOfConductEndpoints.codeOfConduct)}</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{this.getInfoPanel("RedEye", "Privacy Statement", CodeOfConductEndpoints.privacyStatement)}
|
||||||
|
</Stack.Item>
|
||||||
|
<Stack.Item>
|
||||||
|
{this.getInfoPanel("KnowledgeArticle", "Microsoft Terms of Use", CodeOfConductEndpoints.termsOfUse)}
|
||||||
|
</Stack.Item>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
public render(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<HoverCard plainCardProps={{ onRenderPlainCard: this.onHover }} instantOpenOnClick type={HoverCardType.plain}>
|
||||||
|
<div className="infoPanelMain">
|
||||||
|
<Icon className="infoIconMain" iconName="Help" styles={{ root: { verticalAlign: "middle" } }} />
|
||||||
|
<Label className="infoLabelMain">Help</Label>
|
||||||
|
</div>
|
||||||
|
</HoverCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`InfoComponent renders 1`] = `
|
||||||
|
<StyledHoverCardBase
|
||||||
|
instantOpenOnClick={true}
|
||||||
|
plainCardProps={
|
||||||
|
Object {
|
||||||
|
"onRenderPlainCard": [Function],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type="PlainCard"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="infoPanelMain"
|
||||||
|
>
|
||||||
|
<Memo(StyledIconBase)
|
||||||
|
className="infoIconMain"
|
||||||
|
iconName="Help"
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"root": Object {
|
||||||
|
"verticalAlign": "middle",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<StyledLabelBase
|
||||||
|
className="infoLabelMain"
|
||||||
|
>
|
||||||
|
Help
|
||||||
|
</StyledLabelBase>
|
||||||
|
</div>
|
||||||
|
</StyledHoverCardBase>
|
||||||
|
`;
|
|
@ -0,0 +1,75 @@
|
||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`CodeOfConductComponent renders 1`] = `
|
||||||
|
<Stack
|
||||||
|
tokens={
|
||||||
|
Object {
|
||||||
|
"childrenGap": 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<StackItem>
|
||||||
|
<Text
|
||||||
|
style={
|
||||||
|
Object {
|
||||||
|
"fontSize": "20px",
|
||||||
|
"fontWeight": 500,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Azure CosmosDB Notebook Gallery - Code of Conduct and Privacy Statement
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<Text>
|
||||||
|
Azure Cosmos DB Notebook Public Gallery contains notebook samples shared by users of Cosmos DB.
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<Text>
|
||||||
|
In order to access Azure Cosmos DB Notebook Gallery resources, you must accept the
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/cosmos-code-of-conduct"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
code of conduct
|
||||||
|
</StyledLinkBase>
|
||||||
|
and
|
||||||
|
<StyledLinkBase
|
||||||
|
href="https://aka.ms/ms-privacy-policy"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
privacy statement
|
||||||
|
</StyledLinkBase>
|
||||||
|
</Text>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<StyledCheckboxBase
|
||||||
|
label="I have read and accepted the code of conduct and privacy statement"
|
||||||
|
onChange={[Function]}
|
||||||
|
styles={
|
||||||
|
Object {
|
||||||
|
"label": Object {
|
||||||
|
"margin": 0,
|
||||||
|
"padding": "2 0 2 0",
|
||||||
|
},
|
||||||
|
"text": Object {
|
||||||
|
"fontSize": 12,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
<StackItem>
|
||||||
|
<CustomizedPrimaryButton
|
||||||
|
ariaLabel="Continue"
|
||||||
|
className="genericPaneSubmitBtn"
|
||||||
|
disabled={true}
|
||||||
|
onClick={[Function]}
|
||||||
|
tabIndex={0}
|
||||||
|
text="Continue"
|
||||||
|
title="Continue"
|
||||||
|
/>
|
||||||
|
</StackItem>
|
||||||
|
</Stack>
|
||||||
|
`;
|
|
@ -206,6 +206,7 @@ export default class Explorer {
|
||||||
|
|
||||||
// features
|
// features
|
||||||
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
public isGalleryPublishEnabled: ko.Computed<boolean>;
|
||||||
|
public isCodeOfConductEnabled: ko.Computed<boolean>;
|
||||||
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
public isLinkInjectionEnabled: ko.Computed<boolean>;
|
||||||
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
public isGitHubPaneEnabled: ko.Observable<boolean>;
|
||||||
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
public isPublishNotebookPaneEnabled: ko.Observable<boolean>;
|
||||||
|
@ -409,6 +410,9 @@ export default class Explorer {
|
||||||
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
this.isGalleryPublishEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
this.isFeatureEnabled(Constants.Features.enableGalleryPublish)
|
||||||
);
|
);
|
||||||
|
this.isCodeOfConductEnabled = ko.computed<boolean>(() =>
|
||||||
|
this.isFeatureEnabled(Constants.Features.enableCodeOfConduct)
|
||||||
|
);
|
||||||
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
this.isLinkInjectionEnabled = ko.computed<boolean>(() =>
|
||||||
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
this.isFeatureEnabled(Constants.Features.enableLinkInjection)
|
||||||
);
|
);
|
||||||
|
@ -2356,9 +2360,15 @@ export default class Explorer {
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
public publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): void {
|
public async publishNotebook(name: string, content: string | unknown, parentDomElement: HTMLElement): Promise<void> {
|
||||||
if (this.notebookManager) {
|
if (this.notebookManager) {
|
||||||
this.notebookManager.openPublishNotebookPane(name, content, parentDomElement, this.isLinkInjectionEnabled());
|
await this.notebookManager.openPublishNotebookPane(
|
||||||
|
name,
|
||||||
|
content,
|
||||||
|
parentDomElement,
|
||||||
|
this.isCodeOfConductEnabled(),
|
||||||
|
this.isLinkInjectionEnabled()
|
||||||
|
);
|
||||||
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
this.publishNotebookPaneAdapter = this.notebookManager.publishNotebookPaneAdapter;
|
||||||
this.isPublishNotebookPaneEnabled(true);
|
this.isPublishNotebookPaneEnabled(true);
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,13 +108,21 @@ export default class NotebookManager {
|
||||||
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
this.junoClient.getPinnedRepos(this.gitHubOAuthService.getTokenObservable()()?.scope);
|
||||||
}
|
}
|
||||||
|
|
||||||
public openPublishNotebookPane(
|
public async openPublishNotebookPane(
|
||||||
name: string,
|
name: string,
|
||||||
content: string | ImmutableNotebook,
|
content: string | ImmutableNotebook,
|
||||||
parentDomElement: HTMLElement,
|
parentDomElement: HTMLElement,
|
||||||
|
isCodeOfConductEnabled: boolean,
|
||||||
isLinkInjectionEnabled: boolean
|
isLinkInjectionEnabled: boolean
|
||||||
): void {
|
): Promise<void> {
|
||||||
this.publishNotebookPaneAdapter.open(name, getFullName(), content, parentDomElement, isLinkInjectionEnabled);
|
await this.publishNotebookPaneAdapter.open(
|
||||||
|
name,
|
||||||
|
getFullName(),
|
||||||
|
content,
|
||||||
|
parentDomElement,
|
||||||
|
isCodeOfConductEnabled,
|
||||||
|
isLinkInjectionEnabled
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Octokit's error handler uses any
|
// Octokit's error handler uses any
|
||||||
|
|
|
@ -43,10 +43,8 @@ const notebookRecord = makeNotebookRecord({
|
||||||
source: 'display(HTML("<h1>Sample html</h1>"))',
|
source: 'display(HTML("<h1>Sample html</h1>"))',
|
||||||
outputs: List.of({
|
outputs: List.of({
|
||||||
data: Object.freeze({
|
data: Object.freeze({
|
||||||
data: {
|
"text/html": "<h1>Sample output</h1>",
|
||||||
"text/html": "<h1>Sample output</h1>",
|
"text/plain": "<IPython.core.display.HTML object>"
|
||||||
"text/plain": "<IPython.core.display.HTML object>"
|
|
||||||
}
|
|
||||||
} as MediaBundle),
|
} as MediaBundle),
|
||||||
output_type: "display_data",
|
output_type: "display_data",
|
||||||
metadata: undefined
|
metadata: undefined
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { ImmutableNotebook, ImmutableCodeCell, ImmutableOutput } from "@nteract/commutable";
|
import { ImmutableNotebook, ImmutableCodeCell } from "@nteract/commutable";
|
||||||
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
import { NotebookContentItem, NotebookContentItemType } from "./NotebookContentItem";
|
||||||
import { StringUtils } from "../../Utils/StringUtils";
|
import { StringUtils } from "../../Utils/StringUtils";
|
||||||
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
import * as GitHubUtils from "../../Utils/GitHubUtils";
|
||||||
|
@ -102,25 +102,19 @@ export class NotebookUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
|
public static findFirstCodeCellWithDisplay(notebookObject: ImmutableNotebook): number {
|
||||||
let codeCellCount = -1;
|
let codeCellIndex = 0;
|
||||||
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
|
for (let i = 0; i < notebookObject.cellOrder.size; i++) {
|
||||||
const cellId = notebookObject.cellOrder.get(i);
|
const cellId = notebookObject.cellOrder.get(i);
|
||||||
if (cellId) {
|
if (cellId) {
|
||||||
const cell = notebookObject.cellMap.get(cellId);
|
const cell = notebookObject.cellMap.get(cellId);
|
||||||
if (cell && cell.cell_type === "code") {
|
if (cell?.cell_type === "code") {
|
||||||
codeCellCount++;
|
const displayOutput = (cell as ImmutableCodeCell)?.outputs?.find(
|
||||||
const codeCell = cell as ImmutableCodeCell;
|
output => output.output_type === "display_data" || output.output_type === "execute_result"
|
||||||
if (codeCell.outputs) {
|
);
|
||||||
const displayOutput = codeCell.outputs.find((output: ImmutableOutput) => {
|
if (displayOutput) {
|
||||||
if (output.output_type === "display_data" || output.output_type === "execute_result") {
|
return codeCellIndex;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
if (displayOutput) {
|
|
||||||
return codeCellCount;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
codeCellIndex++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,6 +17,7 @@ export interface GenericRightPaneProps {
|
||||||
onSubmit: () => void;
|
onSubmit: () => void;
|
||||||
submitButtonText: string;
|
submitButtonText: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
isSubmitButtonVisible?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenericRightPaneState {
|
export interface GenericRightPaneState {
|
||||||
|
@ -108,6 +109,7 @@ export class GenericRightPaneComponent extends React.Component<GenericRightPaneP
|
||||||
<div className="paneFooter">
|
<div className="paneFooter">
|
||||||
<div className="leftpanel-okbut">
|
<div className="leftpanel-okbut">
|
||||||
<PrimaryButton
|
<PrimaryButton
|
||||||
|
style={{ visibility: this.props.isSubmitButtonVisible ? "visible" : "hidden" }}
|
||||||
ariaLabel="Submit"
|
ariaLabel="Submit"
|
||||||
title="Submit"
|
title="Submit"
|
||||||
onClick={this.props.onSubmit}
|
onClick={this.props.onSubmit}
|
||||||
|
|
|
@ -10,6 +10,8 @@ import { GenericRightPaneComponent, GenericRightPaneProps } from "./GenericRight
|
||||||
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
import { PublishNotebookPaneComponent, PublishNotebookPaneProps } from "./PublishNotebookPaneComponent";
|
||||||
import { ImmutableNotebook } from "@nteract/commutable/src";
|
import { ImmutableNotebook } from "@nteract/commutable/src";
|
||||||
import { toJS } from "@nteract/commutable";
|
import { toJS } from "@nteract/commutable";
|
||||||
|
import { CodeOfConductComponent } from "../Controls/NotebookGallery/CodeOfConductComponent";
|
||||||
|
import { HttpStatusCodes } from "../../Common/Constants";
|
||||||
|
|
||||||
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
parameters: ko.Observable<number>;
|
parameters: ko.Observable<number>;
|
||||||
|
@ -26,6 +28,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
private imageSrc: string;
|
private imageSrc: string;
|
||||||
private notebookObject: ImmutableNotebook;
|
private notebookObject: ImmutableNotebook;
|
||||||
private parentDomElement: HTMLElement;
|
private parentDomElement: HTMLElement;
|
||||||
|
private isCodeOfConductAccepted: boolean;
|
||||||
private isLinkInjectionEnabled: boolean;
|
private isLinkInjectionEnabled: boolean;
|
||||||
|
|
||||||
constructor(private container: Explorer, private junoClient: JunoClient) {
|
constructor(private container: Explorer, private junoClient: JunoClient) {
|
||||||
|
@ -49,7 +52,8 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
title: "Publish to gallery",
|
title: "Publish to gallery",
|
||||||
submitButtonText: "Publish",
|
submitButtonText: "Publish",
|
||||||
onClose: () => this.close(),
|
onClose: () => this.close(),
|
||||||
onSubmit: () => this.submit()
|
onSubmit: () => this.submit(),
|
||||||
|
isSubmitButtonVisible: this.isCodeOfConductAccepted
|
||||||
};
|
};
|
||||||
|
|
||||||
return <GenericRightPaneComponent {...props} />;
|
return <GenericRightPaneComponent {...props} />;
|
||||||
|
@ -59,13 +63,31 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||||
}
|
}
|
||||||
|
|
||||||
public open(
|
public async open(
|
||||||
name: string,
|
name: string,
|
||||||
author: string,
|
author: string,
|
||||||
notebookContent: string | ImmutableNotebook,
|
notebookContent: string | ImmutableNotebook,
|
||||||
parentDomElement: HTMLElement,
|
parentDomElement: HTMLElement,
|
||||||
|
isCodeOfConductEnabled: boolean,
|
||||||
isLinkInjectionEnabled: boolean
|
isLinkInjectionEnabled: boolean
|
||||||
): void {
|
): Promise<void> {
|
||||||
|
if (isCodeOfConductEnabled) {
|
||||||
|
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) {
|
||||||
|
const message = `Failed to check if code of conduct was accepted: ${error}`;
|
||||||
|
Logger.logError(message, "PublishNotebookPaneAdapter/isCodeOfConductAccepted");
|
||||||
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.isCodeOfConductAccepted = true;
|
||||||
|
}
|
||||||
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.author = author;
|
this.author = author;
|
||||||
if (typeof notebookContent === "string") {
|
if (typeof notebookContent === "string") {
|
||||||
|
@ -108,11 +130,9 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
this.content,
|
this.content,
|
||||||
this.isLinkInjectionEnabled
|
this.isLinkInjectionEnabled
|
||||||
);
|
);
|
||||||
if (!response.data) {
|
if (response.data) {
|
||||||
throw new Error(`Received HTTP ${response.status} when publishing ${name} to gallery`);
|
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
||||||
}
|
}
|
||||||
|
|
||||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Info, `Published ${name} to gallery`);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.formError = `Failed to publish ${this.name} to gallery`;
|
this.formError = `Failed to publish ${this.name} to gallery`;
|
||||||
this.formErrorDetail = `${error}`;
|
this.formErrorDetail = `${error}`;
|
||||||
|
@ -162,7 +182,19 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
clearFormError: this.clearFormError
|
clearFormError: this.clearFormError
|
||||||
};
|
};
|
||||||
|
|
||||||
return <PublishNotebookPaneComponent {...publishNotebookPaneProps} />;
|
return !this.isCodeOfConductAccepted ? (
|
||||||
|
<div style={{ padding: "15px", marginTop: "10px" }}>
|
||||||
|
<CodeOfConductComponent
|
||||||
|
junoClient={this.junoClient}
|
||||||
|
onAcceptCodeOfConduct={() => {
|
||||||
|
this.isCodeOfConductAccepted = true;
|
||||||
|
this.triggerRender();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<PublishNotebookPaneComponent {...publishNotebookPaneProps} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
private reset = (): void => {
|
private reset = (): void => {
|
||||||
|
@ -178,5 +210,7 @@ export class PublishNotebookPaneAdapter implements ReactAdapter {
|
||||||
this.imageSrc = undefined;
|
this.imageSrc = undefined;
|
||||||
this.notebookObject = undefined;
|
this.notebookObject = undefined;
|
||||||
this.parentDomElement = undefined;
|
this.parentDomElement = undefined;
|
||||||
|
this.isCodeOfConductAccepted = undefined;
|
||||||
|
this.isLinkInjectionEnabled = undefined;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,7 +166,7 @@ export default class NotebookTabV2 extends TabsBase {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
iconName: "PublishContent",
|
iconName: "PublishContent",
|
||||||
onCommandClick: () => this.publishToGallery(),
|
onCommandClick: async () => await this.publishToGallery(),
|
||||||
commandButtonLabel: publishLabel,
|
commandButtonLabel: publishLabel,
|
||||||
hasPopup: false,
|
hasPopup: false,
|
||||||
disabled: false,
|
disabled: false,
|
||||||
|
@ -456,9 +456,9 @@ export default class NotebookTabV2 extends TabsBase {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private publishToGallery = () => {
|
private publishToGallery = async () => {
|
||||||
const notebookContent = this.notebookComponentAdapter.getContent();
|
const notebookContent = this.notebookComponentAdapter.getContent();
|
||||||
this.container.publishNotebook(
|
await this.container.publishNotebook(
|
||||||
notebookContent.name,
|
notebookContent.name,
|
||||||
notebookContent.content,
|
notebookContent.content,
|
||||||
this.notebookComponentAdapter.getNotebookParentElement()
|
this.notebookComponentAdapter.getNotebookParentElement()
|
||||||
|
|
|
@ -39,6 +39,15 @@ export interface IGalleryItem {
|
||||||
newCellId: string;
|
newCellId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface IPublicGalleryData {
|
||||||
|
metadata: IPublicGalleryMetaData;
|
||||||
|
notebooksData: IGalleryItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPublicGalleryMetaData {
|
||||||
|
acceptedCodeOfConduct: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IUserGallery {
|
export interface IUserGallery {
|
||||||
favorites: string[];
|
favorites: string[];
|
||||||
published: string[];
|
published: string[];
|
||||||
|
@ -163,6 +172,61 @@ export class JunoClient {
|
||||||
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
return this.getNotebooks(`${this.getNotebooksUrl()}/gallery/public`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// will be renamed once feature.enableCodeOfConduct flag is removed
|
||||||
|
public async fetchPublicNotebooks(): Promise<IJunoResponse<IPublicGalleryData>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/public`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: IPublicGalleryData;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async acceptCodeOfConduct(): Promise<IJunoResponse<boolean>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/acceptCodeOfConduct`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: boolean;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async isCodeOfConductAccepted(): Promise<IJunoResponse<boolean>> {
|
||||||
|
const url = `${this.getNotebooksAccountUrl()}/gallery/isCodeOfConductAccepted`;
|
||||||
|
const response = await window.fetch(url, {
|
||||||
|
method: "PATCH",
|
||||||
|
headers: JunoClient.getHeaders()
|
||||||
|
});
|
||||||
|
|
||||||
|
let data: boolean;
|
||||||
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
|
data = await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: response.status,
|
||||||
|
data
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public async getNotebookInfo(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
public async getNotebookInfo(id: string): Promise<IJunoResponse<IGalleryItem>> {
|
||||||
const response = await window.fetch(this.getNotebookInfoUrl(id));
|
const response = await window.fetch(this.getNotebookInfoUrl(id));
|
||||||
|
|
||||||
|
@ -299,7 +363,6 @@ export class JunoClient {
|
||||||
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, {
|
const response = await window.fetch(`${this.getNotebooksAccountUrl()}/gallery`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: JunoClient.getHeaders(),
|
headers: JunoClient.getHeaders(),
|
||||||
|
|
||||||
body: isLinkInjectionEnabled
|
body: isLinkInjectionEnabled
|
||||||
? JSON.stringify({
|
? JSON.stringify({
|
||||||
name,
|
name,
|
||||||
|
@ -323,6 +386,8 @@ export class JunoClient {
|
||||||
let data: IGalleryItem;
|
let data: IGalleryItem;
|
||||||
if (response.status === HttpStatusCodes.OK) {
|
if (response.status === HttpStatusCodes.OK) {
|
||||||
data = await response.json();
|
data = await response.json();
|
||||||
|
} else {
|
||||||
|
throw new Error(`HTTP status ${response.status} thrown. ${(await response.json()).Message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
Loading…
Reference in New Issue