Initial transfer from ADO (#13)

This commit is contained in:
Laurent Nguyen 2020-06-05 04:04:15 +02:00 committed by GitHub
parent ab3486bd05
commit e9d3160b57
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 827 additions and 526 deletions

View File

@ -343,7 +343,7 @@ src/Explorer/Controls/LibraryManagement/LibraryManageComponentAdapter.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx
src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewer.tsx src/NotebookViewer/NotebookViewer.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx

209
package-lock.json generated
View File

@ -1159,11 +1159,6 @@
"minimist": "^1.2.0" "minimist": "^1.2.0"
} }
}, },
"@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
},
"@emotion/is-prop-valid": { "@emotion/is-prop-valid": {
"version": "0.8.8", "version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
@ -1604,96 +1599,6 @@
} }
} }
}, },
"@material-ui/core": {
"version": "4.9.10",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.10.tgz",
"integrity": "sha512-CQuZU9Y10RkwSdxjn785kw2EPcXhv5GKauuVQufR9LlD37kjfn21Im1yvr6wsUzn81oLhEvVPz727UWC0gbqxg==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.9.10",
"@material-ui/system": "^4.9.10",
"@material-ui/types": "^5.0.1",
"@material-ui/utils": "^4.9.6",
"@types/react-transition-group": "^4.2.0",
"clsx": "^1.0.4",
"hoist-non-react-statics": "^3.3.2",
"popper.js": "^1.16.1-lts",
"prop-types": "^15.7.2",
"react-is": "^16.8.0",
"react-transition-group": "^4.3.0"
},
"dependencies": {
"dom-helpers": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^2.6.7"
}
},
"react-transition-group": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
}
}
},
"@material-ui/styles": {
"version": "4.9.14",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.14.tgz",
"integrity": "sha512-zecwWKgRU2VzdmutNovPB4s5LKI0TWyZKc/AHfPu9iY8tg4UoLjpa4Rn9roYrRfuTbBZHI6b0BXcQ8zkis0nzQ==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.8.0",
"@material-ui/types": "^5.1.0",
"@material-ui/utils": "^4.9.6",
"clsx": "^1.0.4",
"csstype": "^2.5.2",
"hoist-non-react-statics": "^3.3.2",
"jss": "^10.0.3",
"jss-plugin-camel-case": "^10.0.3",
"jss-plugin-default-unit": "^10.0.3",
"jss-plugin-global": "^10.0.3",
"jss-plugin-nested": "^10.0.3",
"jss-plugin-props-sort": "^10.0.3",
"jss-plugin-rule-value-function": "^10.0.3",
"jss-plugin-vendor-prefixer": "^10.0.3",
"prop-types": "^15.7.2"
}
},
"@material-ui/system": {
"version": "4.9.14",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.9.14.tgz",
"integrity": "sha512-oQbaqfSnNlEkXEziDcJDDIy8pbvwUmZXWNqlmIwDqr/ZdCK8FuV3f4nxikUh7hvClKV2gnQ9djh5CZFTHkZj3w==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.9.6",
"csstype": "^2.5.2",
"prop-types": "^15.7.2"
}
},
"@material-ui/types": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
"integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A=="
},
"@material-ui/utils": {
"version": "4.9.12",
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.9.12.tgz",
"integrity": "sha512-/0rgZPEOcZq5CFA4+4n6Q6zk7fi8skHhH2Bcra8R3epoJEYy5PL55LuMazPtPH1oKeRausDV/Omz4BbgFsn1HQ==",
"requires": {
"@babel/runtime": "^7.4.4",
"prop-types": "^15.7.2",
"react-is": "^16.8.0"
}
},
"@microsoft/applicationinsights-analytics-js": { "@microsoft/applicationinsights-analytics-js": {
"version": "2.5.4", "version": "2.5.4",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.4.tgz", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.4.tgz",
@ -3371,14 +3276,6 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-transition-group": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.4.tgz",
"integrity": "sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA==",
"requires": {
"@types/react": "*"
}
},
"@types/shallowequal": { "@types/shallowequal": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz",
@ -5624,11 +5521,6 @@
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
"dev": true "dev": true
}, },
"clsx": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz",
"integrity": "sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA=="
},
"co": { "co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -6193,15 +6085,6 @@
"postcss-value-parser": "^3.3.0" "postcss-value-parser": "^3.3.0"
} }
}, },
"css-vendor": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
"integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
"requires": {
"@babel/runtime": "^7.8.3",
"is-in-browser": "^1.0.2"
}
},
"css-what": { "css-what": {
"version": "2.1.3", "version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz", "resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
@ -9915,11 +9798,6 @@
} }
} }
}, },
"hyphenate-style-name": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz",
"integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ=="
},
"iconv-lite": { "iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -10459,11 +10337,6 @@
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==" "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
}, },
"is-in-browser": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
"integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU="
},
"is-number": { "is-number": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@ -11333,83 +11206,6 @@
"verror": "1.10.0" "verror": "1.10.0"
} }
}, },
"jss": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss/-/jss-10.1.1.tgz",
"integrity": "sha512-Xz3qgRUFlxbWk1czCZibUJqhVPObrZHxY3FPsjCXhDld4NOj1BgM14Ir5hVm+Qr6OLqVljjGvoMcCdXNOAbdkQ==",
"requires": {
"@babel/runtime": "^7.3.1",
"csstype": "^2.6.5",
"is-in-browser": "^1.1.3",
"tiny-warning": "^1.0.2"
}
},
"jss-plugin-camel-case": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.1.1.tgz",
"integrity": "sha512-MDIaw8FeD5uFz1seQBKz4pnvDLnj5vIKV5hXSVdMaAVq13xR6SVTVWkIV/keyTs5txxTvzGJ9hXoxgd1WTUlBw==",
"requires": {
"@babel/runtime": "^7.3.1",
"hyphenate-style-name": "^1.0.3",
"jss": "10.1.1"
}
},
"jss-plugin-default-unit": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.1.1.tgz",
"integrity": "sha512-UkeVCA/b3QEA4k0nIKS4uWXDCNmV73WLHdh2oDGZZc3GsQtlOCuiH3EkB/qI60v2MiCq356/SYWsDXt21yjwdg==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-global": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.1.1.tgz",
"integrity": "sha512-VBG3wRyi3Z8S4kMhm8rZV6caYBegsk+QnQZSVmrWw6GVOT/Z4FA7eyMu5SdkorDlG/HVpHh91oFN56O4R9m2VA==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-nested": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.1.1.tgz",
"integrity": "sha512-ozEu7ZBSVrMYxSDplPX3H82XHNQk2DQEJ9TEyo7OVTPJ1hEieqjDFiOQOxXEj9z3PMqkylnUbvWIZRDKCFYw5Q==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1",
"tiny-warning": "^1.0.2"
}
},
"jss-plugin-props-sort": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.1.1.tgz",
"integrity": "sha512-g/joK3eTDZB4pkqpZB38257yD4LXB0X15jxtZAGbUzcKAVUHPl9Jb47Y7lYmiGsShiV4YmQRqG1p2DHMYoK91g==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-rule-value-function": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.1.1.tgz",
"integrity": "sha512-ClV1lvJ3laU9la1CUzaDugEcwnpjPTuJ0yGy2YtcU+gG/w9HMInD5vEv7xKAz53Bk4WiJm5uLOElSEshHyhKNw==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-vendor-prefixer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.1.1.tgz",
"integrity": "sha512-09MZpQ6onQrhaVSF6GHC4iYifQ7+4YC/tAP6D4ZWeZotvCMq1mHLqNKRIaqQ2lkgANjlEot2JnVi1ktu4+L4pw==",
"requires": {
"@babel/runtime": "^7.3.1",
"css-vendor": "^2.0.7",
"jss": "10.1.1"
}
},
"jsx-ast-utils": { "jsx-ast-utils": {
"version": "2.2.3", "version": "2.2.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",
@ -16795,11 +16591,6 @@
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"optional": true "optional": true
}, },
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tinycolor2": { "tinycolor2": {
"version": "1.4.1", "version": "1.4.1",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",

