From e9d3160b57fde3629ddb09760d9b70f485728954 Mon Sep 17 00:00:00 2001 From: Laurent Nguyen Date: Fri, 5 Jun 2020 04:04:15 +0200 Subject: [PATCH] Initial transfer from ADO (#13) --- .eslintignore | 2 +- package-lock.json | 209 ---------- package.json | 1 - src/Contracts/DataModels.ts | 2 + src/Contracts/ViewModels.ts | 9 +- .../Cards/CardStyleConstants.tsx | 19 +- .../Cards/GalleryCardComponent.test.tsx | 18 + .../Cards/GalleryCardComponent.tsx | 4 +- .../GalleryCardComponent.test.tsx.snap | 28 ++ .../GalleryViewerComponent.less | 9 + .../GalleryViewerComponent.test.tsx | 77 ++++ .../GalleryViewerComponent.tsx | 356 ++++++++++++++++++ .../GalleryViewerComponent.test.tsx.snap | 54 +++ .../NotebookMetadataComponent.less | 11 + .../NotebookMetadataComponent.test.tsx | 36 ++ .../NotebookMetadataComponent.tsx | 128 +++++-- .../NotebookViewerComponent.less | 4 +- .../NotebookViewerComponent.tsx | 21 +- .../NotebookMetadataComponent.test.tsx.snap | 25 ++ src/Explorer/Controls/Tabs/TabComponent.less | 47 +-- src/Explorer/Controls/Tabs/TabComponent.tsx | 5 +- src/Explorer/Explorer.ts | 13 +- .../NotebookReadOnlyRenderer.tsx | 9 +- src/Explorer/Tabs/GalleryTab.tsx | 2 +- src/Explorer/Tabs/NotebookViewerTab.tsx | 10 +- src/GalleryViewer/GalleryViewer.less | 9 - src/GalleryViewer/GalleryViewer.tsx | 6 +- src/GalleryViewer/GalleryViewerComponent.tsx | 207 ---------- src/Main.ts | 1 - .../NotebookViewer/NotebookViewer.less | 0 .../NotebookViewer/NotebookViewer.tsx | 10 +- .../NotebookViewer/notebookViewer.html | 0 src/Utils/JunoUtils.ts | 17 +- webpack.config.js | 4 +- 34 files changed, 827 insertions(+), 526 deletions(-) rename src/{GalleryViewer => Explorer/Controls/NotebookGallery}/Cards/CardStyleConstants.tsx (74%) create mode 100644 src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.test.tsx rename src/{GalleryViewer => Explorer/Controls/NotebookGallery}/Cards/GalleryCardComponent.tsx (95%) create mode 100644 src/Explorer/Controls/NotebookGallery/Cards/__snapshots__/GalleryCardComponent.test.tsx.snap create mode 100644 src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.less create mode 100644 src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.test.tsx create mode 100644 src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx create mode 100644 src/Explorer/Controls/NotebookGallery/__snapshots__/GalleryViewerComponent.test.tsx.snap create mode 100644 src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.less create mode 100644 src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx create mode 100644 src/Explorer/Controls/NotebookViewer/__snapshots__/NotebookMetadataComponent.test.tsx.snap delete mode 100644 src/GalleryViewer/GalleryViewer.less delete mode 100644 src/GalleryViewer/GalleryViewerComponent.tsx rename src/{Explorer/Controls => }/NotebookViewer/NotebookViewer.less (100%) rename src/{Explorer/Controls => }/NotebookViewer/NotebookViewer.tsx (77%) rename src/{Explorer/Controls => }/NotebookViewer/notebookViewer.html (100%) diff --git a/.eslintignore b/.eslintignore index 5003732c5..6a3311b92 100644 --- a/.eslintignore +++ b/.eslintignore @@ -343,7 +343,7 @@ src/Explorer/Controls/LibraryManagement/LibraryManageComponentAdapter.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.test.tsx src/Explorer/Controls/Notebook/NotebookTerminalComponent.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/QueriesGridReactComponent/QueriesGridComponent.tsx src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx diff --git a/package-lock.json b/package-lock.json index 2968a51c2..b49ef8b3f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1159,11 +1159,6 @@ "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": { "version": "0.8.8", "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": { "version": "2.5.4", "resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.4.tgz", @@ -3371,14 +3276,6 @@ "@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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz", @@ -5624,11 +5521,6 @@ "integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=", "dev": true }, - "clsx": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz", - "integrity": "sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA==" - }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -6193,15 +6085,6 @@ "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": { "version": "2.1.3", "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": { "version": "0.4.24", "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", "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -11333,83 +11206,6 @@ "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": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz", @@ -16795,11 +16591,6 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", "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": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz", diff --git a/package.json b/package.json index d0139e78c..e73134687 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,6 @@ "@azure/cosmos-language-service": "0.0.4", "@jupyterlab/services": "4.2.0", "@jupyterlab/terminal": "1.2.1", - "@material-ui/core": "4.9.10", "@microsoft/applicationinsights-web": "2.5.4", "@nteract/commutable": "7.1.4", "@nteract/connected-components": "6.7.8", diff --git a/src/Contracts/DataModels.ts b/src/Contracts/DataModels.ts index e9190b0ae..4b15a822b 100644 --- a/src/Contracts/DataModels.ts +++ b/src/Contracts/DataModels.ts @@ -738,6 +738,8 @@ export interface GitHubInfoJunoResponse { gitUrl: string; htmlUrl: string; metadata?: NotebookMetadata; + officialSamplesIndex?: number; + isLikedNotebook?: boolean; } export interface LikedNotebooksJunoResponse { diff --git a/src/Contracts/ViewModels.ts b/src/Contracts/ViewModels.ts index d5551500c..c532112e7 100644 --- a/src/Contracts/ViewModels.ts +++ b/src/Contracts/ViewModels.ts @@ -229,7 +229,12 @@ export interface Explorer { importAndOpenFromGallery: (path: string, newName: string, content: any) => Promise; openNotebookTerminal: (kind: TerminalKind) => void; openGallery: () => void; - openNotebookViewer: (notebookUrl: string, notebookMetadata: DataModels.NotebookMetadata) => void; + openNotebookViewer: ( + notebookUrl: string, + notebookMetadata: DataModels.NotebookMetadata, + onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise, + isLikedNotebook: boolean + ) => void; notebookWorkspaceManager: NotebookWorkspaceManager; sparkClusterManager: SparkClusterManager; notebookContentProvider: IContentProvider; @@ -887,6 +892,8 @@ export interface NotebookViewerTabOptions extends TabOptions { notebookUrl: string; notebookName: string; notebookMetadata: DataModels.NotebookMetadata; + onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise; + isLikedNotebook: boolean; } export interface DocumentsTabOptions extends TabOptions { diff --git a/src/GalleryViewer/Cards/CardStyleConstants.tsx b/src/Explorer/Controls/NotebookGallery/Cards/CardStyleConstants.tsx similarity index 74% rename from src/GalleryViewer/Cards/CardStyleConstants.tsx rename to src/Explorer/Controls/NotebookGallery/Cards/CardStyleConstants.tsx index a9faebc7a..7ac34dcf9 100644 --- a/src/GalleryViewer/Cards/CardStyleConstants.tsx +++ b/src/Explorer/Controls/NotebookGallery/Cards/CardStyleConstants.tsx @@ -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 = { root: { marginLeft: "10px", color: "#0078D4", - fontSize: 12, + backgroundColor: "#FFF", + fontSize: 16, fontWeight: FontWeights.regular, display: "inline-block" } diff --git a/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.test.tsx b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.test.tsx new file mode 100644 index 000000000..1c237fe0e --- /dev/null +++ b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.test.tsx @@ -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(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/GalleryViewer/Cards/GalleryCardComponent.tsx b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx similarity index 95% rename from src/GalleryViewer/Cards/GalleryCardComponent.tsx rename to src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx index 4dea3c057..0789dac7f 100644 --- a/src/GalleryViewer/Cards/GalleryCardComponent.tsx +++ b/src/Explorer/Controls/NotebookGallery/Cards/GalleryCardComponent.tsx @@ -1,5 +1,5 @@ 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 { Icon, Image, Persona, Text } from "office-ui-fabric-react"; import { @@ -10,7 +10,7 @@ import { subtleIconStyles } from "./CardStyleConstants"; -interface GalleryCardComponentProps { +export interface GalleryCardComponentProps { name: string; url: string; notebookMetadata: DataModels.NotebookMetadata; diff --git a/src/Explorer/Controls/NotebookGallery/Cards/__snapshots__/GalleryCardComponent.test.tsx.snap b/src/Explorer/Controls/NotebookGallery/Cards/__snapshots__/GalleryCardComponent.test.tsx.snap new file mode 100644 index 000000000..7ddd466c5 --- /dev/null +++ b/src/Explorer/Controls/NotebookGallery/Cards/__snapshots__/GalleryCardComponent.test.tsx.snap @@ -0,0 +1,28 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`GalleryCardComponent renders 1`] = ` + + + + mycard + + + +`; diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.less b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.less new file mode 100644 index 000000000..b0261f751 --- /dev/null +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.less @@ -0,0 +1,9 @@ +@import "../../../../less/Common/Constants"; + +.galleryContainer { + padding: @LargeSpace @LargeSpace 30px @LargeSpace; + height: 100%; + overflow-y: auto; + width: 100%; + font-family: @DataExplorerFont; +} diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.test.tsx b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.test.tsx new file mode 100644 index 000000000..cba6e24c3 --- /dev/null +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.test.tsx @@ -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, + isLikedNotebook: boolean + ) => Promise.resolve() + }; + + const wrapper = shallow(); + 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, + isLikedNotebook: boolean + ) => Promise.resolve() + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe("GalleryViewerContainerComponent", () => { + it("renders", () => { + const props: GalleryViewerContainerComponentProps = { + container: undefined + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); + +describe("GalleryCardComponent", () => { + it("renders", () => { + const props: GalleryViewerComponentProps = { + container: undefined, + officialSamplesData: [], + likedNotebookData: undefined + }; + + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx new file mode 100644 index 000000000..26200d476 --- /dev/null +++ b/src/Explorer/Controls/NotebookGallery/GalleryViewerComponent.tsx @@ -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; + onClick: ( + url: string, + notebookMetadata: DataModels.NotebookMetadata, + onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise, + isLikedNotebook: boolean + ) => Promise; +} + +export class GalleryCardsComponent extends React.Component { + private sectionStackTokens: IStackTokens = { childrenGap: 30 }; + + public render(): JSX.Element { + return ( + + {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 && ( + this.props.onClick(url, notebookMetadata, updateTabsStatePerNotebook, isLikedNotebook)} + /> + ) + ); + })} + + ); + } +} + +export interface FullWidthTabsProps { + officialSamplesContent: DataModels.GitHubInfoJunoResponse[]; + likedNotebooksContent: DataModels.GitHubInfoJunoResponse[]; + userMetadata: DataModels.UserMetadata; + onClick: ( + url: string, + notebookMetadata: DataModels.NotebookMetadata, + onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise, + isLikedNotebook: boolean + ) => Promise; +} + +interface FullWidthTabsState { + activeTabIndex: number; + officialSamplesContent: DataModels.GitHubInfoJunoResponse[]; + likedNotebooksContent: DataModels.GitHubInfoJunoResponse[]; + userMetadata: DataModels.UserMetadata; +} + +export class FullWidthTabs extends React.Component { + 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: () => ( + + ) + }, + isVisible: () => true + }, + { + title: "Liked Notebooks", + content: { + className: "", + render: () => ( + + ) + }, + 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 ( + + ); + } +} + +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 ? ( + + ) : ( + <> + ); + } +} + +export interface GalleryViewerComponentProps { + container: ViewModels.Explorer; + officialSamplesData: DataModels.GitHubInfoJunoResponse[]; + likedNotebookData: DataModels.LikedNotebooksJunoResponse; +} + +export class GalleryViewerComponent extends React.Component { + public render(): JSX.Element { + return this.props.container ? ( +
+ +
+ ) : ( +
+ +
+ ); + } + + 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, + 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); + } + }; +} diff --git a/src/Explorer/Controls/NotebookGallery/__snapshots__/GalleryViewerComponent.test.tsx.snap b/src/Explorer/Controls/NotebookGallery/__snapshots__/GalleryViewerComponent.test.tsx.snap new file mode 100644 index 000000000..d5e9a8123 --- /dev/null +++ b/src/Explorer/Controls/NotebookGallery/__snapshots__/GalleryViewerComponent.test.tsx.snap @@ -0,0 +1,54 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`FullWidthTabs renders 1`] = ` + +`; + +exports[`GalleryCardComponent renders 1`] = ` +
+ +
+`; + +exports[`GalleryCardsComponent renders 1`] = ` + +`; + +exports[`GalleryViewerContainerComponent renders 1`] = ``; diff --git a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.less b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.less new file mode 100644 index 000000000..24188abf6 --- /dev/null +++ b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.less @@ -0,0 +1,11 @@ +.notebookViewerMetadataContainer { + margin: 0px 10px; + + .title, .decoration, .persona { + display: inline-block; + } + + .extras { + margin-top: 5px; + } +} \ No newline at end of file diff --git a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx new file mode 100644 index 000000000..d01265ad4 --- /dev/null +++ b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.test.tsx @@ -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(); + 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(); + expect(wrapper).toMatchSnapshot(); + }); + + // TODO Add test for metadata display +}); diff --git a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx index ca7c8c718..04381750d 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx @@ -6,47 +6,97 @@ import * as React from "react"; import * as ViewModels from "../../../Contracts/ViewModels"; import { NotebookMetadata } from "../../../Contracts/DataModels"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; -import { Icon, Persona, Text } from "office-ui-fabric-react"; -import CSS from "csstype"; +import { Icon, Persona, Text, IconButton } from "office-ui-fabric-react"; import { siteTextStyles, subtleIconStyles, iconStyles, + iconButtonStyles, mainHelpfulTextStyles, subtleHelpfulTextStyles, helpfulTextStyles -} from "../../../GalleryViewer/Cards/CardStyleConstants"; +} from "../NotebookGallery/Cards/CardStyleConstants"; + +import "./NotebookViewerComponent.less"; initializeIcons(); -interface NotebookMetadataComponentProps { +export interface NotebookMetadataComponentProps { notebookName: string; container: ViewModels.Explorer; notebookMetadata: NotebookMetadata; notebookContent: any; + onNotebookMetadataChange: (newNotebookMetadata: NotebookMetadata) => Promise; + isLikedNotebook: boolean; } -export class NotebookMetadataComponent extends React.Component { - private inlineBlockStyle: CSS.Properties = { - display: "inline-block" +interface NotebookMetadatComponentState { + liked: boolean; + 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 = { - marginTop: "5px" + componentDidMount() { + 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) => { - this.props.container.importAndOpenFromGallery( - this.props.notebookName, - newNotebookName, - JSON.stringify(this.props.notebookContent) - ); - }; - - public render(): JSX.Element { + private onDownload = (): void => { const promptForNotebookName = () => { return new Promise((resolve, reject) => { - var newNotebookName = this.props.notebookName; + let newNotebookName = this.props.notebookName; this.props.container.showOkCancelTextFieldModalDialog( "Save notebook as", undefined, @@ -68,27 +118,35 @@ export class NotebookMetadataComponent extends React.Component { + this.onDownloadClick(newNotebookName); + }); + }; + + public render(): JSX.Element { return (
-

