mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-01-07 11:36:47 +00:00
Compare commits
7 Commits
PROD-2020-
...
MPAC-2020-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1e20796c2 | ||
|
|
543ae9fe4a | ||
|
|
db0b478eb0 | ||
|
|
f6938f5ec5 | ||
|
|
9affc34301 | ||
|
|
15953da51e | ||
|
|
15f9146ac9 |
@@ -271,7 +271,6 @@ src/Shared/AddCollectionUtility.test.ts
|
||||
src/Shared/AddCollectionUtility.ts
|
||||
src/Shared/AddDatabaseUtility.test.ts
|
||||
src/Shared/AddDatabaseUtility.ts
|
||||
src/Shared/Ajax.ts
|
||||
src/Shared/Constants.ts
|
||||
src/Shared/DefaultExperienceUtility.test.ts
|
||||
src/Shared/DefaultExperienceUtility.ts
|
||||
|
||||
24
.github/workflows/ci.yml
vendored
24
.github/workflows/ci.yml
vendored
@@ -155,8 +155,31 @@ jobs:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
|
||||
CYPRESS_CONNECTION_STRING: ${{ secrets.CONNECTION_STRING_MONGO }}
|
||||
accessibility:
|
||||
name: "Accessibility | Hosted"
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Use Node.js 12.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12.x
|
||||
- name: Accessibility Check
|
||||
run: |
|
||||
# Ubuntu gets mad when webpack runs too many files watchers
|
||||
cat /proc/sys/fs/inotify/max_user_watches
|
||||
sudo sysctl fs.inotify.max_user_watches=524288
|
||||
sudo sysctl -p
|
||||
npm ci
|
||||
npm start &
|
||||
npx wait-on -i 5000 https-get://0.0.0.0:1234/
|
||||
node utils/accesibilityCheck.js
|
||||
shell: bash
|
||||
env:
|
||||
NODE_TLS_REJECT_UNAUTHORIZED: 0
|
||||
nuget:
|
||||
name: Publish Nuget
|
||||
if: github.ref == 'refs/heads/master'
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
@@ -180,6 +203,7 @@ jobs:
|
||||
path: "*.nupkg"
|
||||
nugetmpac:
|
||||
name: Publish Nuget MPAC
|
||||
if: github.ref == 'refs/heads/master'
|
||||
needs: [lint, format, compile, build, unittest, endtoendemulator, endtoendsql, endtoendmongo]
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
||||
15
package-lock.json
generated
15
package-lock.json
generated
@@ -9230,6 +9230,21 @@
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.9.1.tgz",
|
||||
"integrity": "sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug=="
|
||||
},
|
||||
"axe-core": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-3.5.5.tgz",
|
||||
"integrity": "sha512-5P0QZ6J5xGikH780pghEdbEKijCTrruK9KxtPZCFWUpef0f6GipO+xEZ5GKCb020mmqgbiNO6TcA55CriL784Q==",
|
||||
"dev": true
|
||||
},
|
||||
"axe-puppeteer": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axe-puppeteer/-/axe-puppeteer-1.1.0.tgz",
|
||||
"integrity": "sha512-VS17Y1rDQe6A0PdeTPxwOSBfmOdj6efgxyre9cN1du1snnVilczSDtQsgifBKBlzoL/3DKfGpgIi+N+zrzODOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"axe-core": "^3.5.3"
|
||||
}
|
||||
},
|
||||
"babel-code-frame": {
|
||||
"version": "6.26.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.26.0.tgz",
|
||||
|
||||
@@ -116,6 +116,7 @@
|
||||
"@typescript-eslint/eslint-plugin": "3.2.0",
|
||||
"@typescript-eslint/parser": "3.2.0",
|
||||
"adal-angular": "1.0.15",
|
||||
"axe-puppeteer": "1.1.0",
|
||||
"babel-jest": "24.9.0",
|
||||
"babel-loader": "8.1.0",
|
||||
"buffer": "5.1.0",
|
||||
|
||||
@@ -553,6 +553,7 @@ export interface GraphParameters extends RpParameters {
|
||||
pk: string;
|
||||
coll: string;
|
||||
cd: Boolean;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
}
|
||||
|
||||
export interface CreationRequest {
|
||||
@@ -570,6 +571,7 @@ export interface SqlCollectionParameters extends RpParameters {
|
||||
coll: string;
|
||||
cd: Boolean;
|
||||
analyticalStorageTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
}
|
||||
|
||||
export interface MongoCreationRequest extends CreationRequest {
|
||||
@@ -588,6 +590,7 @@ export interface GraphCreationRequest extends CreationRequest {
|
||||
resource: {
|
||||
id: string;
|
||||
partitionKey: {};
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
};
|
||||
options: RpOptions;
|
||||
};
|
||||
@@ -613,6 +616,7 @@ export interface SqlCollectionCreationRequest extends CreationRequest {
|
||||
id: string;
|
||||
partitionKey: {};
|
||||
analyticalStorageTtl?: number;
|
||||
indexingPolicy?: IndexingPolicy;
|
||||
};
|
||||
options: RpOptions;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Dialog, DialogType, DialogFooter, IDialogProps } from "office-ui-fabric
|
||||
import { IButtonProps, PrimaryButton, DefaultButton } from "office-ui-fabric-react/lib/Button";
|
||||
import { ITextFieldProps, TextField } from "office-ui-fabric-react/lib/TextField";
|
||||
import { Link } from "office-ui-fabric-react/lib/Link";
|
||||
import { FontIcon } from "office-ui-fabric-react";
|
||||
|
||||
export interface TextFieldProps extends ITextFieldProps {
|
||||
label: string;
|
||||
@@ -84,7 +85,7 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
|
||||
{textFieldProps && <TextField {...textFieldProps} />}
|
||||
{linkProps && (
|
||||
<Link href={linkProps.linkUrl} target="_blank">
|
||||
{linkProps.linkText}
|
||||
{linkProps.linkText} <FontIcon iconName="NavigateExternalInline" />
|
||||
</Link>
|
||||
)}
|
||||
<DialogFooter>
|
||||
|
||||
@@ -5,11 +5,9 @@ export class GalleryHeaderComponent extends React.Component {
|
||||
private static readonly azureText = "Microsoft Azure";
|
||||
private static readonly cosmosdbText = "Cosmos DB";
|
||||
private static readonly galleryText = "Gallery";
|
||||
private static readonly loginText = "Log in";
|
||||
private static readonly loginText = "Sign In";
|
||||
private static readonly openPortal = () => window.open("https://portal.azure.com", "_blank");
|
||||
private static readonly openDataExplorer = () => (window.location.href = new URL("./", window.location.href).href);
|
||||
private static readonly openGallery = () =>
|
||||
(window.location.href = new URL("./galleryViewer.html", window.location.href).href);
|
||||
private static readonly headerItemStyle: React.CSSProperties = {
|
||||
color: "white"
|
||||
};
|
||||
@@ -63,7 +61,7 @@ export class GalleryHeaderComponent extends React.Component {
|
||||
<Stack.Item>
|
||||
{this.renderHeaderItem(
|
||||
GalleryHeaderComponent.galleryText,
|
||||
GalleryHeaderComponent.openGallery,
|
||||
undefined,
|
||||
GalleryHeaderComponent.headerItemTextProps
|
||||
)}
|
||||
</Stack.Item>
|
||||
|
||||
@@ -20,6 +20,7 @@ describe("GalleryCardComponent", () => {
|
||||
views: 0
|
||||
},
|
||||
isFavorite: false,
|
||||
showDownload: true,
|
||||
showDelete: true,
|
||||
onClick: undefined,
|
||||
onTagClick: undefined,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Card, ICardTokens } from "@uifabric/react-cards";
|
||||
import { Card } from "@uifabric/react-cards";
|
||||
import {
|
||||
FontWeights,
|
||||
Icon,
|
||||
@@ -18,10 +18,12 @@ import * as React from "react";
|
||||
import { IGalleryItem } from "../../../../Juno/JunoClient";
|
||||
import { FileSystemUtil } from "../../../Notebook/FileSystemUtil";
|
||||
import CosmosDBLogo from "../../../../../images/CosmosDB-logo.svg";
|
||||
import { StyleConstants } from "../../../../Common/Constants";
|
||||
|
||||
export interface GalleryCardComponentProps {
|
||||
data: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
showDownload: boolean;
|
||||
showDelete: boolean;
|
||||
onClick: () => void;
|
||||
onTagClick: (tag: string) => void;
|
||||
@@ -32,30 +34,30 @@ export interface GalleryCardComponentProps {
|
||||
}
|
||||
|
||||
export class GalleryCardComponent extends React.Component<GalleryCardComponentProps> {
|
||||
public static readonly CARD_HEIGHT = 384;
|
||||
public static readonly CARD_WIDTH = 256;
|
||||
|
||||
private static readonly cardImageHeight = 144;
|
||||
private static readonly cardDescriptionMaxChars = 88;
|
||||
private static readonly cardTokens: ICardTokens = {
|
||||
width: GalleryCardComponent.CARD_WIDTH,
|
||||
height: GalleryCardComponent.CARD_HEIGHT,
|
||||
childrenGap: 8,
|
||||
childrenMargin: 10
|
||||
};
|
||||
private static readonly cardItemGapBig = 10;
|
||||
private static readonly cardItemGapSmall = 8;
|
||||
|
||||
public render(): JSX.Element {
|
||||
const cardButtonsVisible = this.props.isFavorite !== undefined || this.props.showDownload || this.props.showDelete;
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric"
|
||||
};
|
||||
|
||||
const dateString = new Date(this.props.data.created).toLocaleString("default", options);
|
||||
const cardTitle = FileSystemUtil.stripExtension(this.props.data.name, "ipynb");
|
||||
|
||||
return (
|
||||
<Card aria-label="Notebook Card" tokens={GalleryCardComponent.cardTokens} onClick={this.props.onClick}>
|
||||
<Card.Item>
|
||||
<Card
|
||||
aria-label={cardTitle}
|
||||
data-is-focusable="true"
|
||||
tokens={{ width: GalleryCardComponent.CARD_WIDTH, childrenGap: 0 }}
|
||||
onClick={event => this.onClick(event, this.props.onClick)}
|
||||
>
|
||||
<Card.Item tokens={{ padding: GalleryCardComponent.cardItemGapBig }}>
|
||||
<Persona
|
||||
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
||||
text={this.props.data.author}
|
||||
@@ -63,69 +65,89 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
/>
|
||||
</Card.Item>
|
||||
|
||||
<Card.Item fill>
|
||||
<Card.Item>
|
||||
<Image
|
||||
src={
|
||||
this.props.data.thumbnailUrl ||
|
||||
`https://placehold.it/${GalleryCardComponent.CARD_WIDTH}x${GalleryCardComponent.cardImageHeight}`
|
||||
}
|
||||
src={this.props.data.thumbnailUrl}
|
||||
width={GalleryCardComponent.CARD_WIDTH}
|
||||
height={GalleryCardComponent.cardImageHeight}
|
||||
imageFit={ImageFit.cover}
|
||||
alt="Notebook cover image"
|
||||
alt={`${cardTitle} cover image`}
|
||||
/>
|
||||
</Card.Item>
|
||||
|
||||
<Card.Section>
|
||||
<Card.Section styles={{ root: { padding: GalleryCardComponent.cardItemGapBig } }}>
|
||||
<Text variant="small" nowrap>
|
||||
{this.props.data.tags?.map((tag, index, array) => (
|
||||
<span key={tag}>
|
||||
<Link onClick={(event): void => this.onTagClick(event, tag)}>{tag}</Link>
|
||||
<Link onClick={event => this.onClick(event, () => this.props.onTagClick(tag))}>{tag}</Link>
|
||||
{index === array.length - 1 ? <></> : ", "}
|
||||
</span>
|
||||
))}
|
||||
</Text>
|
||||
<Text styles={{ root: { fontWeight: FontWeights.semibold } }} nowrap>
|
||||
{FileSystemUtil.stripExtension(this.props.data.name, "ipynb")}
|
||||
|
||||
<Text
|
||||
styles={{
|
||||
root: {
|
||||
fontWeight: FontWeights.semibold,
|
||||
paddingTop: GalleryCardComponent.cardItemGapSmall,
|
||||
paddingBottom: GalleryCardComponent.cardItemGapSmall
|
||||
}
|
||||
}}
|
||||
nowrap
|
||||
>
|
||||
{cardTitle}
|
||||
</Text>
|
||||
|
||||
<Text variant="small" styles={{ root: { height: 36 } }}>
|
||||
{this.props.data.description.substr(0, GalleryCardComponent.cardDescriptionMaxChars)}
|
||||
</Text>
|
||||
|
||||
<span>
|
||||
{this.generateIconText("RedEye", this.props.data.views.toString())}
|
||||
{this.generateIconText("Download", this.props.data.downloads.toString())}
|
||||
{this.props.isFavorite !== undefined &&
|
||||
this.generateIconText("Heart", this.props.data.favorites.toString())}
|
||||
</span>
|
||||
</Card.Section>
|
||||
|
||||
<Card.Section horizontal styles={{ root: { alignItems: "flex-end" } }}>
|
||||
{this.generateIconText("RedEye", this.props.data.views.toString())}
|
||||
{this.generateIconText("Download", this.props.data.downloads.toString())}
|
||||
{this.props.isFavorite !== undefined && this.generateIconText("Heart", this.props.data.favorites.toString())}
|
||||
</Card.Section>
|
||||
{cardButtonsVisible && (
|
||||
<Card.Section
|
||||
styles={{
|
||||
root: {
|
||||
marginLeft: GalleryCardComponent.cardItemGapBig,
|
||||
marginRight: GalleryCardComponent.cardItemGapBig
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||
|
||||
<Card.Item>
|
||||
<Separator styles={{ root: { padding: 0, height: 1 } }} />
|
||||
</Card.Item>
|
||||
<span>
|
||||
{this.props.isFavorite !== undefined &&
|
||||
this.generateIconButtonWithTooltip(
|
||||
this.props.isFavorite ? "HeartFill" : "Heart",
|
||||
this.props.isFavorite ? "Unlike" : "Like",
|
||||
"left",
|
||||
this.props.isFavorite ? this.props.onUnfavoriteClick : this.props.onFavoriteClick
|
||||
)}
|
||||
|
||||
<Card.Section horizontal styles={{ root: { marginTop: 0 } }}>
|
||||
{this.props.isFavorite !== undefined &&
|
||||
this.generateIconButtonWithTooltip(
|
||||
this.props.isFavorite ? "HeartFill" : "Heart",
|
||||
this.props.isFavorite ? "Unlike" : "Like",
|
||||
this.props.isFavorite ? this.onUnfavoriteClick : this.onFavoriteClick
|
||||
)}
|
||||
{this.props.showDownload &&
|
||||
this.generateIconButtonWithTooltip("Download", "Download", "left", this.props.onDownloadClick)}
|
||||
|
||||
{this.generateIconButtonWithTooltip("Download", "Download", this.onDownloadClick)}
|
||||
|
||||
{this.props.showDelete && (
|
||||
<div style={{ width: "100%", textAlign: "right" }}>
|
||||
{this.generateIconButtonWithTooltip("Delete", "Remove", this.onDeleteClick)}
|
||||
</div>
|
||||
)}
|
||||
</Card.Section>
|
||||
{this.props.showDelete &&
|
||||
this.generateIconButtonWithTooltip("Delete", "Remove", "right", this.props.onDeleteClick)}
|
||||
</span>
|
||||
</Card.Section>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
private generateIconText = (iconName: string, text: string): JSX.Element => {
|
||||
return (
|
||||
<Text variant="tiny" styles={{ root: { color: "#ccc" } }}>
|
||||
<Text
|
||||
variant="tiny"
|
||||
styles={{ root: { color: StyleConstants.BaseMedium, paddingRight: GalleryCardComponent.cardItemGapSmall } }}
|
||||
>
|
||||
<Icon iconName={iconName} styles={{ root: { verticalAlign: "middle" } }} /> {text}
|
||||
</Text>
|
||||
);
|
||||
@@ -138,70 +160,37 @@ export class GalleryCardComponent extends React.Component<GalleryCardComponentPr
|
||||
private generateIconButtonWithTooltip = (
|
||||
iconName: string,
|
||||
title: string,
|
||||
onClick: (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
) => void
|
||||
horizontalAlign: "right" | "left",
|
||||
activate: () => void
|
||||
): JSX.Element => {
|
||||
return (
|
||||
<TooltipHost
|
||||
content={title}
|
||||
id={`TooltipHost-IconButton-${iconName}`}
|
||||
calloutProps={{ gapSpace: 0 }}
|
||||
styles={{ root: { display: "inline-block" } }}
|
||||
styles={{ root: { display: "inline-block", float: horizontalAlign } }}
|
||||
>
|
||||
<IconButton iconProps={{ iconName }} title={title} ariaLabel={title} onClick={onClick} />
|
||||
<IconButton
|
||||
iconProps={{ iconName }}
|
||||
title={title}
|
||||
ariaLabel={title}
|
||||
onClick={event => this.onClick(event, activate)}
|
||||
/>
|
||||
</TooltipHost>
|
||||
);
|
||||
};
|
||||
|
||||
private onTagClick = (
|
||||
event: React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>,
|
||||
tag: string
|
||||
private onClick = (
|
||||
event:
|
||||
| React.MouseEvent<HTMLElement | HTMLAnchorElement | HTMLButtonElement | LinkBase, MouseEvent>
|
||||
| React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>,
|
||||
activate: () => void
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onTagClick(tag);
|
||||
};
|
||||
|
||||
private onFavoriteClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onFavoriteClick();
|
||||
};
|
||||
|
||||
private onUnfavoriteClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onUnfavoriteClick();
|
||||
};
|
||||
|
||||
private onDownloadClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onDownloadClick();
|
||||
};
|
||||
|
||||
private onDeleteClick = (
|
||||
event: React.MouseEvent<
|
||||
HTMLAnchorElement | HTMLButtonElement | HTMLDivElement | BaseButton | Button | HTMLSpanElement,
|
||||
MouseEvent
|
||||
>
|
||||
): void => {
|
||||
event.stopPropagation();
|
||||
this.props.onDeleteClick();
|
||||
event.preventDefault();
|
||||
activate();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,35 +2,47 @@
|
||||
|
||||
exports[`GalleryCardComponent renders 1`] = `
|
||||
<Card
|
||||
aria-label="Notebook Card"
|
||||
aria-label="name"
|
||||
data-is-focusable="true"
|
||||
onClick={[Function]}
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 8,
|
||||
"childrenMargin": 10,
|
||||
"height": 384,
|
||||
"childrenGap": 0,
|
||||
"width": 256,
|
||||
}
|
||||
}
|
||||
>
|
||||
<CardItem>
|
||||
<CardItem
|
||||
tokens={
|
||||
Object {
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledPersonaBase
|
||||
imageUrl={false}
|
||||
secondaryText="Invalid Date"
|
||||
text="author"
|
||||
/>
|
||||
</CardItem>
|
||||
<CardItem
|
||||
fill={true}
|
||||
>
|
||||
<CardItem>
|
||||
<Memo(StyledImageBase)
|
||||
alt="Notebook cover image"
|
||||
alt="name cover image"
|
||||
height={144}
|
||||
imageFit={2}
|
||||
src="thumbnailUrl"
|
||||
width={256}
|
||||
/>
|
||||
</CardItem>
|
||||
<CardSection>
|
||||
<CardSection
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"padding": 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
nowrap={true}
|
||||
variant="small"
|
||||
@@ -51,6 +63,8 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
Object {
|
||||
"root": Object {
|
||||
"fontWeight": 600,
|
||||
"paddingBottom": 8,
|
||||
"paddingTop": 8,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -69,88 +83,91 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
>
|
||||
description
|
||||
</Text>
|
||||
<span>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": undefined,
|
||||
"paddingRight": 8,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Memo(StyledIconBase)
|
||||
iconName="RedEye"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": undefined,
|
||||
"paddingRight": 8,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Memo(StyledIconBase)
|
||||
iconName="Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": undefined,
|
||||
"paddingRight": 8,
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Memo(StyledIconBase)
|
||||
iconName="Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
</span>
|
||||
</CardSection>
|
||||
<CardSection
|
||||
horizontal={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"alignItems": "flex-end",
|
||||
"marginLeft": 10,
|
||||
"marginRight": 10,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Memo(StyledIconBase)
|
||||
iconName="RedEye"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Memo(StyledIconBase)
|
||||
iconName="Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
<Text
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"color": "#ccc",
|
||||
},
|
||||
}
|
||||
}
|
||||
variant="tiny"
|
||||
>
|
||||
<Memo(StyledIconBase)
|
||||
iconName="Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"verticalAlign": "middle",
|
||||
},
|
||||
}
|
||||
}
|
||||
/>
|
||||
|
||||
0
|
||||
</Text>
|
||||
</CardSection>
|
||||
<CardItem>
|
||||
<Styled
|
||||
styles={
|
||||
Object {
|
||||
@@ -161,79 +178,63 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
}
|
||||
}
|
||||
/>
|
||||
</CardItem>
|
||||
<CardSection
|
||||
horizontal={true}
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"marginTop": 0,
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Like"
|
||||
id="TooltipHost-IconButton-Heart"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Like"
|
||||
iconProps={
|
||||
<span>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Like"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
content="Download"
|
||||
id="TooltipHost-IconButton-Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Download"
|
||||
iconProps={
|
||||
content="Like"
|
||||
id="TooltipHost-IconButton-Heart"
|
||||
styles={
|
||||
Object {
|
||||
"iconName": "Download",
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
"float": "left",
|
||||
},
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Download"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "right",
|
||||
"width": "100%",
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Like"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Heart",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Like"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
"gapSpace": 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
>
|
||||
content="Download"
|
||||
id="TooltipHost-IconButton-Download"
|
||||
styles={
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
"float": "left",
|
||||
},
|
||||
}
|
||||
}
|
||||
>
|
||||
<CustomizedIconButton
|
||||
ariaLabel="Download"
|
||||
iconProps={
|
||||
Object {
|
||||
"iconName": "Download",
|
||||
}
|
||||
}
|
||||
onClick={[Function]}
|
||||
title="Download"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
<StyledTooltipHostBase
|
||||
calloutProps={
|
||||
Object {
|
||||
@@ -246,6 +247,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
Object {
|
||||
"root": Object {
|
||||
"display": "inline-block",
|
||||
"float": "right",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -261,7 +263,7 @@ exports[`GalleryCardComponent renders 1`] = `
|
||||
title="Remove"
|
||||
/>
|
||||
</StyledTooltipHostBase>
|
||||
</div>
|
||||
</span>
|
||||
</CardSection>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import * as React from "react";
|
||||
import * as ViewModels from "../../../Contracts/ViewModels";
|
||||
import { JunoClient, IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { GalleryTab, SortBy, GalleryViewerComponentProps, GalleryViewerComponent } from "./GalleryViewerComponent";
|
||||
import { NotebookViewerComponentProps, NotebookViewerComponent } from "../NotebookViewer/NotebookViewerComponent";
|
||||
import * as GalleryUtils from "../../../Utils/GalleryUtils";
|
||||
|
||||
export interface GalleryAndNotebookViewerComponentProps {
|
||||
container?: ViewModels.Explorer;
|
||||
junoClient: JunoClient;
|
||||
notebookUrl?: string;
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
interface GalleryAndNotebookViewerComponentState {
|
||||
notebookUrl: string;
|
||||
galleryItem: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
export class GalleryAndNotebookViewerComponent extends React.Component<
|
||||
GalleryAndNotebookViewerComponentProps,
|
||||
GalleryAndNotebookViewerComponentState
|
||||
> {
|
||||
constructor(props: GalleryAndNotebookViewerComponentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
notebookUrl: props.notebookUrl,
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
selectedTab: props.selectedTab,
|
||||
sortBy: props.sortBy,
|
||||
searchText: props.searchText
|
||||
};
|
||||
}
|
||||
|
||||
public render(): JSX.Element {
|
||||
if (this.state.notebookUrl) {
|
||||
const props: NotebookViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
notebookUrl: this.state.notebookUrl,
|
||||
galleryItem: this.state.galleryItem,
|
||||
isFavorite: this.state.isFavorite,
|
||||
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
|
||||
onBackClick: this.onBackClick,
|
||||
onTagClick: this.loadTaggedItems
|
||||
};
|
||||
|
||||
return <NotebookViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
const props: GalleryViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
selectedTab: this.state.selectedTab,
|
||||
sortBy: this.state.sortBy,
|
||||
searchText: this.state.searchText,
|
||||
openNotebook: this.openNotebook,
|
||||
onSelectedTabChange: this.onSelectedTabChange,
|
||||
onSortByChange: this.onSortByChange,
|
||||
onSearchTextChange: this.onSearchTextChange
|
||||
};
|
||||
|
||||
return <GalleryViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
private onBackClick = (): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined
|
||||
});
|
||||
};
|
||||
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined,
|
||||
searchText: tag
|
||||
});
|
||||
};
|
||||
|
||||
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
||||
this.setState({
|
||||
notebookUrl: this.props.junoClient.getNotebookContentUrl(data.id),
|
||||
galleryItem: data,
|
||||
isFavorite
|
||||
});
|
||||
};
|
||||
|
||||
private onSelectedTabChange = (selectedTab: GalleryTab): void => {
|
||||
this.setState({
|
||||
selectedTab
|
||||
});
|
||||
};
|
||||
|
||||
private onSortByChange = (sortBy: SortBy): void => {
|
||||
this.setState({
|
||||
sortBy
|
||||
});
|
||||
};
|
||||
|
||||
private onSearchTextChange = (searchText: string): void => {
|
||||
this.setState({
|
||||
searchText
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../../Bindings/ReactBindingHandler";
|
||||
import {
|
||||
GalleryAndNotebookViewerComponentProps,
|
||||
GalleryAndNotebookViewerComponent
|
||||
} from "./GalleryAndNotebookViewerComponent";
|
||||
|
||||
export class GalleryAndNotebookViewerComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
|
||||
constructor(private props: GalleryAndNotebookViewerComponentProps) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
return <GalleryAndNotebookViewerComponent {...this.props} />;
|
||||
}
|
||||
|
||||
public triggerRender(): void {
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ describe("GalleryViewerComponent", () => {
|
||||
selectedTab: GalleryTab.OfficialSamples,
|
||||
sortBy: SortBy.MostViewed,
|
||||
searchText: undefined,
|
||||
openNotebook: undefined,
|
||||
onSelectedTabChange: undefined,
|
||||
onSortByChange: undefined,
|
||||
onSearchTextChange: undefined
|
||||
|
||||
@@ -31,6 +31,7 @@ export interface GalleryViewerComponentProps {
|
||||
selectedTab: GalleryTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
openNotebook: (data: IGalleryItem, isFavorite: boolean) => void;
|
||||
onSelectedTabChange: (newTab: GalleryTab) => void;
|
||||
onSortByChange: (sortBy: SortBy) => void;
|
||||
onSearchTextChange: (searchText: string) => void;
|
||||
@@ -66,13 +67,14 @@ interface GalleryTabInfo {
|
||||
content: JSX.Element;
|
||||
}
|
||||
|
||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState>
|
||||
implements GalleryUtils.DialogEnabledComponent {
|
||||
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps, GalleryViewerComponentState> {
|
||||
public static readonly OfficialSamplesTitle = "Official samples";
|
||||
public static readonly PublicGalleryTitle = "Public gallery";
|
||||
public static readonly FavoritesTitle = "Liked";
|
||||
public static readonly PublishedTitle = "Your published work";
|
||||
|
||||
private static readonly rowsPerPage = 5;
|
||||
|
||||
private static readonly mostViewedText = "Most viewed";
|
||||
private static readonly mostDownloadedText = "Most downloaded";
|
||||
private static readonly mostFavoritedText = "Most liked";
|
||||
@@ -128,10 +130,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}
|
||||
}
|
||||
|
||||
setDialogProps = (dialogProps: DialogProps): void => {
|
||||
this.setState({ dialogProps });
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const tabs: GalleryTabInfo[] = [this.createTab(GalleryTab.OfficialSamples, this.state.sampleNotebooks)];
|
||||
|
||||
@@ -178,8 +176,8 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
|
||||
private createTabContent(data: IGalleryItem[]): JSX.Element {
|
||||
return (
|
||||
<Stack tokens={{ childrenGap: 20 }}>
|
||||
<Stack horizontal tokens={{ childrenGap: 20 }}>
|
||||
<Stack tokens={{ childrenGap: 10 }}>
|
||||
<Stack horizontal tokens={{ childrenGap: 20, padding: 10 }}>
|
||||
<Stack.Item grow>
|
||||
<SearchBox value={this.state.searchText} placeholder="Search" onChange={this.onSearchBoxChange} />
|
||||
</Stack.Item>
|
||||
@@ -389,8 +387,10 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
}
|
||||
|
||||
private getPageSpecification = (itemIndex?: number, visibleRect?: IRectangle): IPageSpecification => {
|
||||
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH);
|
||||
this.rowCount = Math.floor(visibleRect.height / GalleryCardComponent.CARD_HEIGHT);
|
||||
if (itemIndex === 0) {
|
||||
this.columnCount = Math.floor(visibleRect.width / GalleryCardComponent.CARD_WIDTH) || this.columnCount;
|
||||
this.rowCount = GalleryViewerComponent.rowsPerPage;
|
||||
}
|
||||
|
||||
return {
|
||||
height: visibleRect.height,
|
||||
@@ -406,8 +406,9 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
const props: GalleryCardComponentProps = {
|
||||
data,
|
||||
isFavorite,
|
||||
showDownload: !!this.props.container,
|
||||
showDelete: this.state.selectedTab === GalleryTab.Published,
|
||||
onClick: () => this.openNotebook(data, isFavorite),
|
||||
onClick: () => this.props.openNotebook(data, isFavorite),
|
||||
onTagClick: this.loadTaggedItems,
|
||||
onFavoriteClick: () => this.favoriteItem(data),
|
||||
onUnfavoriteClick: () => this.unfavoriteItem(data),
|
||||
@@ -422,20 +423,6 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
);
|
||||
};
|
||||
|
||||
private openNotebook = (data: IGalleryItem, isFavorite: boolean): void => {
|
||||
if (this.props.container && this.props.junoClient) {
|
||||
this.props.container.openGallery(this.props.junoClient.getNotebookContentUrl(data.id), data, isFavorite);
|
||||
} else {
|
||||
const params = new URLSearchParams({
|
||||
[GalleryUtils.NotebookViewerParams.NotebookUrl]: this.props.junoClient.getNotebookContentUrl(data.id),
|
||||
[GalleryUtils.NotebookViewerParams.GalleryItemId]: data.id
|
||||
});
|
||||
|
||||
const location = new URL("./notebookViewer.html", window.location.href).href;
|
||||
window.open(`${location}?${params.toString()}`);
|
||||
}
|
||||
};
|
||||
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
const searchText = tag;
|
||||
this.setState({
|
||||
@@ -465,9 +452,7 @@ export class GalleryViewerComponent extends React.Component<GalleryViewerCompone
|
||||
};
|
||||
|
||||
private downloadItem = async (data: IGalleryItem): Promise<void> => {
|
||||
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, data, item =>
|
||||
this.refreshSelectedTab(item)
|
||||
);
|
||||
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, data, item => this.refreshSelectedTab(item));
|
||||
};
|
||||
|
||||
private deleteItem = async (data: IGalleryItem): Promise<void> => {
|
||||
|
||||
@@ -21,7 +21,7 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
<Stack
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
"childrenGap": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
@@ -30,6 +30,7 @@ exports[`GalleryViewerComponent renders 1`] = `
|
||||
tokens={
|
||||
Object {
|
||||
"childrenGap": 20,
|
||||
"padding": 10,
|
||||
}
|
||||
}
|
||||
>
|
||||
|
||||
@@ -16,11 +16,12 @@ import * as React from "react";
|
||||
import { IGalleryItem } from "../../../Juno/JunoClient";
|
||||
import { FileSystemUtil } from "../../Notebook/FileSystemUtil";
|
||||
import "./NotebookViewerComponent.less";
|
||||
import CosmosDBLogo from "../../../../images/CosmosDB-logo.svg";
|
||||
|
||||
export interface NotebookMetadataComponentProps {
|
||||
data: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
downloadButtonText: string;
|
||||
downloadButtonText?: string;
|
||||
onTagClick: (tag: string) => void;
|
||||
onFavoriteClick: () => void;
|
||||
onUnfavoriteClick: () => void;
|
||||
@@ -54,11 +55,18 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
|
||||
{this.props.downloadButtonText && (
|
||||
<PrimaryButton text={this.props.downloadButtonText} onClick={this.props.onDownloadClick} />
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
<Stack horizontal verticalAlign="center" tokens={{ childrenGap: 10 }}>
|
||||
<Persona text={this.props.data.author} size={PersonaSize.size32} />
|
||||
<Persona
|
||||
imageUrl={this.props.data.isSample && CosmosDBLogo}
|
||||
text={this.props.data.author}
|
||||
size={PersonaSize.size32}
|
||||
/>
|
||||
<Text>{dateString}</Text>
|
||||
<Text>
|
||||
<Icon iconName="RedEye" /> {this.props.data.views}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { Notebook } from "@nteract/commutable";
|
||||
import { createContentRef } from "@nteract/core";
|
||||
import { Icon, Link } from "office-ui-fabric-react";
|
||||
import { Icon, Link, ProgressIndicator } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import { contents } from "rx-jupyter";
|
||||
import * as Logger from "../../../Common/Logger";
|
||||
@@ -36,10 +36,13 @@ interface NotebookViewerComponentState {
|
||||
galleryItem?: IGalleryItem;
|
||||
isFavorite?: boolean;
|
||||
dialogProps: DialogProps;
|
||||
showProgressBar: boolean;
|
||||
}
|
||||
|
||||
export class NotebookViewerComponent extends React.Component<NotebookViewerComponentProps, NotebookViewerComponentState>
|
||||
implements GalleryUtils.DialogEnabledComponent {
|
||||
export class NotebookViewerComponent extends React.Component<
|
||||
NotebookViewerComponentProps,
|
||||
NotebookViewerComponentState
|
||||
> {
|
||||
private clientManager: NotebookClientV2;
|
||||
private notebookComponentBootstrapper: NotebookComponentBootstrapper;
|
||||
|
||||
@@ -65,26 +68,24 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
content: undefined,
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
dialogProps: undefined
|
||||
dialogProps: undefined,
|
||||
showProgressBar: true
|
||||
};
|
||||
|
||||
this.loadNotebookContent();
|
||||
}
|
||||
|
||||
setDialogProps = (dialogProps: DialogProps): void => {
|
||||
this.setState({ dialogProps });
|
||||
};
|
||||
|
||||
private async loadNotebookContent(): Promise<void> {
|
||||
try {
|
||||
const response = await fetch(this.props.notebookUrl);
|
||||
if (!response.ok) {
|
||||
this.setState({ showProgressBar: false });
|
||||
throw new Error(`Received HTTP ${response.status} while fetching ${this.props.notebookUrl}`);
|
||||
}
|
||||
|
||||
const notebook: Notebook = await response.json();
|
||||
this.notebookComponentBootstrapper.setContent("json", notebook);
|
||||
this.setState({ content: notebook });
|
||||
this.setState({ content: notebook, showProgressBar: false });
|
||||
|
||||
if (this.props.galleryItem) {
|
||||
const response = await this.props.junoClient.increaseNotebookViews(this.props.galleryItem.id);
|
||||
@@ -95,6 +96,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
this.setState({ galleryItem: response.data });
|
||||
}
|
||||
} catch (error) {
|
||||
this.setState({ showProgressBar: false });
|
||||
const message = `Failed to load notebook content: ${error}`;
|
||||
Logger.logError(message, "NotebookViewerComponent/loadNotebookContent");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
@@ -117,9 +119,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
<NotebookMetadataComponent
|
||||
data={this.state.galleryItem}
|
||||
isFavorite={this.state.isFavorite}
|
||||
downloadButtonText={
|
||||
this.props.container ? "Download to my notebooks" : "Edit/Run in Cosmos DB data explorer"
|
||||
}
|
||||
downloadButtonText={this.props.container && "Download to my notebooks"}
|
||||
onTagClick={this.props.onTagClick}
|
||||
onFavoriteClick={this.favoriteItem}
|
||||
onUnfavoriteClick={this.unfavoriteItem}
|
||||
@@ -130,6 +130,8 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
<></>
|
||||
)}
|
||||
|
||||
{this.state.showProgressBar && <ProgressIndicator />}
|
||||
|
||||
{this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
|
||||
hideInputs: this.props.hideInputs
|
||||
})}
|
||||
@@ -173,7 +175,7 @@ export class NotebookViewerComponent extends React.Component<NotebookViewerCompo
|
||||
};
|
||||
|
||||
private downloadItem = async (): Promise<void> => {
|
||||
GalleryUtils.downloadItem(this, this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
GalleryUtils.downloadItem(this.props.container, this.props.junoClient, this.state.galleryItem, item =>
|
||||
this.setState({ galleryItem: item })
|
||||
);
|
||||
};
|
||||
|
||||
@@ -48,6 +48,7 @@ exports[`NotebookMetadataComponent renders liked notebook 1`] = `
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledPersonaBase
|
||||
imageUrl={false}
|
||||
size={11}
|
||||
text="author"
|
||||
/>
|
||||
@@ -147,6 +148,7 @@ exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
|
||||
verticalAlign="center"
|
||||
>
|
||||
<StyledPersonaBase
|
||||
imageUrl={false}
|
||||
size={11}
|
||||
text="author"
|
||||
/>
|
||||
|
||||
@@ -3101,7 +3101,6 @@ export default class Explorer implements ViewModels.Explorer {
|
||||
|
||||
if (galleryTab) {
|
||||
this.tabsManager.activateTab(galleryTab);
|
||||
(galleryTab as any).updateGalleryParams(notebookUrl, galleryItem, isFavorite);
|
||||
} else {
|
||||
if (!this.galleryTab) {
|
||||
this.galleryTab = await import(/* webpackChunkName: "GalleryTab" */ "./Tabs/GalleryTab");
|
||||
|
||||
@@ -889,6 +889,7 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
|
||||
this.container.armEndpoint(),
|
||||
databaseId,
|
||||
collectionId,
|
||||
indexingPolicy,
|
||||
offerThroughput,
|
||||
partitionKeyPath,
|
||||
partitionKey.version,
|
||||
@@ -908,6 +909,7 @@ export default class AddCollectionPane extends ContextualPaneBase implements Vie
|
||||
databaseId,
|
||||
this._getAnalyticalStorageTtl(),
|
||||
collectionId,
|
||||
indexingPolicy,
|
||||
offerThroughput,
|
||||
partitionKeyPath,
|
||||
partitionKey.version,
|
||||
|
||||
@@ -1 +1 @@
|
||||
<div style="height: 100%" data-bind="react:galleryComponentAdapter, setTemplateReady: true"></div>
|
||||
<div style="height: 100%" data-bind="react:galleryAndNotebookViewerComponentAdapter, setTemplateReady: true"></div>
|
||||
|
||||
@@ -1,129 +1,21 @@
|
||||
import * as ko from "knockout";
|
||||
import * as React from "react";
|
||||
import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
|
||||
import * as ViewModels from "../../Contracts/ViewModels";
|
||||
import { IGalleryItem, JunoClient } from "../../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../../Utils/GalleryUtils";
|
||||
import {
|
||||
GalleryTab as GalleryViewerTab,
|
||||
GalleryViewerComponent,
|
||||
GalleryViewerComponentProps,
|
||||
SortBy
|
||||
} from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import {
|
||||
NotebookViewerComponent,
|
||||
NotebookViewerComponentProps
|
||||
} from "../Controls/NotebookViewer/NotebookViewerComponent";
|
||||
import { GalleryAndNotebookViewerComponentProps } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
|
||||
import { GalleryAndNotebookViewerComponentAdapter } from "../Controls/NotebookGallery/GalleryAndNotebookViewerComponentAdapter";
|
||||
import { GalleryTab as GalleryViewerTab, SortBy } from "../Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import TabsBase from "./TabsBase";
|
||||
|
||||
/**
|
||||
* Notebook gallery tab
|
||||
*/
|
||||
interface GalleryComponentAdapterProps {
|
||||
container: ViewModels.Explorer;
|
||||
junoClient: JunoClient;
|
||||
notebookUrl: string;
|
||||
galleryItem: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
selectedTab: GalleryViewerTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
interface GalleryComponentAdapterState {
|
||||
notebookUrl: string;
|
||||
galleryItem: IGalleryItem;
|
||||
isFavorite: boolean;
|
||||
selectedTab: GalleryViewerTab;
|
||||
sortBy: SortBy;
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
class GalleryComponentAdapter implements ReactAdapter {
|
||||
public parameters: ko.Observable<number>;
|
||||
private state: GalleryComponentAdapterState;
|
||||
|
||||
constructor(private props: GalleryComponentAdapterProps) {
|
||||
this.parameters = ko.observable<number>(Date.now());
|
||||
this.state = {
|
||||
notebookUrl: props.notebookUrl,
|
||||
galleryItem: props.galleryItem,
|
||||
isFavorite: props.isFavorite,
|
||||
selectedTab: props.selectedTab,
|
||||
sortBy: props.sortBy,
|
||||
searchText: props.searchText
|
||||
};
|
||||
}
|
||||
|
||||
public renderComponent(): JSX.Element {
|
||||
if (this.state.notebookUrl) {
|
||||
const props: NotebookViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
notebookUrl: this.state.notebookUrl,
|
||||
galleryItem: this.state.galleryItem,
|
||||
isFavorite: this.state.isFavorite,
|
||||
backNavigationText: GalleryUtils.getTabTitle(this.state.selectedTab),
|
||||
onBackClick: this.onBackClick,
|
||||
onTagClick: this.loadTaggedItems
|
||||
};
|
||||
|
||||
return <NotebookViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
const props: GalleryViewerComponentProps = {
|
||||
container: this.props.container,
|
||||
junoClient: this.props.junoClient,
|
||||
selectedTab: this.state.selectedTab,
|
||||
sortBy: this.state.sortBy,
|
||||
searchText: this.state.searchText,
|
||||
onSelectedTabChange: this.onSelectedTabChange,
|
||||
onSortByChange: this.onSortByChange,
|
||||
onSearchTextChange: this.onSearchTextChange
|
||||
};
|
||||
|
||||
return <GalleryViewerComponent {...props} />;
|
||||
}
|
||||
|
||||
public setState(state: Partial<GalleryComponentAdapterState>): void {
|
||||
this.state = Object.assign(this.state, state);
|
||||
window.requestAnimationFrame(() => this.parameters(Date.now()));
|
||||
}
|
||||
|
||||
private onBackClick = (): void => {
|
||||
this.props.container.openGallery();
|
||||
};
|
||||
|
||||
private loadTaggedItems = (tag: string): void => {
|
||||
this.setState({
|
||||
notebookUrl: undefined,
|
||||
searchText: tag
|
||||
});
|
||||
};
|
||||
|
||||
private onSelectedTabChange = (selectedTab: GalleryViewerTab): void => {
|
||||
this.state.selectedTab = selectedTab;
|
||||
};
|
||||
|
||||
private onSortByChange = (sortBy: SortBy): void => {
|
||||
this.state.sortBy = sortBy;
|
||||
};
|
||||
|
||||
private onSearchTextChange = (searchText: string): void => {
|
||||
this.state.searchText = searchText;
|
||||
};
|
||||
}
|
||||
|
||||
export default class GalleryTab extends TabsBase implements ViewModels.Tab {
|
||||
private container: ViewModels.Explorer;
|
||||
private galleryComponentAdapterProps: GalleryComponentAdapterProps;
|
||||
private galleryComponentAdapter: GalleryComponentAdapter;
|
||||
public galleryAndNotebookViewerComponentAdapter: GalleryAndNotebookViewerComponentAdapter;
|
||||
|
||||
constructor(options: ViewModels.GalleryTabOptions) {
|
||||
super(options);
|
||||
|
||||
this.container = options.container;
|
||||
this.galleryComponentAdapterProps = {
|
||||
const props: GalleryAndNotebookViewerComponentProps = {
|
||||
container: options.container,
|
||||
junoClient: options.junoClient,
|
||||
notebookUrl: options.notebookUrl,
|
||||
@@ -134,18 +26,10 @@ export default class GalleryTab extends TabsBase implements ViewModels.Tab {
|
||||
searchText: undefined
|
||||
};
|
||||
|
||||
this.galleryComponentAdapter = new GalleryComponentAdapter(this.galleryComponentAdapterProps);
|
||||
this.galleryAndNotebookViewerComponentAdapter = new GalleryAndNotebookViewerComponentAdapter(props);
|
||||
}
|
||||
|
||||
protected getContainer(): ViewModels.Explorer {
|
||||
return this.container;
|
||||
}
|
||||
|
||||
public updateGalleryParams(notebookUrl?: string, galleryItem?: IGalleryItem, isFavorite?: boolean): void {
|
||||
this.galleryComponentAdapter.setState({
|
||||
notebookUrl,
|
||||
galleryItem,
|
||||
isFavorite
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,31 +1,33 @@
|
||||
import "bootstrap/dist/css/bootstrap.css";
|
||||
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
|
||||
import { Text, Link } from "office-ui-fabric-react";
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import { initializeConfiguration } from "../Config";
|
||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||
import {
|
||||
GalleryTab,
|
||||
GalleryViewerComponent,
|
||||
GalleryViewerComponentProps,
|
||||
SortBy
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
GalleryAndNotebookViewerComponent,
|
||||
GalleryAndNotebookViewerComponentProps
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryAndNotebookViewerComponent";
|
||||
import { GalleryTab, SortBy } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
import { JunoClient } from "../Juno/JunoClient";
|
||||
import * as GalleryUtils from "../Utils/GalleryUtils";
|
||||
import { GalleryHeaderComponent } from "../Explorer/Controls/Header/GalleryHeaderComponent";
|
||||
|
||||
const enableNotebooksUrl = "https://aka.ms/cosmos-enable-notebooks";
|
||||
const createAccountUrl = "https://aka.ms/cosmos-create-account-portal";
|
||||
|
||||
const onInit = async () => {
|
||||
const dataExplorerUrl = new URL("./", window.location.href).href;
|
||||
|
||||
initializeIcons();
|
||||
await initializeConfiguration();
|
||||
const galleryViewerProps = GalleryUtils.getGalleryViewerProps(window.location.search);
|
||||
|
||||
const props: GalleryViewerComponentProps = {
|
||||
const props: GalleryAndNotebookViewerComponentProps = {
|
||||
junoClient: new JunoClient(),
|
||||
selectedTab: galleryViewerProps.selectedTab || GalleryTab.OfficialSamples,
|
||||
sortBy: galleryViewerProps.sortBy || SortBy.MostViewed,
|
||||
searchText: galleryViewerProps.searchText,
|
||||
onSelectedTabChange: undefined,
|
||||
onSortByChange: undefined,
|
||||
onSearchTextChange: undefined
|
||||
searchText: galleryViewerProps.searchText
|
||||
};
|
||||
|
||||
const element = (
|
||||
@@ -34,7 +36,21 @@ const onInit = async () => {
|
||||
<GalleryHeaderComponent />
|
||||
</header>
|
||||
<div style={{ marginLeft: 138, marginRight: 138 }}>
|
||||
<GalleryViewerComponent {...props} />
|
||||
<div style={{ paddingLeft: 26, paddingRight: 26, paddingTop: 20 }}>
|
||||
<Text block>
|
||||
Welcome to the Azure Cosmos DB notebooks gallery! View the sample notebooks to learn about use cases, best
|
||||
practices, and how to get started with Azure Cosmos DB.
|
||||
</Text>
|
||||
<Text styles={{ root: { marginTop: 10 } }} block>
|
||||
If you'd like to run or edit the notebook in your own Azure Cosmos DB account,{" "}
|
||||
<Link href={dataExplorerUrl}>sign in</Link> and select an account with{" "}
|
||||
<Link href={enableNotebooksUrl}>notebooks enabled</Link>. From there, you can download the sample to your
|
||||
account. If you don't have an account yet, you can{" "}
|
||||
<Link href={createAccountUrl}>create one from the Azure portal</Link>.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<GalleryAndNotebookViewerComponent {...props} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<link rel="shortcut icon" href="images/CosmosDB_rgb_ui_lighttheme.ico" type="image/x-icon" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body style="overflow-y:scroll">
|
||||
<div class="galleryContent" id="galleryContent"></div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -68,6 +68,7 @@ class HostedExplorer {
|
||||
|
||||
this._controlbarCommands = ko.observableArray([
|
||||
{
|
||||
id: "commandbutton-connect",
|
||||
iconSrc: ConnectIcon,
|
||||
iconAlt: "connect button",
|
||||
onCommandClick: () => this.openConnectPane(),
|
||||
@@ -78,6 +79,7 @@ class HostedExplorer {
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
id: "commandbutton-settings",
|
||||
iconSrc: SettingsIcon,
|
||||
iconAlt: "setting button",
|
||||
onCommandClick: () => this.openSettingsPane(),
|
||||
@@ -88,6 +90,7 @@ class HostedExplorer {
|
||||
disabled: false
|
||||
},
|
||||
{
|
||||
id: "commandbutton-feedback",
|
||||
iconSrc: FeedbackIcon,
|
||||
iconAlt: "feeback button",
|
||||
onCommandClick: () =>
|
||||
|
||||
10
src/Index.ts
10
src/Index.ts
@@ -2,22 +2,12 @@ import "../less/index.less";
|
||||
import "./Libs/jquery";
|
||||
|
||||
import * as ko from "knockout";
|
||||
import { CorrelationBackend } from "./Common/Constants";
|
||||
import Ajax from "./Shared/Ajax";
|
||||
|
||||
class Index {
|
||||
public navigationSelection: ko.Observable<string>;
|
||||
public correlationSrc: ko.Observable<string>;
|
||||
|
||||
constructor() {
|
||||
this.navigationSelection = ko.observable("quickstart");
|
||||
this.correlationSrc = ko.observable("");
|
||||
|
||||
Ajax.get("/_explorer/installation_id.txt").then(result => {
|
||||
// TODO: Detect correct URL for each environment automatically.
|
||||
const url: string = `${CorrelationBackend.Url}?emulator_id=${result}`;
|
||||
this.correlationSrc(url);
|
||||
});
|
||||
}
|
||||
|
||||
public quickstart_click() {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import * as SharedConstants from "../Shared/Constants";
|
||||
import { AddDbUtilities } from "../Shared/AddDatabaseUtility";
|
||||
import { CreateCollectionUtilities, CreateSqlCollectionUtilities, Utilities } from "./AddCollectionUtility";
|
||||
jest.mock("AddDatabaseUtility");
|
||||
@@ -19,6 +20,7 @@ describe("Add Collection Utitlity", () => {
|
||||
rg: "b1",
|
||||
st: true,
|
||||
defaultTtl: -1,
|
||||
indexingPolicy: SharedConstants.IndexingPolicies.AllPropertiesIndexed,
|
||||
partitionKeyVersion: 2
|
||||
};
|
||||
const additionalOptions = {};
|
||||
@@ -28,6 +30,7 @@ describe("Add Collection Utitlity", () => {
|
||||
properties.db,
|
||||
properties.defaultTtl,
|
||||
properties.coll,
|
||||
properties.indexingPolicy,
|
||||
properties.offerThroughput,
|
||||
properties.pk,
|
||||
properties.partitionKeyVersion,
|
||||
@@ -55,6 +58,7 @@ describe("Add Collection Utitlity", () => {
|
||||
rg: "b1",
|
||||
st: true,
|
||||
analyticalStorageTtl: -1,
|
||||
indexingPolicy: SharedConstants.IndexingPolicies.AllPropertiesIndexed,
|
||||
partitionKeyVersion: 2
|
||||
};
|
||||
const additionalOptions = {};
|
||||
@@ -65,6 +69,7 @@ describe("Add Collection Utitlity", () => {
|
||||
properties.db,
|
||||
properties.analyticalStorageTtl,
|
||||
properties.coll,
|
||||
properties.indexingPolicy,
|
||||
properties.offerThroughput,
|
||||
properties.pk,
|
||||
properties.partitionKeyVersion,
|
||||
@@ -95,6 +100,7 @@ describe("Add Collection Utitlity", () => {
|
||||
sid: "a1",
|
||||
rg: "b1",
|
||||
st: true,
|
||||
indexingPolicy: SharedConstants.IndexingPolicies.AllPropertiesIndexed,
|
||||
partitionKeyVersion: 2
|
||||
};
|
||||
const additionalOptions = {};
|
||||
@@ -103,6 +109,7 @@ describe("Add Collection Utitlity", () => {
|
||||
armEndpoint,
|
||||
properties.db,
|
||||
properties.coll,
|
||||
properties.indexingPolicy,
|
||||
properties.offerThroughput,
|
||||
properties.pk,
|
||||
properties.partitionKeyVersion,
|
||||
@@ -127,6 +134,7 @@ describe("Add Collection Utitlity", () => {
|
||||
sid: "a1",
|
||||
rg: "b1",
|
||||
st: true,
|
||||
indexingPolicy: SharedConstants.IndexingPolicies.AllPropertiesIndexed,
|
||||
partitionKeyVersion: 2
|
||||
};
|
||||
const additionalOptions = {};
|
||||
@@ -136,6 +144,7 @@ describe("Add Collection Utitlity", () => {
|
||||
armEndpoint,
|
||||
properties.db,
|
||||
properties.coll,
|
||||
properties.indexingPolicy,
|
||||
properties.offerThroughput,
|
||||
properties.pk,
|
||||
properties.partitionKeyVersion,
|
||||
|
||||
@@ -18,6 +18,7 @@ export class CreateSqlCollectionUtilities {
|
||||
databaseId: string,
|
||||
analyticalStorageTtl: number,
|
||||
collectionId: string,
|
||||
indexingPolicy: DataModels.IndexingPolicy,
|
||||
offerThroughput: number,
|
||||
partitionKey: string,
|
||||
partitionKeyVersion: number,
|
||||
@@ -41,6 +42,7 @@ export class CreateSqlCollectionUtilities {
|
||||
rg,
|
||||
dba,
|
||||
analyticalStorageTtl,
|
||||
indexingPolicy,
|
||||
partitionKeyVersion
|
||||
};
|
||||
|
||||
@@ -75,6 +77,10 @@ export class CreateSqlCollectionUtilities {
|
||||
rpPayloadToCreateCollection.properties.resource.analyticalStorageTtl = params.analyticalStorageTtl;
|
||||
}
|
||||
|
||||
if (params.indexingPolicy) {
|
||||
rpPayloadToCreateCollection.properties.resource.indexingPolicy = params.indexingPolicy;
|
||||
}
|
||||
|
||||
if (!params.st) {
|
||||
if (rpOptions) {
|
||||
rpPayloadToCreateCollection.properties.options = rpOptions;
|
||||
@@ -117,6 +123,7 @@ export class CreateCollectionUtilities {
|
||||
armEndpoint: string,
|
||||
databaseId: string,
|
||||
collectionId: string,
|
||||
indexingPolicy: DataModels.IndexingPolicy,
|
||||
offerThroughput: number,
|
||||
partitionKey: string,
|
||||
partitionKeyVersion: number,
|
||||
@@ -137,6 +144,7 @@ export class CreateCollectionUtilities {
|
||||
sid,
|
||||
rg,
|
||||
dba,
|
||||
indexingPolicy,
|
||||
partitionKeyVersion
|
||||
};
|
||||
|
||||
@@ -167,6 +175,10 @@ export class CreateCollectionUtilities {
|
||||
}
|
||||
};
|
||||
|
||||
if (params.indexingPolicy) {
|
||||
rpPayloadToCreateCollection.properties.resource.indexingPolicy = params.indexingPolicy;
|
||||
}
|
||||
|
||||
if (!params.st) {
|
||||
if (rpOptions) {
|
||||
rpPayloadToCreateCollection.properties.options = rpOptions;
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import Q from "q";
|
||||
import $ from "jquery";
|
||||
|
||||
export default class Ajax {
|
||||
public static head<T>(url: string): Q.Promise<any> {
|
||||
return Ajax._ajax(url, "HEAD");
|
||||
}
|
||||
|
||||
public static post<T>(url: string, data?: any): Q.Promise<any> {
|
||||
return Ajax._ajax(url, "POST", data);
|
||||
}
|
||||
|
||||
public static put<T>(url: string, data?: any): Q.Promise<any> {
|
||||
return Ajax._ajax(url, "PUT", data);
|
||||
}
|
||||
|
||||
public static get<T>(url: string, data?: any): Q.Promise<any> {
|
||||
return Ajax._ajax(url, "GET", data);
|
||||
}
|
||||
|
||||
public static Delete<T>(url: string, data?: any): Q.Promise<any> {
|
||||
return Ajax._ajax(url, "DELETE", data);
|
||||
}
|
||||
|
||||
static _ajax<T>(url: string, method: string, data?: any): Q.Promise<any> {
|
||||
return Q($.ajax(url, Ajax._getNetAjaxSettings(url, method, data)));
|
||||
}
|
||||
|
||||
static _getNetAjaxSettings<T>(url: string, method: string, data?: any): JQueryAjaxSettings<T> {
|
||||
var newSettings: JQueryAjaxSettings<T> = {
|
||||
url: url,
|
||||
type: method,
|
||||
cache: false,
|
||||
contentType: "application/json",
|
||||
traditional: true
|
||||
};
|
||||
|
||||
if (!!data) {
|
||||
newSettings.data = typeof data === "string" ? data : JSON.stringify(data || {});
|
||||
}
|
||||
return newSettings;
|
||||
}
|
||||
}
|
||||
@@ -24,22 +24,12 @@ describe("GalleryUtils", () => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
it("downloadItem shows dialog in standalone gallery", () => {
|
||||
const setDialogProps = jest.fn().mockImplementation();
|
||||
|
||||
GalleryUtils.downloadItem({ setDialogProps }, undefined, undefined, galleryItem, undefined);
|
||||
|
||||
expect(setDialogProps).toBeCalled();
|
||||
});
|
||||
|
||||
it("downloadItem shows dialog in data explorer", () => {
|
||||
const setDialogProps = jest.fn().mockImplementation();
|
||||
const container = new ExplorerStub();
|
||||
container.showOkCancelModalDialog = jest.fn().mockImplementation();
|
||||
|
||||
GalleryUtils.downloadItem({ setDialogProps }, container, undefined, galleryItem, undefined);
|
||||
GalleryUtils.downloadItem(container, undefined, galleryItem, undefined);
|
||||
|
||||
expect(setDialogProps).not.toBeCalled();
|
||||
expect(container.showOkCancelModalDialog).toBeCalled();
|
||||
});
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { LinkProps, DialogProps } from "../Explorer/Controls/DialogReactComponent/DialogComponent";
|
||||
import { IGalleryItem, JunoClient } from "../Juno/JunoClient";
|
||||
import * as ViewModels from "../Contracts/ViewModels";
|
||||
import { NotificationConsoleUtils } from "./NotificationConsoleUtils";
|
||||
@@ -10,10 +9,6 @@ import {
|
||||
GalleryViewerComponent
|
||||
} from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
|
||||
|
||||
export interface DialogEnabledComponent {
|
||||
setDialogProps: (dialogProps: DialogProps) => void;
|
||||
}
|
||||
|
||||
export enum NotebookViewerParams {
|
||||
NotebookUrl = "notebookUrl",
|
||||
GalleryItemId = "galleryItemId",
|
||||
@@ -38,105 +33,50 @@ export interface GalleryViewerProps {
|
||||
searchText: string;
|
||||
}
|
||||
|
||||
function showOkCancelModalDialog(
|
||||
component: DialogEnabledComponent,
|
||||
title: string,
|
||||
msg: string,
|
||||
linkProps: LinkProps,
|
||||
showCloseButton: boolean,
|
||||
okLabel: string,
|
||||
onOk: () => void,
|
||||
cancelLabel: string,
|
||||
onCancel: () => void
|
||||
): void {
|
||||
component.setDialogProps({
|
||||
linkProps,
|
||||
isModal: true,
|
||||
visible: true,
|
||||
title,
|
||||
subText: msg,
|
||||
primaryButtonText: okLabel,
|
||||
secondaryButtonText: cancelLabel,
|
||||
onPrimaryButtonClick: () => {
|
||||
component.setDialogProps(undefined);
|
||||
onOk && onOk();
|
||||
},
|
||||
onSecondaryButtonClick: () => {
|
||||
component.setDialogProps(undefined);
|
||||
onCancel && onCancel();
|
||||
},
|
||||
showCloseButton,
|
||||
onDismiss: () => component.setDialogProps(undefined)
|
||||
});
|
||||
}
|
||||
|
||||
export function downloadItem(
|
||||
component: DialogEnabledComponent,
|
||||
container: ViewModels.Explorer,
|
||||
junoClient: JunoClient,
|
||||
data: IGalleryItem,
|
||||
onComplete: (item: IGalleryItem) => void
|
||||
): void {
|
||||
const name = data.name;
|
||||
container.showOkCancelModalDialog(
|
||||
"Download to My Notebooks",
|
||||
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
||||
"Download",
|
||||
async () => {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Downloading ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
if (container) {
|
||||
container.showOkCancelModalDialog(
|
||||
"Download to My Notebooks",
|
||||
`Download ${name} from gallery as a copy to your notebooks to run and/or edit the notebook.`,
|
||||
"Download",
|
||||
async () => {
|
||||
const notificationId = NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.InProgress,
|
||||
`Downloading ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await junoClient.getNotebookContent(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
|
||||
}
|
||||
|
||||
await container.importAndOpenContent(data.name, response.data);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully downloaded ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||
if (increaseDownloadResponse.data) {
|
||||
onComplete(increaseDownloadResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Failed to download ${data.name}: ${error}`;
|
||||
Logger.logError(message, "GalleryUtils/downloadItem");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
try {
|
||||
const response = await junoClient.getNotebookContent(data.id);
|
||||
if (!response.data) {
|
||||
throw new Error(`Received HTTP ${response.status} when fetching ${data.name}`);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
} else {
|
||||
showOkCancelModalDialog(
|
||||
component,
|
||||
"Edit/Run notebook in Cosmos DB data explorer",
|
||||
`In order to edit/run ${name} in Cosmos DB data explorer, a Cosmos DB account will be needed. If you do not have a Cosmos DB account yet, please create one.`,
|
||||
{
|
||||
linkText: "Learn more about Cosmos DB",
|
||||
linkUrl: "https://azure.microsoft.com/en-us/services/cosmos-db"
|
||||
},
|
||||
true,
|
||||
"Open data explorer",
|
||||
() => {
|
||||
window.open("https://cosmos.azure.com");
|
||||
},
|
||||
"Create Cosmos DB account",
|
||||
() => {
|
||||
window.open("https://ms.portal.azure.com/#create/Microsoft.DocumentDB");
|
||||
await container.importAndOpenContent(data.name, response.data);
|
||||
NotificationConsoleUtils.logConsoleMessage(
|
||||
ConsoleDataType.Info,
|
||||
`Successfully downloaded ${name} to My Notebooks`
|
||||
);
|
||||
|
||||
const increaseDownloadResponse = await junoClient.increaseNotebookDownloadCount(data.id);
|
||||
if (increaseDownloadResponse.data) {
|
||||
onComplete(increaseDownloadResponse.data);
|
||||
}
|
||||
} catch (error) {
|
||||
const message = `Failed to download ${data.name}: ${error}`;
|
||||
Logger.logError(message, "GalleryUtils/downloadItem");
|
||||
NotificationConsoleUtils.logConsoleMessage(ConsoleDataType.Error, message);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
NotificationConsoleUtils.clearInProgressMessageWithId(notificationId);
|
||||
},
|
||||
"Cancel",
|
||||
undefined
|
||||
);
|
||||
}
|
||||
|
||||
export async function favoriteItem(
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="items">
|
||||
<div class="items" role="menubar">
|
||||
<div class="cosmosDBTitle">
|
||||
<span
|
||||
class="title"
|
||||
@@ -53,6 +53,7 @@
|
||||
id="explorerMenu"
|
||||
name="explorer"
|
||||
class="iframe"
|
||||
title="explorer"
|
||||
src="explorer.html?v=1.0.1&platform=Hosted"
|
||||
data-bind="visible: navigationSelection() === 'explorer'"
|
||||
>
|
||||
|
||||
@@ -56,7 +56,5 @@
|
||||
data-bind="visible: navigationSelection() === 'explorer'"
|
||||
>
|
||||
</iframe>
|
||||
|
||||
<iframe id="correlation" class="iframe" data-bind="attr: { src: correlationSrc }"> </iframe>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
22
utils/accesibilityCheck.js
Normal file
22
utils/accesibilityCheck.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const { AxePuppeteer } = require("axe-puppeteer");
|
||||
const puppeteer = require("puppeteer");
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({ ignoreHTTPSErrors: true });
|
||||
const page = await browser.newPage();
|
||||
await page.setBypassCSP(true);
|
||||
await page.goto("https://localhost:1234/hostedExplorer.html");
|
||||
|
||||
const results = await new AxePuppeteer(page).withTags(["wcag2a", "wcag2aa"]).analyze();
|
||||
if (results.violations && results.violations.length && results.violations.length > 0) {
|
||||
throw results.violations;
|
||||
}
|
||||
|
||||
await page.close();
|
||||
await browser.close();
|
||||
console.log(`Accessibility Check Passed!`);
|
||||
})().catch(err => {
|
||||
console.error(`Accessibility Check Failed: ${err.length} Errors`);
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
33
utils/deployment-status/index.js
Normal file
33
utils/deployment-status/index.js
Normal file
@@ -0,0 +1,33 @@
|
||||
const fetch = require("node-fetch");
|
||||
const chalk = require("chalk");
|
||||
const moment = require("moment");
|
||||
const log = console.log;
|
||||
|
||||
async function main() {
|
||||
const prodResponse = await fetch("https://cosmos.azure.com/version.txt");
|
||||
const mpacResponse = await fetch("https://cosmos.azure.com/mpac/version.txt");
|
||||
const commitsResponse = await fetch("https://api.github.com/repos/Azure/cosmos-explorer/commits");
|
||||
const prod = await prodResponse.text();
|
||||
const mpac = await mpacResponse.text();
|
||||
const commits = await commitsResponse.json();
|
||||
const [, prodSha, prodDateString] = prod.match(/(\w+)\s(.+)/);
|
||||
const [, mpacSha, mpacDateString] = mpac.match(/(\w+)\s(.+)/);
|
||||
const prodDate = moment(prodDateString);
|
||||
const mpacDate = moment(mpacDateString);
|
||||
|
||||
let color = "red";
|
||||
|
||||
commits.forEach(commit => {
|
||||
if (commit.sha === mpacSha) {
|
||||
color = "yellow";
|
||||
log(chalk.keyword(color)(`\n=========== MPAC ${mpacDate.fromNow()} =============\n`));
|
||||
}
|
||||
if (commit.sha === prodSha) {
|
||||
color = "green";
|
||||
log(chalk.keyword(color)(`\n============= PROD ${prodDate.fromNow()} =============\n`));
|
||||
}
|
||||
log(chalk.keyword(color)(commit.commit.message.split("\n")[0], commit.author.login, commit.sha));
|
||||
});
|
||||
}
|
||||
|
||||
main();
|
||||
67
utils/deployment-status/package-lock.json
generated
Normal file
67
utils/deployment-status/package-lock.json
generated
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "deployment-status",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@types/color-name": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz",
|
||||
"integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ=="
|
||||
},
|
||||
"ansi-styles": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz",
|
||||
"integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==",
|
||||
"requires": {
|
||||
"@types/color-name": "^1.1.1",
|
||||
"color-convert": "^2.0.1"
|
||||
}
|
||||
},
|
||||
"chalk": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz",
|
||||
"integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==",
|
||||
"requires": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
}
|
||||
},
|
||||
"color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"requires": {
|
||||
"color-name": "~1.1.4"
|
||||
}
|
||||
},
|
||||
"color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
|
||||
},
|
||||
"has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.27.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.27.0.tgz",
|
||||
"integrity": "sha512-al0MUK7cpIcglMv3YF13qSgdAIqxHTO7brRtaz3DlSULbqfazqkc5kEjNrLDOM7fsjshoFIihnU8snrP7zUvhQ=="
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz",
|
||||
"integrity": "sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA=="
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.1.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz",
|
||||
"integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==",
|
||||
"requires": {
|
||||
"has-flag": "^4.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
utils/deployment-status/package.json
Normal file
17
utils/deployment-status/package.json
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "deployment-status",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.0",
|
||||
"moment": "^2.27.0",
|
||||
"node-fetch": "^2.6.0"
|
||||
}
|
||||
}
|
||||
@@ -148,7 +148,7 @@ module.exports = function(env = {}, argv = {}) {
|
||||
chunks: ["notebookViewer"]
|
||||
}),
|
||||
new HtmlWebpackPlugin({
|
||||
filename: "galleryViewer.html",
|
||||
filename: "gallery/index.html",
|
||||
template: "src/GalleryViewer/galleryViewer.html",
|
||||
chunks: ["galleryViewer"]
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user