View File

@ -8,7 +8,6 @@
"@azure/cosmos-language-service": "0.0.4", "@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "4.2.0", "@jupyterlab/services": "4.2.0",
"@jupyterlab/terminal": "1.2.1", "@jupyterlab/terminal": "1.2.1",
"@material-ui/core": "4.9.10",
"@microsoft/applicationinsights-web": "2.5.4", "@microsoft/applicationinsights-web": "2.5.4",
"@nteract/commutable": "7.1.4", "@nteract/commutable": "7.1.4",
"@nteract/connected-components": "6.7.8", "@nteract/connected-components": "6.7.8",

View File

@ -738,6 +738,8 @@ export interface GitHubInfoJunoResponse {
gitUrl: string; gitUrl: string;
htmlUrl: string; htmlUrl: string;
metadata?: NotebookMetadata; metadata?: NotebookMetadata;
officialSamplesIndex?: number;
isLikedNotebook?: boolean;
} }
export interface LikedNotebooksJunoResponse { export interface LikedNotebooksJunoResponse {

View File

@ -229,7 +229,12 @@ export interface Explorer {
importAndOpenFromGallery: (path: string, newName: string, content: any) => Promise<boolean>; importAndOpenFromGallery: (path: string, newName: string, content: any) => Promise<boolean>;
openNotebookTerminal: (kind: TerminalKind) => void; openNotebookTerminal: (kind: TerminalKind) => void;
openGallery: () => void; openGallery: () => void;
openNotebookViewer: (notebookUrl: string, notebookMetadata: DataModels.NotebookMetadata) => void; openNotebookViewer: (
notebookUrl: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => void;
notebookWorkspaceManager: NotebookWorkspaceManager; notebookWorkspaceManager: NotebookWorkspaceManager;
sparkClusterManager: SparkClusterManager; sparkClusterManager: SparkClusterManager;
notebookContentProvider: IContentProvider; notebookContentProvider: IContentProvider;
@ -887,6 +892,8 @@ export interface NotebookViewerTabOptions extends TabOptions {
notebookUrl: string; notebookUrl: string;
notebookName: string; notebookName: string;
notebookMetadata: DataModels.NotebookMetadata; notebookMetadata: DataModels.NotebookMetadata;
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>;
isLikedNotebook: boolean;
} }
export interface DocumentsTabOptions extends TabOptions { export interface DocumentsTabOptions extends TabOptions {

View File

@ -22,11 +22,28 @@ export const subtleHelpfulTextStyles: ITextStyles = {
} }
}; };
export const iconButtonStyles: IIconStyles = {
root: {
marginLeft: "10px",
color: "#0078D4",
backgroundColor: "#FFF",
fontSize: 16,
fontWeight: FontWeights.regular,
display: "inline-block",
selectors: {
":hover .ms-Button-icon": {
color: "#ccc"
}
}
}
};
export const iconStyles: IIconStyles = { export const iconStyles: IIconStyles = {
root: { root: {
marginLeft: "10px", marginLeft: "10px",
color: "#0078D4", color: "#0078D4",
fontSize: 12, backgroundColor: "#FFF",
fontSize: 16,
fontWeight: FontWeights.regular, fontWeight: FontWeights.regular,
display: "inline-block" display: "inline-block"
} }

View File

@ -0,0 +1,18 @@
import React from "react";
import { shallow } from "enzyme";
import { GalleryCardComponent, GalleryCardComponentProps } from "./GalleryCardComponent";
describe("GalleryCardComponent", () => {
it("renders", () => {
const props: GalleryCardComponentProps = {
name: "mycard",
url: "url",
notebookMetadata: undefined,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onClick: () => {}
};
const wrapper = shallow(<GalleryCardComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -1,5 +1,5 @@
import * as React from "react"; import * as React from "react";
import * as DataModels from "../../Contracts/DataModels"; import * as DataModels from "../../../../Contracts/DataModels";
import { Card, ICardTokens, ICardSectionTokens } from "@uifabric/react-cards"; import { Card, ICardTokens, ICardSectionTokens } from "@uifabric/react-cards";
import { Icon, Image, Persona, Text } from "office-ui-fabric-react"; import { Icon, Image, Persona, Text } from "office-ui-fabric-react";
import { import {
@ -10,7 +10,7 @@ import {
subtleIconStyles subtleIconStyles
} from "./CardStyleConstants"; } from "./CardStyleConstants";
interface GalleryCardComponentProps { export interface GalleryCardComponentProps {
name: string; name: string;
url: string; url: string;
notebookMetadata: DataModels.NotebookMetadata; notebookMetadata: DataModels.NotebookMetadata;

View File

@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GalleryCardComponent renders 1`] = `
<Card
aria-label="Notebook Card"
onClick={[Function]}
tokens={
Object {
"childrenMargin": 12,
}
}
>
<CardSection>
<Text
styles={
Object {
"root": Object {
"color": "#333333",
"fontWeight": 600,
},
}
}
>
mycard
</Text>
</CardSection>
</Card>
`;

View File

@ -0,0 +1,9 @@
@import "../../../../less/Common/Constants";
.galleryContainer {
padding: @LargeSpace @LargeSpace 30px @LargeSpace;
height: 100%;
overflow-y: auto;
width: 100%;
font-family: @DataExplorerFont;
}

View File

@ -0,0 +1,77 @@
import React from "react";
import { shallow } from "enzyme";
import {
GalleryViewerContainerComponent,
GalleryViewerContainerComponentProps,
FullWidthTabs,
FullWidthTabsProps,
GalleryCardsComponent,
GalleryCardsComponentProps,
GalleryViewerComponent,
GalleryViewerComponentProps
} from "./GalleryViewerComponent";
import * as DataModels from "../../../Contracts/DataModels";
describe("GalleryCardsComponent", () => {
it("renders", () => {
// TODO Mock this
const props: GalleryCardsComponentProps = {
data: [],
userMetadata: undefined,
onNotebookMetadataChange: (officialSamplesIndex: number, notebookMetadata: DataModels.NotebookMetadata) =>
Promise.resolve(),
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise.resolve()
};
const wrapper = shallow(<GalleryCardsComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
describe("FullWidthTabs", () => {
it("renders", () => {
const props: FullWidthTabsProps = {
officialSamplesContent: [],
likedNotebooksContent: [],
userMetadata: undefined,
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise.resolve()
};
const wrapper = shallow(<FullWidthTabs {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
describe("GalleryViewerContainerComponent", () => {
it("renders", () => {
const props: GalleryViewerContainerComponentProps = {
container: undefined
};
const wrapper = shallow(<GalleryViewerContainerComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});
describe("GalleryCardComponent", () => {
it("renders", () => {
const props: GalleryViewerComponentProps = {
container: undefined,
officialSamplesData: [],
likedNotebookData: undefined
};
const wrapper = shallow(<GalleryViewerComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
});

View File

@ -0,0 +1,356 @@
/**
* Gallery Viewer
*/
import * as React from "react";
import * as DataModels from "../../../Contracts/DataModels";
import * as ViewModels from "../../../Contracts/ViewModels";
import { GalleryCardComponent } from "./Cards/GalleryCardComponent";
import { Stack, IStackTokens } from "office-ui-fabric-react";
import { JunoUtils } from "../../../Utils/JunoUtils";
import { CosmosClient } from "../../../Common/CosmosClient";
import { config } from "../../../Config";
import path from "path";
import { SessionStorageUtility, StorageKey } from "../../../Shared/StorageUtility";
import { NotificationConsoleUtils } from "../../../Utils/NotificationConsoleUtils";
import { ConsoleDataType } from "../../Menus/NotificationConsole/NotificationConsoleComponent";
import * as TabComponent from "../Tabs/TabComponent";
import "./GalleryViewerComponent.less";
export interface GalleryCardsComponentProps {
data: DataModels.GitHubInfoJunoResponse[];
userMetadata: DataModels.UserMetadata;
onNotebookMetadataChange: (
officialSamplesIndex: number,
notebookMetadata: DataModels.NotebookMetadata
) => Promise<void>;
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise<void>;
}
export class GalleryCardsComponent extends React.Component<GalleryCardsComponentProps> {
private sectionStackTokens: IStackTokens = { childrenGap: 30 };
public render(): JSX.Element {
return (
<Stack horizontal wrap tokens={this.sectionStackTokens}>
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse, index: any) => {
const name = githubInfo.name;
const url = githubInfo.downloadUrl;
const notebookMetadata = githubInfo.metadata || {
date: "2008-12-01",
description: "Great notebook",
tags: ["favorite", "sample"],
author: "Laurent Nguyen",
views: 432,
likes: 123,
downloads: 56,
imageUrl:
"https://media.magazine.ferrari.com/images/2019/02/27/170304506-c1bcf028-b513-45f6-9f27-0cadac619c3d.jpg"
};
const officialSamplesIndex = githubInfo.officialSamplesIndex;
const isLikedNotebook = githubInfo.isLikedNotebook;
const updateTabsStatePerNotebook = this.props.onNotebookMetadataChange
? (notebookMetadata: DataModels.NotebookMetadata) =>
this.props.onNotebookMetadataChange(officialSamplesIndex, notebookMetadata)
: undefined;
return (
name !== ".gitignore" &&
url && (
<GalleryCardComponent
key={url}
name={name}
url={url}
notebookMetadata={notebookMetadata}
onClick={() => this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)}
/>
)
);
})}
</Stack>
);
}
}
export interface FullWidthTabsProps {
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
userMetadata: DataModels.UserMetadata;
onClick: (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => Promise<void>;
}
interface FullWidthTabsState {
activeTabIndex: number;
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
userMetadata: DataModels.UserMetadata;
}
export class FullWidthTabs extends React.Component<FullWidthTabsProps, FullWidthTabsState> {
private authorizationToken = CosmosClient.authorizationToken();
private appTabs: TabComponent.Tab[];
constructor(props: FullWidthTabsProps) {
super(props);
this.state = {
activeTabIndex: 0,
officialSamplesContent: this.props.officialSamplesContent,
likedNotebooksContent: this.props.likedNotebooksContent,
userMetadata: this.props.userMetadata
};
this.appTabs = [
{
title: "Official Samples",
content: {
className: "",
render: () => (
<GalleryCardsComponent
data={this.state.officialSamplesContent}
onClick={this.props.onClick}
userMetadata={this.state.userMetadata}
onNotebookMetadataChange={this.updateTabsState}
/>
)
},
isVisible: () => true
},
{
title: "Liked Notebooks",
content: {
className: "",
render: () => (
<GalleryCardsComponent
data={this.state.likedNotebooksContent}
onClick={this.props.onClick}
userMetadata={this.state.userMetadata}
onNotebookMetadataChange={this.updateTabsState}
/>
)
},
isVisible: () => true
}
];
}
public updateTabsState = async (officialSamplesIndex: number, notebookMetadata: DataModels.NotebookMetadata) => {
let currentLikedNotebooksContent = [...this.state.likedNotebooksContent];
let currentUserMetadata = { ...this.state.userMetadata };
let currentLikedNotebooks = [...currentUserMetadata.likedNotebooks];
const currentOfficialSamplesContent = [...this.state.officialSamplesContent];
const currentOfficialSamplesObject = { ...currentOfficialSamplesContent[officialSamplesIndex] };
const metadata = { ...currentOfficialSamplesObject.metadata };
const metadataLikesUpdates = metadata.likes - notebookMetadata.likes;
metadata.views = notebookMetadata.views;
metadata.downloads = notebookMetadata.downloads;
metadata.likes = notebookMetadata.likes;
currentOfficialSamplesObject.metadata = metadata;
// Notebook has been liked. Add To likedNotebooksContent, update isLikedNotebook flag
if (metadataLikesUpdates < 0) {
currentOfficialSamplesObject.isLikedNotebook = true;
currentLikedNotebooksContent = currentLikedNotebooksContent.concat(currentOfficialSamplesObject);
currentLikedNotebooks = currentLikedNotebooks.concat(currentOfficialSamplesObject.path);
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
} else if (metadataLikesUpdates > 0) {
// Notebook has been unliked. Remove from likedNotebooksContent after matching the path, update isLikedNotebook flag
currentOfficialSamplesObject.isLikedNotebook = false;
const likedNotebookIndex = currentLikedNotebooks.findIndex((path: string) => {
return path === currentOfficialSamplesObject.path;
});
currentLikedNotebooksContent.splice(likedNotebookIndex, 1);
currentLikedNotebooks.splice(likedNotebookIndex, 1);
currentUserMetadata = { likedNotebooks: currentLikedNotebooks };
}
currentOfficialSamplesContent[officialSamplesIndex] = currentOfficialSamplesObject;
this.setState({
activeTabIndex: 0,
userMetadata: currentUserMetadata,
likedNotebooksContent: currentLikedNotebooksContent,
officialSamplesContent: currentOfficialSamplesContent
});
JunoUtils.updateNotebookMetadata(this.authorizationToken, notebookMetadata).then(
async returnedNotebookMetadata => {
if (metadataLikesUpdates !== 0) {
JunoUtils.updateUserMetadata(this.authorizationToken, currentUserMetadata);
// TODO: update state here?
}
},
error => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error updating notebook metadata: ${JSON.stringify(error)}`
);
// TODO add telemetry
}
);
};
private onTabIndexChange = (activeTabIndex: number) => this.setState({ activeTabIndex });
public render() {
return (
<TabComponent.TabComponent
tabs={this.appTabs}
onTabIndexChange={this.onTabIndexChange.bind(this)}
currentTabIndex={this.state.activeTabIndex}
hideHeader={false}
/>
);
}
}
export interface GalleryViewerContainerComponentProps {
container: ViewModels.Explorer;
}
interface GalleryViewerContainerComponentState {
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
likedNotebooksData: DataModels.LikedNotebooksJunoResponse;
}
export class GalleryViewerContainerComponent extends React.Component<
GalleryViewerContainerComponentProps,
GalleryViewerContainerComponentState
> {
constructor(props: GalleryViewerContainerComponentProps) {
super(props);
this.state = {
officialSamplesData: undefined,
likedNotebooksData: undefined
};
}
componentDidMount() {
const authToken = CosmosClient.authorizationToken();
JunoUtils.getOfficialSampleNotebooks(authToken).then(
(data1: DataModels.GitHubInfoJunoResponse[]) => {
const officialSamplesData = data1;
JunoUtils.getLikedNotebooks(authToken).then(
(data2: DataModels.LikedNotebooksJunoResponse) => {
const likedNotebooksData = data2;
officialSamplesData.map((value: DataModels.GitHubInfoJunoResponse, index: number) => {
value.officialSamplesIndex = index;
value.isLikedNotebook = likedNotebooksData.userMetadata.likedNotebooks.includes(value.path);
});
likedNotebooksData.likedNotebooksContent.map((value: DataModels.GitHubInfoJunoResponse) => {
value.isLikedNotebook = true;
value.officialSamplesIndex = officialSamplesData.findIndex(
(officialSample: DataModels.GitHubInfoJunoResponse) => {
return officialSample.path === value.path;
}
);
});
this.setState({
officialSamplesData: officialSamplesData,
likedNotebooksData: likedNotebooksData
});
},
error => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error fetching liked notebooks: ${JSON.stringify(error)}`
);
// TODO Add telemetry
}
);
},
error => {
NotificationConsoleUtils.logConsoleMessage(
ConsoleDataType.Error,
`Error fetching sample notebooks: ${JSON.stringify(error)}`
);
// TODO Add telemetry
}
);
}
public render(): JSX.Element {
return this.state.officialSamplesData && this.state.likedNotebooksData ? (
<GalleryViewerComponent
container={this.props.container}
officialSamplesData={this.state.officialSamplesData}
likedNotebookData={this.state.likedNotebooksData}
/>
) : (
<></>
);
}
}
export interface GalleryViewerComponentProps {
container: ViewModels.Explorer;
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
likedNotebookData: DataModels.LikedNotebooksJunoResponse;
}
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps> {
public render(): JSX.Element {
return this.props.container ? (
<div className="galleryContainer">
<FullWidthTabs
officialSamplesContent={this.props.officialSamplesData}
likedNotebooksContent={this.props.likedNotebookData.likedNotebooksContent}
userMetadata={this.props.likedNotebookData.userMetadata}
onClick={this.openNotebookViewer}
/>
</div>
) : (
<div className="galleryContainer">
<GalleryCardsComponent
data={this.props.officialSamplesData}
onClick={this.openNotebookViewer}
userMetadata={undefined}
onNotebookMetadataChange={undefined}
/>
</div>
);
}
public getOfficialSamplesData(): DataModels.GitHubInfoJunoResponse[] {
return this.props.officialSamplesData;
}
public getLikedNotebookData(): DataModels.LikedNotebooksJunoResponse {
return this.props.likedNotebookData;
}
public openNotebookViewer = async (
url: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) => {
if (!this.props.container) {
SessionStorageUtility.setEntryString(
StorageKey.NotebookMetadata,
notebookMetadata ? JSON.stringify(notebookMetadata) : null
);
SessionStorageUtility.setEntryString(StorageKey.NotebookName, path.basename(url));
window.open(`${config.hostedExplorerURL}notebookViewer.html?notebookurl=${url}`, "_blank");
} else {
this.props.container.openNotebookViewer(url, notebookMetadata, onNotebookMetadataChange, isLikedNotebook);
}
};
}

View File

@ -0,0 +1,54 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FullWidthTabs renders 1`] = `
<TabComponent
currentTabIndex={0}
hideHeader={false}
onTabIndexChange={[Function]}
tabs={
Array [
Object {
"content": Object {
"className": "",
"render": [Function],
},
"isVisible": [Function],
"title": "Official Samples",
},
Object {
"content": Object {
"className": "",
"render": [Function],
},
"isVisible": [Function],
"title": "Liked Notebooks",
},
]
}
/>
`;
exports[`GalleryCardComponent renders 1`] = `
<div
className="galleryContainer"
>
<GalleryCardsComponent
data={Array []}
onClick={[Function]}
/>
</div>
`;
exports[`GalleryCardsComponent renders 1`] = `
<Stack
horizontal={true}
tokens={
Object {
"childrenGap": 30,
}
}
wrap={true}
/>
`;
exports[`GalleryViewerContainerComponent renders 1`] = `<Fragment />`;

View File

@ -0,0 +1,11 @@
.notebookViewerMetadataContainer {
margin: 0px 10px;
.title, .decoration, .persona {
display: inline-block;
}
.extras {
margin-top: 5px;
}
}

View File

@ -0,0 +1,36 @@
import React from "react";
import { shallow } from "enzyme";
import { NotebookMetadataComponentProps, NotebookMetadataComponent } from "./NotebookMetadataComponent";
import { NotebookMetadata } from "../../../Contracts/DataModels";
describe("NotebookMetadataComponent", () => {
it("renders un-liked notebook", () => {
const props: NotebookMetadataComponentProps = {
notebookName: "My notebook",
container: undefined,
notebookMetadata: undefined,
notebookContent: {},
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise.resolve(),
isLikedNotebook: false
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
it("renders liked notebook", () => {
const props: NotebookMetadataComponentProps = {
notebookName: "My notebook",
container: undefined,
notebookMetadata: undefined,
notebookContent: {},
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise.resolve(),
isLikedNotebook: true
};
const wrapper = shallow(<NotebookMetadataComponent {...props} />);
expect(wrapper).toMatchSnapshot();
});
// TODO Add test for metadata display
});

View File

@ -6,47 +6,97 @@ import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { NotebookMetadata } from "../../../Contracts/DataModels"; import { NotebookMetadata } from "../../../Contracts/DataModels";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
import { Icon, Persona, Text } from "office-ui-fabric-react"; import { Icon, Persona, Text, IconButton } from "office-ui-fabric-react";
import CSS from "csstype";
import { import {
siteTextStyles, siteTextStyles,
subtleIconStyles, subtleIconStyles,
iconStyles, iconStyles,
iconButtonStyles,
mainHelpfulTextStyles, mainHelpfulTextStyles,
subtleHelpfulTextStyles, subtleHelpfulTextStyles,
helpfulTextStyles helpfulTextStyles
} from "../../../GalleryViewer/Cards/CardStyleConstants"; } from "../NotebookGallery/Cards/CardStyleConstants";
import "./NotebookViewerComponent.less";
initializeIcons(); initializeIcons();
interface NotebookMetadataComponentProps { export interface NotebookMetadataComponentProps {
notebookName: string; notebookName: string;
container: ViewModels.Explorer; container: ViewModels.Explorer;
notebookMetadata: NotebookMetadata; notebookMetadata: NotebookMetadata;
notebookContent: any; notebookContent: any;
onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
isLikedNotebook: boolean;
} }
export class NotebookMetadataComponent extends React.Component<NotebookMetadataComponentProps> { interface NotebookMetadatComponentState {
private inlineBlockStyle: CSS.Properties = { liked: boolean;
display: "inline-block" notebookMetadata: NotebookMetadata;
}
export class NotebookMetadataComponent extends React.Component<
NotebookMetadataComponentProps,
NotebookMetadatComponentState
> {
constructor(props: NotebookMetadataComponentProps) {
super(props);
this.state = {
liked: this.props.isLikedNotebook,
notebookMetadata: this.props.notebookMetadata
};
}
private onDownloadClick = (newNotebookName: string) => {
this.props.container
.importAndOpenFromGallery(this.props.notebookName, newNotebookName, JSON.stringify(this.props.notebookContent))
.then(() => {
if (this.props.notebookMetadata) {
if (this.props.onNotebookMetadataChange) {
const notebookMetadata = { ...this.state.notebookMetadata };
notebookMetadata.downloads += 1;
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
this.setState({ notebookMetadata: notebookMetadata });
});
}
}
});
}; };
private marginTopStyle: CSS.Properties = { componentDidMount() {
marginTop: "5px" if (this.props.onNotebookMetadataChange) {
const notebookMetadata = { ...this.state.notebookMetadata };
if (this.props.notebookMetadata) {
notebookMetadata.views += 1;
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
this.setState({ notebookMetadata: notebookMetadata });
});
}
}
}
private onLike = (): void => {
if (this.props.onNotebookMetadataChange) {
const notebookMetadata = { ...this.state.notebookMetadata };
let liked: boolean;
if (this.state.liked) {
liked = false;
notebookMetadata.likes -= 1;
} else {
liked = true;
notebookMetadata.likes += 1;
}
this.props.onNotebookMetadataChange(notebookMetadata).then(() => {
this.setState({ liked: liked, notebookMetadata: notebookMetadata });
});
}
}; };
private onDownloadClick: (newNotebookName: string) => void = (newNotebookName: string) => { private onDownload = (): void => {
this.props.container.importAndOpenFromGallery(
this.props.notebookName,
newNotebookName,
JSON.stringify(this.props.notebookContent)
);
};
public render(): JSX.Element {
const promptForNotebookName = () => { const promptForNotebookName = () => {
return new Promise<string>((resolve, reject) => { return new Promise<string>((resolve, reject) => {
var newNotebookName = this.props.notebookName; let newNotebookName = this.props.notebookName;
this.props.container.showOkCancelTextFieldModalDialog( this.props.container.showOkCancelTextFieldModalDialog(
"Save notebook as", "Save notebook as",
undefined, undefined,
@ -68,27 +118,35 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
}); });
}; };
promptForNotebookName().then((newNotebookName: string) => {
this.onDownloadClick(newNotebookName);
});
};
public render(): JSX.Element {
return ( return (
<div className="notebookViewerMetadataContainer"> <div className="notebookViewerMetadataContainer">
<h3 style={this.inlineBlockStyle}>{this.props.notebookName}</h3> <h3 className="title">{this.props.notebookName}</h3>
{this.props.notebookMetadata && ( {this.props.notebookMetadata && (
<div style={this.inlineBlockStyle}> <div className="decoration">
<Icon iconName="Heart" styles={iconStyles} /> {this.props.container ? (
<Text variant="medium" styles={mainHelpfulTextStyles}> <IconButton
{this.props.notebookMetadata.likes} likes iconProps={{ iconName: this.state.liked ? "HeartFill" : "Heart" }}
styles={iconButtonStyles}
onClick={this.onLike}
/>
) : (
<Icon iconName="Heart" styles={iconStyles} />
)}
<Text variant="large" styles={mainHelpfulTextStyles}>
{this.state.notebookMetadata.likes} likes
</Text> </Text>
</div> </div>
)} )}
{this.props.container && ( {this.props.container && (
<button <button aria-label="downloadButton" className="downloadButton" onClick={this.onDownload}>
aria-label="downloadButton"
className="downloadButton"
onClick={async () => {
promptForNotebookName().then(this.onDownloadClick);
}}
>
Download Notebook Download Notebook
</button> </button>
)} )}
@ -97,20 +155,20 @@ export class NotebookMetadataComponent extends React.Component<NotebookMetadataC
<> <>
<div> <div>
<Persona <Persona
style={this.inlineBlockStyle} className="persona"
text={this.props.notebookMetadata.author} text={this.props.notebookMetadata.author}
secondaryText={this.props.notebookMetadata.date} secondaryText={this.props.notebookMetadata.date}
/> />
</div> </div>
<div> <div>
<div style={this.marginTopStyle}> <div className="extras">
<Icon iconName="RedEye" styles={subtleIconStyles} /> <Icon iconName="RedEye" styles={subtleIconStyles} />
<Text variant="small" styles={subtleHelpfulTextStyles}> <Text variant="small" styles={subtleHelpfulTextStyles}>
{this.props.notebookMetadata.views} {this.state.notebookMetadata.views}
</Text> </Text>
<Icon iconName="Download" styles={subtleIconStyles} /> <Icon iconName="Download" styles={subtleIconStyles} />
<Text variant="small" styles={subtleHelpfulTextStyles}> <Text variant="small" styles={subtleHelpfulTextStyles}>
{this.props.notebookMetadata.downloads} {this.state.notebookMetadata.downloads}
</Text> </Text>
</div> </div>
<Text variant="small" styles={siteTextStyles}> <Text variant="small" styles={siteTextStyles}>

View File

@ -4,7 +4,7 @@
padding: @DefaultSpace; padding: @DefaultSpace;
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow-y: scroll; overflow-y: auto;
} }
.downloadButton { .downloadButton {
@ -20,7 +20,7 @@
display: "inline-block"; display: "inline-block";
margin: 10px; margin: 10px;
} }
.active, .downloadButton:hover { .active, .downloadButton:hover {
color: @BaseMedium; color: @BaseMedium;
} }

View File

@ -16,12 +16,14 @@ import "./NotebookViewerComponent.less";
export interface NotebookViewerComponentProps { export interface NotebookViewerComponentProps {
notebookName: string; notebookName: string;
notebookUrl: string; notebookUrl: string;
container: ViewModels.Explorer; container?: ViewModels.Explorer;
notebookMetadata: NotebookMetadata; notebookMetadata: NotebookMetadata;
onNotebookMetadataChange?: (newNotebookMetadata: NotebookMetadata) => Promise<void>;
isLikedNotebook?: boolean;
hideInputs?: boolean;
} }
interface NotebookViewerComponentState { interface NotebookViewerComponentState {
element: JSX.Element;
content: any; content: any;
} }
@ -50,7 +52,7 @@ export class NotebookViewerComponent extends React.Component<
contentRef: createContentRef() contentRef: createContentRef()
}); });
this.state = { element: undefined, content: undefined }; this.state = { content: undefined };
} }
private async getJsonNotebookContent(): Promise<any> { private async getJsonNotebookContent(): Promise<any> {
@ -65,24 +67,25 @@ export class NotebookViewerComponent extends React.Component<
componentDidMount() { componentDidMount() {
this.getJsonNotebookContent().then((jsonContent: any) => { this.getJsonNotebookContent().then((jsonContent: any) => {
this.notebookComponentBootstrapper.setContent("json", jsonContent); this.notebookComponentBootstrapper.setContent("json", jsonContent);
const notebookReadonlyComponent = this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer); this.setState({ content: jsonContent });
this.setState({ element: notebookReadonlyComponent, content: jsonContent });
}); });
} }
public render(): JSX.Element { public render(): JSX.Element {
return this.state != null ? ( return (
<div className="notebookViewerContainer"> <div className="notebookViewerContainer">
<NotebookMetadataComponent <NotebookMetadataComponent
notebookMetadata={this.props.notebookMetadata} notebookMetadata={this.props.notebookMetadata}
notebookName={this.props.notebookName} notebookName={this.props.notebookName}
container={this.props.container} container={this.props.container}
notebookContent={this.state.content} notebookContent={this.state.content}
onNotebookMetadataChange={this.props.onNotebookMetadataChange}
isLikedNotebook={this.props.isLikedNotebook}
/> />
{this.state.element} {this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, {
hideInputs: this.props.hideInputs
})}
</div> </div>
) : (
<></>
); );
} }
} }

View File

@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`NotebookMetadataComponent renders liked notebook 1`] = `
<div
className="notebookViewerMetadataContainer"
>
<h3
className="title"
>
My notebook
</h3>
</div>
`;
exports[`NotebookMetadataComponent renders un-liked notebook 1`] = `
<div
className="notebookViewerMetadataContainer"
>
<h3
className="title"
>
My notebook
</h3>
</div>
`;

View File

@ -1,27 +1,28 @@
@import "../../../../less/Common/Constants"; @import "../../../../less/Common/Constants";
.tabSwitch { .tabComponentContainer {
margin-left: @LargeSpace; height: 100%;
margin-bottom: 20px;
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
}
.tabComponentContent {
height: calc(100% - 20px);
.flex-display(); .flex-display();
.flex-direction();
.tabSwitch {
margin-left: @LargeSpace;
margin-bottom: 20px;
.tab {
margin-right: @MediumSpace;
}
.toggleSwitch {
.toggleSwitch();
}
.selectedToggle {
.selectedToggle();
}
.unselectedToggle {
.unselectedToggle();
}
}
} }

View File

@ -1,5 +1,6 @@
import * as React from "react"; import * as React from "react";
import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement"; import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement";
import "./TabComponent.less";
export interface TabContent { export interface TabContent {
render: () => JSX.Element; render: () => JSX.Element;
@ -75,10 +76,10 @@ export class TabComponent extends React.Component<TabComponentProps> {
} }
return ( return (
<React.Fragment> <div className="tabComponentContainer">
{!this.props.hideHeader && <div className="tabs tabSwitch">{this.renderTabTitles()}</div>} {!this.props.hideHeader && <div className="tabs tabSwitch">{this.renderTabTitles()}</div>}
<div className={className}>{currentTabContent.render()}</div> <div className={className}>{currentTabContent.render()}</div>
</React.Fragment> </div>
); );
} }
} }

View File

@ -1143,7 +1143,7 @@ export default class Explorer implements ViewModels.Explorer {
isModal: true, isModal: true,
visible: true, visible: true,
title: `Enable Azure Synapse Link on your Cosmos DB account`, title: `Enable Azure Synapse Link on your Cosmos DB account`,
subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads. subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads.
Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`, Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`,
primaryButtonText: "Enable Azure Synapse Link", primaryButtonText: "Enable Azure Synapse Link",
secondaryButtonText: "Cancel", secondaryButtonText: "Cancel",
@ -3278,7 +3278,12 @@ export default class Explorer implements ViewModels.Explorer {
newTab.onTabClick(); newTab.onTabClick();
} }
public openNotebookViewer(notebookUrl: string, notebookMetadata: DataModels.NotebookMetadata) { public openNotebookViewer(
notebookUrl: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) {
const notebookName = path.basename(notebookUrl); const notebookName = path.basename(notebookUrl);
const title = notebookName; const title = notebookName;
const hashLocation = notebookUrl; const hashLocation = notebookUrl;
@ -3320,7 +3325,9 @@ export default class Explorer implements ViewModels.Explorer {
openedTabs: this.openedTabs(), openedTabs: this.openedTabs(),
notebookUrl: notebookUrl, notebookUrl: notebookUrl,
notebookName: notebookName, notebookName: notebookName,
notebookMetadata: notebookMetadata notebookMetadata: notebookMetadata,
onNotebookMetadataChange: onNotebookMetadataChange,
isLikedNotebook: isLikedNotebook
}); });
this.openedTabs.push(newTab); this.openedTabs.push(newTab);

View File

@ -9,11 +9,12 @@ import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { actions, ContentRef } from "@nteract/core"; import { actions, ContentRef } from "@nteract/core";
import loadTransform from "../NotebookComponent/loadTransform"; import loadTransform from "../NotebookComponent/loadTransform";
import CodeMirrorEditor from "@nteract/editor"; import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror";
import "./NotebookReadOnlyRenderer.less"; import "./NotebookReadOnlyRenderer.less";
export interface NotebookRendererProps { export interface NotebookRendererProps {
contentRef: any; contentRef: any;
hideInputs?: boolean;
} }
interface PassedEditorProps { interface PassedEditorProps {
@ -46,7 +47,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<CodeCell id={id} contentRef={contentRef}> <CodeCell id={id} contentRef={contentRef}>
{{ {{
editor: { editor: {
codemirror: (props: PassedEditorProps) => <CodeMirrorEditor {...props} readOnly={"nocursor"} /> codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
}, },
prompt: ({ id, contentRef }) => <></> prompt: ({ id, contentRef }) => <></>
}} }}
@ -63,7 +65,8 @@ class NotebookReadOnlyRenderer extends React.Component<NotebookRendererProps> {
<RawCell id={id} contentRef={contentRef} cell_type="raw"> <RawCell id={id} contentRef={contentRef} cell_type="raw">
{{ {{
editor: { editor: {
codemirror: (props: PassedEditorProps) => <CodeMirrorEditor {...props} readOnly={"nocursor"} /> codemirror: (props: PassedEditorProps) =>
this.props.hideInputs ? <></> : <CodeMirrorEditor {...props} readOnly={"nocursor"} />
} }
}} }}
</RawCell> </RawCell>

View File

@ -3,7 +3,7 @@ import * as ViewModels from "../../Contracts/ViewModels";
import TabsBase from "./TabsBase"; import TabsBase from "./TabsBase";
import * as React from "react"; import * as React from "react";
import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler";
import { GalleryViewerContainerComponent } from "../../GalleryViewer/GalleryViewerComponent"; import { GalleryViewerContainerComponent } from "../Controls/NotebookGallery/GalleryViewerComponent";
/** /**
* Notebook gallery tab * Notebook gallery tab

View File

@ -16,7 +16,9 @@ class NotebookViewerComponentAdapter implements ReactAdapter {
private notebookUrl: string, private notebookUrl: string,
private notebookName: string, private notebookName: string,
private container: ViewModels.Explorer, private container: ViewModels.Explorer,
private notebookMetadata: DataModels.NotebookMetadata private notebookMetadata: DataModels.NotebookMetadata,
private onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
private isLikedNotebook: boolean
) {} ) {}
public renderComponent(): JSX.Element { public renderComponent(): JSX.Element {
@ -26,6 +28,8 @@ class NotebookViewerComponentAdapter implements ReactAdapter {
notebookMetadata={this.notebookMetadata} notebookMetadata={this.notebookMetadata}
notebookName={this.notebookName} notebookName={this.notebookName}
container={this.container} container={this.container}
onNotebookMetadataChange={this.onNotebookMetadataChange}
isLikedNotebook={this.isLikedNotebook}
/> />
) : ( ) : (
<></> <></>
@ -46,7 +50,9 @@ export default class NotebookViewerTab extends TabsBase implements ViewModels.Ta
options.notebookUrl, options.notebookUrl,
options.notebookName, options.notebookName,
options.container, options.container,
options.notebookMetadata options.notebookMetadata,
options.onNotebookMetadataChange,
options.isLikedNotebook
); );
this.notebookViewerComponentAdapter.parameters = ko.computed<boolean>(() => { this.notebookViewerComponentAdapter.parameters = ko.computed<boolean>(() => {

View File

@ -1,9 +0,0 @@
@import "../../less/Common/Constants";
.galleryContainer {
padding: @DefaultSpace;
height: 100%;
overflow-y: scroll;
width: 100%;
font-family: @DataExplorerFont;
}

View File

@ -1,13 +1,13 @@
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import "bootstrap/dist/css/bootstrap.css"; import "bootstrap/dist/css/bootstrap.css";
import "./GalleryViewer.less"; import { CosmosClient } from "../Common/CosmosClient";
import { GalleryViewerComponent } from "./GalleryViewerComponent"; import { GalleryViewerComponent } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import { JunoUtils } from "../Utils/JunoUtils"; import { JunoUtils } from "../Utils/JunoUtils";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
const onInit = async () => { const onInit = async () => {
initializeIcons(); initializeIcons();
const officialSamplesData = await JunoUtils.getOfficialSampleNotebooks(); const officialSamplesData = await JunoUtils.getOfficialSampleNotebooks(CosmosClient.authorizationToken());
const galleryViewerComponent = new GalleryViewerComponent({ const galleryViewerComponent = new GalleryViewerComponent({
officialSamplesData: officialSamplesData, officialSamplesData: officialSamplesData,
likedNotebookData: undefined, likedNotebookData: undefined,

View File

@ -1,207 +0,0 @@
/**
* Gallery Viewer
*/
import * as React from "react";
import * as DataModels from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { GalleryCardComponent } from "./Cards/GalleryCardComponent";
import { Stack, IStackTokens } from "office-ui-fabric-react";
import AppBar from "@material-ui/core/AppBar";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import Typography from "@material-ui/core/Typography";
import Box from "@material-ui/core/Box";
import { JunoUtils } from "../Utils/JunoUtils";
import { CosmosClient } from "../Common/CosmosClient";
import { config } from "../Config";
import path from "path";
import { SessionStorageUtility, StorageKey } from "../Shared/StorageUtility";
import "./GalleryViewer.less";
interface GalleryCardsComponentProps {
data: DataModels.GitHubInfoJunoResponse[];
onClick: (url: string, notebookMetadata: DataModels.NotebookMetadata) => Promise<void>;
}
class GalleryCardsComponent extends React.Component<GalleryCardsComponentProps> {
private sectionStackTokens: IStackTokens = { childrenGap: 30 };
public render(): JSX.Element {
return (
<Stack horizontal wrap tokens={this.sectionStackTokens}>
{this.props.data.map((githubInfo: DataModels.GitHubInfoJunoResponse, index: any) => {
const name = githubInfo.name;
const url = githubInfo.downloadUrl;
const notebookMetadata = githubInfo.metadata;
return (
name !== ".gitignore" &&
url && (
<GalleryCardComponent
key={url}
name={name}
url={url}
notebookMetadata={notebookMetadata}
onClick={() => this.props.onClick(url, notebookMetadata)}
/>
)
);
})}
</Stack>
);
}
}
const TabPanel = (props: any) => (
<Typography
component="div"
role="tabpanel"
hidden={props.value !== props.index}
id={`full-width-tabpanel-${props.index}`}
aria-labelledby={`full-width-tab-${props.index}`}
>
{props.value === props.index && <Box p={2}>{props.children}</Box>}
</Typography>
);
const a11yProps = (index: number) => {
return {
id: `full-width-tab-${index}`,
"aria-controls": `full-width-tabpanel-${index}`
};
};
interface FullWidthTabsProps {
officialSamplesContent: DataModels.GitHubInfoJunoResponse[];
likedNotebooksContent: DataModels.GitHubInfoJunoResponse[];
onClick: (url: string, notebookMetadata: DataModels.NotebookMetadata) => Promise<void>;
}
const FullWidthTabs = (props: FullWidthTabsProps) => {
const [value, setValue] = React.useState(0);
const handleChange = ({}, newValue: any) => {
setValue(newValue);
};
return (
<>
<AppBar position="static" color="transparent" style={{ background: "transparent", boxShadow: "none" }}>
<Tabs
value={value}
onChange={handleChange}
indicatorColor="primary"
textColor="primary"
aria-label="gallery tabs"
>
<Tab label="Official Samples" {...a11yProps(0)} />
<Tab label="Liked Notebooks" {...a11yProps(1)} />
</Tabs>
</AppBar>
<TabPanel value={value} index={0}>
<GalleryCardsComponent data={props.officialSamplesContent} onClick={props.onClick} />
</TabPanel>
<TabPanel value={value} index={1}>
<GalleryCardsComponent data={props.likedNotebooksContent} onClick={props.onClick} />
</TabPanel>
</>
);
};
export interface GalleryViewerContainerComponentProps {
container: ViewModels.Explorer;
}
export interface GalleryViewerContainerComponentState {
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
likedNotebooksData: DataModels.LikedNotebooksJunoResponse;
}
export class GalleryViewerContainerComponent extends React.Component<
GalleryViewerContainerComponentProps,
GalleryViewerContainerComponentState
> {
constructor(props: GalleryViewerContainerComponentProps) {
super(props);
this.state = {
officialSamplesData: undefined,
likedNotebooksData: undefined
};
}
componentDidMount() {
JunoUtils.getOfficialSampleNotebooks().then((data1: DataModels.GitHubInfoJunoResponse[]) => {
const officialSamplesData = data1;
JunoUtils.getLikedNotebooks(CosmosClient.authorizationToken()).then(
(data2: DataModels.LikedNotebooksJunoResponse) => {
const likedNotebooksData = data2;
this.setState({
officialSamplesData: officialSamplesData,
likedNotebooksData: likedNotebooksData
});
}
);
});
}
public render(): JSX.Element {
return this.state.officialSamplesData && this.state.likedNotebooksData ? (
<GalleryViewerComponent
container={this.props.container}
officialSamplesData={this.state.officialSamplesData}
likedNotebookData={this.state.likedNotebooksData}
/>
) : (
<></>
);
}
}
export interface GalleryViewerComponentProps {
container: ViewModels.Explorer;
officialSamplesData: DataModels.GitHubInfoJunoResponse[];
likedNotebookData: DataModels.LikedNotebooksJunoResponse;
}
export class GalleryViewerComponent extends React.Component<GalleryViewerComponentProps> {
private authorizationToken = CosmosClient.authorizationToken();
public render(): JSX.Element {
return this.props.container ? (
<div className="galleryContainer">
<FullWidthTabs
officialSamplesContent={this.props.officialSamplesData}
likedNotebooksContent={this.props.likedNotebookData.likedNotebooksContent}
onClick={this.openNotebookViewer}
/>
</div>
) : (
<div className="galleryContainer">
<GalleryCardsComponent data={this.props.officialSamplesData} onClick={this.openNotebookViewer} />
</div>
);
}
public getOfficialSamplesData(): DataModels.GitHubInfoJunoResponse[] {
return this.props.officialSamplesData;
}
public getLikedNotebookData(): DataModels.LikedNotebooksJunoResponse {
return this.props.likedNotebookData;
}
public openNotebookViewer = async (url: string, notebookMetadata: DataModels.NotebookMetadata) => {
if (!this.props.container) {
SessionStorageUtility.setEntryString(
StorageKey.NotebookMetadata,
notebookMetadata ? JSON.stringify(notebookMetadata) : null
);
SessionStorageUtility.setEntryString(StorageKey.NotebookName, path.basename(url));
window.open(`${config.hostedExplorerURL}notebookViewer.html?notebookurl=${url}`, "_blank");
} else {
this.props.container.openNotebookViewer(url, notebookMetadata);
}
};
}

View File

@ -15,7 +15,6 @@ import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less";
import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less";
import "./Explorer/Controls/DynamicList/DynamicListComponent.less"; import "./Explorer/Controls/DynamicList/DynamicListComponent.less";
import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less";
import "./Explorer/Controls/Tabs/TabComponent.less";
import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less";
import "../less/TableStyles/queryBuilder.less"; import "../less/TableStyles/queryBuilder.less";
import "../externals/jquery.dataTables.min.css"; import "../externals/jquery.dataTables.min.css";

View File

@ -1,9 +1,9 @@
import React from "react"; import React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import "bootstrap/dist/css/bootstrap.css"; import "bootstrap/dist/css/bootstrap.css";
import { NotebookMetadata } from "../../../Contracts/DataModels"; import { NotebookMetadata } from "../Contracts/DataModels";
import { NotebookViewerComponent } from "./NotebookViewerComponent"; import { NotebookViewerComponent } from "../Explorer/Controls/NotebookViewer/NotebookViewerComponent";
import { SessionStorageUtility, StorageKey } from "../../../Shared/StorageUtility"; import { SessionStorageUtility, StorageKey } from "../Shared/StorageUtility";
const getNotebookUrl = (): string => { const getNotebookUrl = (): string => {
const regex: RegExp = new RegExp("[?&]notebookurl=([^&#]*)|&|#|$"); const regex: RegExp = new RegExp("[?&]notebookurl=([^&#]*)|&|#|$");
@ -26,12 +26,14 @@ const onInit = async () => {
SessionStorageUtility.removeEntry(StorageKey.NotebookName); SessionStorageUtility.removeEntry(StorageKey.NotebookName);
} }
const urlParams = new URLSearchParams(window.location.search);
const notebookViewerComponent = ( const notebookViewerComponent = (
<NotebookViewerComponent <NotebookViewerComponent
notebookMetadata={notebookMetadata} notebookMetadata={notebookMetadata}
notebookName={notebookName} notebookName={notebookName}
notebookUrl={getNotebookUrl()} notebookUrl={getNotebookUrl()}
container={null} hideInputs={urlParams.get("hideinputs") === "true"}
/> />
); );
ReactDOM.render(notebookViewerComponent, document.getElementById("notebookContent")); ReactDOM.render(notebookViewerComponent, document.getElementById("notebookContent"));

View File

@ -8,17 +8,22 @@ export class JunoUtils {
public static async getLikedNotebooks(authorizationToken: string): Promise<DataModels.LikedNotebooksJunoResponse> { public static async getLikedNotebooks(authorizationToken: string): Promise<DataModels.LikedNotebooksJunoResponse> {
//TODO: Add Get method once juno has it implemented //TODO: Add Get method once juno has it implemented
return { return {
likedNotebooksContent: await JunoUtils.getOfficialSampleNotebooks(), likedNotebooksContent: [],
userMetadata: { userMetadata: {
likedNotebooks: [] likedNotebooks: []
} }
}; };
} }
public static async getOfficialSampleNotebooks(): Promise<DataModels.GitHubInfoJunoResponse[]> { public static async getOfficialSampleNotebooks(
authorizationToken: string
): Promise<DataModels.GitHubInfoJunoResponse[]> {
try { try {
const response = await window.fetch(config.JUNO_ENDPOINT + "/api/galleries/notebooks", { const response = await window.fetch(config.JUNO_ENDPOINT + "/api/notebooks/galleries", {
method: "GET" method: "GET",
headers: {
authorization: authorizationToken
}
}); });
if (!response.ok) { if (!response.ok) {
throw new Error("Status code:" + response.status); throw new Error("Status code:" + response.status);
@ -35,14 +40,16 @@ export class JunoUtils {
): Promise<DataModels.UserMetadata> { ): Promise<DataModels.UserMetadata> {
return undefined; return undefined;
//TODO: add userMetadata updation code //TODO: add userMetadata updation code
// TODO: Make sure to throw error if failed
} }
public static async updateNotebookMetadata( public static async updateNotebookMetadata(
authorizationToken: string, authorizationToken: string,
userMetadata: DataModels.NotebookMetadata notebookMetadata: DataModels.NotebookMetadata
): Promise<DataModels.NotebookMetadata> { ): Promise<DataModels.NotebookMetadata> {
return undefined; return undefined;
//TODO: add notebookMetadata updation code //TODO: add notebookMetadata updation code
// TODO: Make sure to throw error if failed
} }
public static toPinnedRepo(item: RepoListItem): IPinnedRepo { public static toPinnedRepo(item: RepoListItem): IPinnedRepo {

View File

@ -144,7 +144,7 @@ module.exports = function(env = {}, argv = {}) {
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
filename: "notebookViewer.html", filename: "notebookViewer.html",
template: "src/Explorer/Controls/NotebookViewer/notebookViewer.html", template: "src/NotebookViewer/notebookViewer.html",
chunks: ["notebookViewer"] chunks: ["notebookViewer"]
}), }),
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
@ -175,7 +175,7 @@ module.exports = function(env = {}, argv = {}) {
hostedExplorer: "./src/HostedExplorer.ts", hostedExplorer: "./src/HostedExplorer.ts",
heatmap: "./src/Controls/Heatmap/Heatmap.ts", heatmap: "./src/Controls/Heatmap/Heatmap.ts",
terminal: "./src/Terminal/index.ts", terminal: "./src/Terminal/index.ts",
notebookViewer: "./src/Explorer/Controls/NotebookViewer/NotebookViewer.tsx", notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx",
galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx", galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx",
connectToGitHub: "./src/GitHub/GitHubConnector.ts" connectToGitHub: "./src/GitHub/GitHubConnector.ts"
}, },