{this.props.notebookName}

+

{this.props.notebookName}

{this.props.notebookMetadata && ( -
- - - {this.props.notebookMetadata.likes} likes +
+ {this.props.container ? ( + + ) : ( + + )} + + {this.state.notebookMetadata.likes} likes
)} {this.props.container && ( - )} @@ -97,20 +155,20 @@ export class NotebookMetadataComponent extends React.Component
-
+
- {this.props.notebookMetadata.views} + {this.state.notebookMetadata.views} - {this.props.notebookMetadata.downloads} + {this.state.notebookMetadata.downloads}
diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.less b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.less index b7406b589..a99718f94 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.less +++ b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.less @@ -4,7 +4,7 @@ padding: @DefaultSpace; height: 100%; width: 100%; - overflow-y: scroll; + overflow-y: auto; } .downloadButton { @@ -20,7 +20,7 @@ display: "inline-block"; margin: 10px; } - + .active, .downloadButton:hover { color: @BaseMedium; } diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx index be729623a..7dba59508 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx +++ b/src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx @@ -16,12 +16,14 @@ import "./NotebookViewerComponent.less"; export interface NotebookViewerComponentProps { notebookName: string; notebookUrl: string; - container: ViewModels.Explorer; + container?: ViewModels.Explorer; notebookMetadata: NotebookMetadata; + onNotebookMetadataChange?: (newNotebookMetadata: NotebookMetadata) => Promise; + isLikedNotebook?: boolean; + hideInputs?: boolean; } interface NotebookViewerComponentState { - element: JSX.Element; content: any; } @@ -50,7 +52,7 @@ export class NotebookViewerComponent extends React.Component< contentRef: createContentRef() }); - this.state = { element: undefined, content: undefined }; + this.state = { content: undefined }; } private async getJsonNotebookContent(): Promise { @@ -65,24 +67,25 @@ export class NotebookViewerComponent extends React.Component< componentDidMount() { this.getJsonNotebookContent().then((jsonContent: any) => { this.notebookComponentBootstrapper.setContent("json", jsonContent); - const notebookReadonlyComponent = this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer); - this.setState({ element: notebookReadonlyComponent, content: jsonContent }); + this.setState({ content: jsonContent }); }); } public render(): JSX.Element { - return this.state != null ? ( + return (
- {this.state.element} + {this.notebookComponentBootstrapper.renderComponent(NotebookReadOnlyRenderer, { + hideInputs: this.props.hideInputs + })}
- ) : ( - <> ); } } diff --git a/src/Explorer/Controls/NotebookViewer/__snapshots__/NotebookMetadataComponent.test.tsx.snap b/src/Explorer/Controls/NotebookViewer/__snapshots__/NotebookMetadataComponent.test.tsx.snap new file mode 100644 index 000000000..b0b6c06f9 --- /dev/null +++ b/src/Explorer/Controls/NotebookViewer/__snapshots__/NotebookMetadataComponent.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NotebookMetadataComponent renders liked notebook 1`] = ` +
+

+ My notebook +

+
+`; + +exports[`NotebookMetadataComponent renders un-liked notebook 1`] = ` +
+

+ My notebook +

+
+`; diff --git a/src/Explorer/Controls/Tabs/TabComponent.less b/src/Explorer/Controls/Tabs/TabComponent.less index d4ed78c0c..4de5d8974 100644 --- a/src/Explorer/Controls/Tabs/TabComponent.less +++ b/src/Explorer/Controls/Tabs/TabComponent.less @@ -1,27 +1,28 @@ @import "../../../../less/Common/Constants"; -.tabSwitch { - margin-left: @LargeSpace; - margin-bottom: 20px; - - .tab { - margin-right: @MediumSpace; - } - - .toggleSwitch { - .toggleSwitch(); - } - - .selectedToggle { - .selectedToggle(); - } - - .unselectedToggle { - .unselectedToggle(); - } -} - -.tabComponentContent { - height: calc(100% - 20px); +.tabComponentContainer { + height: 100%; .flex-display(); + .flex-direction(); + + .tabSwitch { + margin-left: @LargeSpace; + margin-bottom: 20px; + + .tab { + margin-right: @MediumSpace; + } + + .toggleSwitch { + .toggleSwitch(); + } + + .selectedToggle { + .selectedToggle(); + } + + .unselectedToggle { + .unselectedToggle(); + } + } } diff --git a/src/Explorer/Controls/Tabs/TabComponent.tsx b/src/Explorer/Controls/Tabs/TabComponent.tsx index 4c7fe4878..6e1bf5d09 100644 --- a/src/Explorer/Controls/Tabs/TabComponent.tsx +++ b/src/Explorer/Controls/Tabs/TabComponent.tsx @@ -1,5 +1,6 @@ import * as React from "react"; import { AccessibleElement } from "../../Controls/AccessibleElement/AccessibleElement"; +import "./TabComponent.less"; export interface TabContent { render: () => JSX.Element; @@ -75,10 +76,10 @@ export class TabComponent extends React.Component { } return ( - +
{!this.props.hideHeader &&
{this.renderTabTitles()}
}
{currentTabContent.render()}
- +
); } } diff --git a/src/Explorer/Explorer.ts b/src/Explorer/Explorer.ts index d9523f8e4..781c063b7 100644 --- a/src/Explorer/Explorer.ts +++ b/src/Explorer/Explorer.ts @@ -1143,7 +1143,7 @@ export default class Explorer implements ViewModels.Explorer { isModal: true, visible: true, 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`, primaryButtonText: "Enable Azure Synapse Link", secondaryButtonText: "Cancel", @@ -3278,7 +3278,12 @@ export default class Explorer implements ViewModels.Explorer { newTab.onTabClick(); } - public openNotebookViewer(notebookUrl: string, notebookMetadata: DataModels.NotebookMetadata) { + public openNotebookViewer( + notebookUrl: string, + notebookMetadata: DataModels.NotebookMetadata, + onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise, + isLikedNotebook: boolean + ) { const notebookName = path.basename(notebookUrl); const title = notebookName; const hashLocation = notebookUrl; @@ -3320,7 +3325,9 @@ export default class Explorer implements ViewModels.Explorer { openedTabs: this.openedTabs(), notebookUrl: notebookUrl, notebookName: notebookName, - notebookMetadata: notebookMetadata + notebookMetadata: notebookMetadata, + onNotebookMetadataChange: onNotebookMetadataChange, + isLikedNotebook: isLikedNotebook }); this.openedTabs.push(newTab); diff --git a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx index a96912a36..de2354181 100644 --- a/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx +++ b/src/Explorer/Notebook/NotebookRenderer/NotebookReadOnlyRenderer.tsx @@ -9,11 +9,12 @@ import { connect } from "react-redux"; import { Dispatch } from "redux"; import { actions, ContentRef } from "@nteract/core"; import loadTransform from "../NotebookComponent/loadTransform"; -import CodeMirrorEditor from "@nteract/editor"; +import CodeMirrorEditor from "@nteract/stateful-components/lib/inputs/connected-editors/codemirror"; import "./NotebookReadOnlyRenderer.less"; export interface NotebookRendererProps { contentRef: any; + hideInputs?: boolean; } interface PassedEditorProps { @@ -46,7 +47,8 @@ class NotebookReadOnlyRenderer extends React.Component { {{ editor: { - codemirror: (props: PassedEditorProps) => + codemirror: (props: PassedEditorProps) => + this.props.hideInputs ? <> : }, prompt: ({ id, contentRef }) => <> }} @@ -63,7 +65,8 @@ class NotebookReadOnlyRenderer extends React.Component { {{ editor: { - codemirror: (props: PassedEditorProps) => + codemirror: (props: PassedEditorProps) => + this.props.hideInputs ? <> : } }} diff --git a/src/Explorer/Tabs/GalleryTab.tsx b/src/Explorer/Tabs/GalleryTab.tsx index e85bd9833..856399b8b 100644 --- a/src/Explorer/Tabs/GalleryTab.tsx +++ b/src/Explorer/Tabs/GalleryTab.tsx @@ -3,7 +3,7 @@ import * as ViewModels from "../../Contracts/ViewModels"; import TabsBase from "./TabsBase"; import * as React from "react"; import { ReactAdapter } from "../../Bindings/ReactBindingHandler"; -import { GalleryViewerContainerComponent } from "../../GalleryViewer/GalleryViewerComponent"; +import { GalleryViewerContainerComponent } from "../Controls/NotebookGallery/GalleryViewerComponent"; /** * Notebook gallery tab diff --git a/src/Explorer/Tabs/NotebookViewerTab.tsx b/src/Explorer/Tabs/NotebookViewerTab.tsx index 7436a9b17..df2dffe0f 100644 --- a/src/Explorer/Tabs/NotebookViewerTab.tsx +++ b/src/Explorer/Tabs/NotebookViewerTab.tsx @@ -16,7 +16,9 @@ class NotebookViewerComponentAdapter implements ReactAdapter { private notebookUrl: string, private notebookName: string, private container: ViewModels.Explorer, - private notebookMetadata: DataModels.NotebookMetadata + private notebookMetadata: DataModels.NotebookMetadata, + private onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise, + private isLikedNotebook: boolean ) {} public renderComponent(): JSX.Element { @@ -26,6 +28,8 @@ class NotebookViewerComponentAdapter implements ReactAdapter { notebookMetadata={this.notebookMetadata} notebookName={this.notebookName} 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.notebookName, options.container, - options.notebookMetadata + options.notebookMetadata, + options.onNotebookMetadataChange, + options.isLikedNotebook ); this.notebookViewerComponentAdapter.parameters = ko.computed(() => { diff --git a/src/GalleryViewer/GalleryViewer.less b/src/GalleryViewer/GalleryViewer.less deleted file mode 100644 index 22895826e..000000000 --- a/src/GalleryViewer/GalleryViewer.less +++ /dev/null @@ -1,9 +0,0 @@ -@import "../../less/Common/Constants"; - -.galleryContainer { - padding: @DefaultSpace; - height: 100%; - overflow-y: scroll; - width: 100%; - font-family: @DataExplorerFont; -} diff --git a/src/GalleryViewer/GalleryViewer.tsx b/src/GalleryViewer/GalleryViewer.tsx index d464c3e78..dad7f937a 100644 --- a/src/GalleryViewer/GalleryViewer.tsx +++ b/src/GalleryViewer/GalleryViewer.tsx @@ -1,13 +1,13 @@ import * as ReactDOM from "react-dom"; import "bootstrap/dist/css/bootstrap.css"; -import "./GalleryViewer.less"; -import { GalleryViewerComponent } from "./GalleryViewerComponent"; +import { CosmosClient } from "../Common/CosmosClient"; +import { GalleryViewerComponent } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent"; import { JunoUtils } from "../Utils/JunoUtils"; import { initializeIcons } from "office-ui-fabric-react/lib/Icons"; const onInit = async () => { initializeIcons(); - const officialSamplesData = await JunoUtils.getOfficialSampleNotebooks(); + const officialSamplesData = await JunoUtils.getOfficialSampleNotebooks(CosmosClient.authorizationToken()); const galleryViewerComponent = new GalleryViewerComponent({ officialSamplesData: officialSamplesData, likedNotebookData: undefined, diff --git a/src/GalleryViewer/GalleryViewerComponent.tsx b/src/GalleryViewer/GalleryViewerComponent.tsx deleted file mode 100644 index 0222f80ba..000000000 --- a/src/GalleryViewer/GalleryViewerComponent.tsx +++ /dev/null @@ -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; -} - -class GalleryCardsComponent extends React.Component { - private sectionStackTokens: IStackTokens = { childrenGap: 30 }; - public render(): JSX.Element { - return ( - - {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 && ( - this.props.onClick(url, notebookMetadata)} - /> - ) - ); - })} - - ); - } -} - -const TabPanel = (props: any) => ( - -); - -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; -} - -const FullWidthTabs = (props: FullWidthTabsProps) => { - const [value, setValue] = React.useState(0); - - const handleChange = ({}, newValue: any) => { - setValue(newValue); - }; - - return ( - <> - - - - - - - - - - - - - - ); -}; - -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 ? ( - - ) : ( - <> - ); - } -} - -export interface GalleryViewerComponentProps { - container: ViewModels.Explorer; - officialSamplesData: DataModels.GitHubInfoJunoResponse[]; - likedNotebookData: DataModels.LikedNotebooksJunoResponse; -} - -export class GalleryViewerComponent extends React.Component { - private authorizationToken = CosmosClient.authorizationToken(); - - public render(): JSX.Element { - return this.props.container ? ( -
- -
- ) : ( -
- -
- ); - } - - 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); - } - }; -} diff --git a/src/Main.ts b/src/Main.ts index a4935dbcf..a7a4f2289 100644 --- a/src/Main.ts +++ b/src/Main.ts @@ -15,7 +15,6 @@ import "./Explorer/Menus/CommandBar/MemoryTrackerComponent.less"; import "./Explorer/Controls/CollapsiblePanel/CollapsiblePanelComponent.less"; import "./Explorer/Controls/DynamicList/DynamicListComponent.less"; import "./Explorer/Controls/JsonEditor/JsonEditorComponent.less"; -import "./Explorer/Controls/Tabs/TabComponent.less"; import "./Explorer/Graph/GraphExplorerComponent/graphExplorer.less"; import "../less/TableStyles/queryBuilder.less"; import "../externals/jquery.dataTables.min.css"; diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewer.less b/src/NotebookViewer/NotebookViewer.less similarity index 100% rename from src/Explorer/Controls/NotebookViewer/NotebookViewer.less rename to src/NotebookViewer/NotebookViewer.less diff --git a/src/Explorer/Controls/NotebookViewer/NotebookViewer.tsx b/src/NotebookViewer/NotebookViewer.tsx similarity index 77% rename from src/Explorer/Controls/NotebookViewer/NotebookViewer.tsx rename to src/NotebookViewer/NotebookViewer.tsx index 26f78031b..7fbcbdddd 100644 --- a/src/Explorer/Controls/NotebookViewer/NotebookViewer.tsx +++ b/src/NotebookViewer/NotebookViewer.tsx @@ -1,9 +1,9 @@ import React from "react"; import * as ReactDOM from "react-dom"; import "bootstrap/dist/css/bootstrap.css"; -import { NotebookMetadata } from "../../../Contracts/DataModels"; -import { NotebookViewerComponent } from "./NotebookViewerComponent"; -import { SessionStorageUtility, StorageKey } from "../../../Shared/StorageUtility"; +import { NotebookMetadata } from "../Contracts/DataModels"; +import { NotebookViewerComponent } from "../Explorer/Controls/NotebookViewer/NotebookViewerComponent"; +import { SessionStorageUtility, StorageKey } from "../Shared/StorageUtility"; const getNotebookUrl = (): string => { const regex: RegExp = new RegExp("[?&]notebookurl=([^&#]*)|&|#|$"); @@ -26,12 +26,14 @@ const onInit = async () => { SessionStorageUtility.removeEntry(StorageKey.NotebookName); } + const urlParams = new URLSearchParams(window.location.search); + const notebookViewerComponent = ( ); ReactDOM.render(notebookViewerComponent, document.getElementById("notebookContent")); diff --git a/src/Explorer/Controls/NotebookViewer/notebookViewer.html b/src/NotebookViewer/notebookViewer.html similarity index 100% rename from src/Explorer/Controls/NotebookViewer/notebookViewer.html rename to src/NotebookViewer/notebookViewer.html diff --git a/src/Utils/JunoUtils.ts b/src/Utils/JunoUtils.ts index 761c4661b..1f95f3e60 100644 --- a/src/Utils/JunoUtils.ts +++ b/src/Utils/JunoUtils.ts @@ -8,17 +8,22 @@ export class JunoUtils { public static async getLikedNotebooks(authorizationToken: string): Promise { //TODO: Add Get method once juno has it implemented return { - likedNotebooksContent: await JunoUtils.getOfficialSampleNotebooks(), + likedNotebooksContent: [], userMetadata: { likedNotebooks: [] } }; } - public static async getOfficialSampleNotebooks(): Promise { + public static async getOfficialSampleNotebooks( + authorizationToken: string + ): Promise { try { - const response = await window.fetch(config.JUNO_ENDPOINT + "/api/galleries/notebooks", { - method: "GET" + const response = await window.fetch(config.JUNO_ENDPOINT + "/api/notebooks/galleries", { + method: "GET", + headers: { + authorization: authorizationToken + } }); if (!response.ok) { throw new Error("Status code:" + response.status); @@ -35,14 +40,16 @@ export class JunoUtils { ): Promise { return undefined; //TODO: add userMetadata updation code + // TODO: Make sure to throw error if failed } public static async updateNotebookMetadata( authorizationToken: string, - userMetadata: DataModels.NotebookMetadata + notebookMetadata: DataModels.NotebookMetadata ): Promise { return undefined; //TODO: add notebookMetadata updation code + // TODO: Make sure to throw error if failed } public static toPinnedRepo(item: RepoListItem): IPinnedRepo { diff --git a/webpack.config.js b/webpack.config.js index 037a67fea..adb76c869 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -144,7 +144,7 @@ module.exports = function(env = {}, argv = {}) { }), new HtmlWebpackPlugin({ filename: "notebookViewer.html", - template: "src/Explorer/Controls/NotebookViewer/notebookViewer.html", + template: "src/NotebookViewer/notebookViewer.html", chunks: ["notebookViewer"] }), new HtmlWebpackPlugin({ @@ -175,7 +175,7 @@ module.exports = function(env = {}, argv = {}) { hostedExplorer: "./src/HostedExplorer.ts", heatmap: "./src/Controls/Heatmap/Heatmap.ts", terminal: "./src/Terminal/index.ts", - notebookViewer: "./src/Explorer/Controls/NotebookViewer/NotebookViewer.tsx", + notebookViewer: "./src/NotebookViewer/NotebookViewer.tsx", galleryViewer: "./src/GalleryViewer/GalleryViewer.tsx", connectToGitHub: "./src/GitHub/GitHubConnector.ts" },