Compare commits

...

7 Commits

Author SHA1 Message Date
Steve Faulkner
3951f01422 More robust fix 2020-06-09 16:29:41 -05:00
Steve Faulkner
b62cd98f67 Merge branch 'firefox-emulator-fix' into fix-emulator-upload 2020-06-09 15:15:10 -05:00
Steve Faulkner
0240eac920 Fix emulator upload by passing down config.platform 2020-06-09 14:51:30 -05:00
Steve Faulkner
5fb2fe2798 Update to latest JS SDK. Fixes Firefox emulator bug. Close #12 2020-06-08 12:25:10 -05:00
Tanuj Mittal
aa8236666e Use graphql in GitHubClient and misc fixes (#8)
* Use graphql in GitHubClient

* Replace usage of Array.find with _.find
2020-06-05 12:22:41 -07:00
Laurent Nguyen
e9d3160b57 Initial transfer from ADO (#13) 2020-06-04 19:04:15 -07:00
Tanuj Mittal
ab3486bd05 Add telemetry to track execute cell from <Prompt/> (#15) 2020-06-02 10:46:12 -07:00
60 changed files with 1667 additions and 1205 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

324
package-lock.json generated
View File

@@ -5,9 +5,9 @@
"requires": true, "requires": true,
"dependencies": { "dependencies": {
"@azure/cosmos": { "@azure/cosmos": {
"version": "3.6.3", "version": "3.7.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.6.3.tgz", "resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.7.0.tgz",
"integrity": "sha512-JoCDxl0TnL6EHL4xD3KC9r2bMivK13q1jl7h69wd/YFLlt3aBTTCehtAX+y4alNSENpL53XdRdw/cna0mI2XDw==", "integrity": "sha512-3SRxnmy6NncdX5eYqGuRTack52hloS9YhQ0aOKwWJ8Z4dDSrVH3XB2Mcp/WokoIpVm0Bq5nUC8FsvLBZKfRkyg==",
"requires": { "requires": {
"@types/debug": "^4.1.4", "@types/debug": "^4.1.4",
"debug": "^4.1.1", "debug": "^4.1.1",
@@ -18,7 +18,14 @@
"priorityqueuejs": "^1.0.0", "priorityqueuejs": "^1.0.0",
"semaphore": "^1.0.5", "semaphore": "^1.0.5",
"tslib": "^1.10.0", "tslib": "^1.10.0",
"uuid": "^3.3.2" "uuid": "^8.1.0"
},
"dependencies": {
"uuid": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz",
"integrity": "sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg=="
}
} }
}, },
"@azure/cosmos-language-service": { "@azure/cosmos-language-service": {
@@ -1159,11 +1166,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 +1606,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",
@@ -2517,32 +2429,42 @@
} }
}, },
"@octokit/auth-token": { "@octokit/auth-token": {
"version": "2.4.0", "version": "2.4.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.1.tgz",
"integrity": "sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==", "integrity": "sha512-NB81O5h39KfHYGtgfWr2booRxp2bWOJoqbWwbyUg2hw6h35ArWYlAST5B3XwAkbdcx13yt84hFXyFP5X0QToWA==",
"requires": { "requires": {
"@octokit/types": "^2.0.0" "@octokit/types": "^4.0.1"
},
"dependencies": {
"@octokit/types": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-4.0.2.tgz",
"integrity": "sha512-+4X6qfhT/fk/5FD66395NrFLxCzD6FsGlpPwfwvnukdyfYbhiZB/FJltiT1XM5Q63rGGBSf9FPaNV3WpNHm54A==",
"requires": {
"@types/node": ">= 8"
}
}
} }
}, },
"@octokit/core": { "@octokit/core": {
"version": "2.5.0", "version": "2.5.3",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-2.5.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/core/-/core-2.5.3.tgz",
"integrity": "sha512-uvzmkemQrBgD8xuGbjhxzJN1darJk9L2cS+M99cHrDG2jlSVpxNJVhoV86cXdYBqdHCc9Z995uLCczaaHIYA6Q==", "integrity": "sha512-23AHK9xBW0v79Ck8h5U+5iA4MW7aosqv+Yr6uZXolVGNzzHwryNH5wM386/6+etiKUTwLFZTqyMU9oQpIBZcFA==",
"requires": { "requires": {
"@octokit/auth-token": "^2.4.0", "@octokit/auth-token": "^2.4.0",
"@octokit/graphql": "^4.3.1", "@octokit/graphql": "^4.3.1",
"@octokit/request": "^5.4.0", "@octokit/request": "^5.4.0",
"@octokit/types": "^2.0.0", "@octokit/types": "^4.0.1",
"before-after-hook": "^2.1.0", "before-after-hook": "^2.1.0",
"universal-user-agent": "^5.0.0" "universal-user-agent": "^5.0.0"
} }
}, },
"@octokit/endpoint": { "@octokit/endpoint": {
"version": "6.0.1", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.2.tgz",
"integrity": "sha512-pOPHaSz57SFT/m3R5P8MUu4wLPszokn5pXcB/pzavLTQf2jbU+6iayTvzaY6/BiotuRS0qyEUkx3QglT4U958A==", "integrity": "sha512-xs1mmCEZ2y4shXCpFjNq3UbmNR+bLzxtZim2L0zfEtj9R6O6kc4qLDvYw66hvO6lUsYzPTM5hMkltbuNAbRAcQ==",
"requires": { "requires": {
"@octokit/types": "^2.11.1", "@octokit/types": "^4.0.1",
"is-plain-object": "^3.0.0", "is-plain-object": "^3.0.0",
"universal-user-agent": "^5.0.0" "universal-user-agent": "^5.0.0"
}, },
@@ -2563,21 +2485,21 @@
} }
}, },
"@octokit/graphql": { "@octokit/graphql": {
"version": "4.4.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.4.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.0.tgz",
"integrity": "sha512-Du3hAaSROQ8EatmYoSAJjzAz3t79t9Opj/WY1zUgxVUGfIKn0AEjg+hlOLscF6fv6i/4y/CeUvsWgIfwMkTccw==", "integrity": "sha512-StJWfn0M1QfhL3NKBz31e1TdDNZrHLLS57J2hin92SIfzlOVBuUaRkp31AGkGOAFOAVtyEX6ZiZcsjcJDjeb5g==",
"requires": { "requires": {
"@octokit/request": "^5.3.0", "@octokit/request": "^5.3.0",
"@octokit/types": "^2.0.0", "@octokit/types": "^4.0.1",
"universal-user-agent": "^5.0.0" "universal-user-agent": "^5.0.0"
} }
}, },
"@octokit/plugin-paginate-rest": { "@octokit/plugin-paginate-rest": {
"version": "2.2.0", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.1.tgz",
"integrity": "sha512-KoNxC3PLNar8UJwR+1VMQOw2IoOrrFdo5YOiDKnBhpVbKpw+zkBKNMNKwM44UWL25Vkn0Sl3nYIEGKY+gW5ebw==", "integrity": "sha512-/tHpIF2XpN40AyhIq295YRjb4g7Q5eKob0qM3thYJ0Z+CgmNsWKM/fWse/SUR8+LdprP1O4ZzSKQE+71TCwK+w==",
"requires": { "requires": {
"@octokit/types": "^2.12.1" "@octokit/types": "^4.0.1"
} }
}, },
"@octokit/plugin-request-log": { "@octokit/plugin-request-log": {
@@ -2586,22 +2508,22 @@
"integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw==" "integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw=="
}, },
"@octokit/plugin-rest-endpoint-methods": { "@octokit/plugin-rest-endpoint-methods": {
"version": "3.7.1", "version": "3.12.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-3.7.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-3.12.3.tgz",
"integrity": "sha512-YOlcE3bbk2ohaOVdRj9ww7AUYfmnS9hwJJGSj3/rFlNfMGOId4G8dLlhghXpdNSn05H0FRoI94UlFUKnn30Cyw==", "integrity": "sha512-9nrVDP1tBd7EtobGr5hZcYGTM0kBNmIvPJazrUd5OJO0NZWiQaQOqAnzApmC9cZ4o7RempV21ScpWkKGhrT51A==",
"requires": { "requires": {
"@octokit/types": "^2.11.1", "@octokit/types": "^4.0.0",
"deprecation": "^2.3.1" "deprecation": "^2.3.1"
} }
}, },
"@octokit/request": { "@octokit/request": {
"version": "5.4.2", "version": "5.4.4",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.4.tgz",
"integrity": "sha512-zKdnGuQ2TQ2vFk9VU8awFT4+EYf92Z/v3OlzRaSh4RIP0H6cvW1BFPXq4XYvNez+TPQjqN+0uSkCYnMFFhcFrw==", "integrity": "sha512-vqv1lz41c6VTxUvF9nM+a6U+vvP3vGk7drDpr0DVQg4zyqlOiKVrY17DLD6de5okj+YLHKcoqaUZTBtlNZ1BtQ==",
"requires": { "requires": {
"@octokit/endpoint": "^6.0.1", "@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.0.0", "@octokit/request-error": "^2.0.0",
"@octokit/types": "^2.11.1", "@octokit/types": "^4.0.1",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
"is-plain-object": "^3.0.0", "is-plain-object": "^3.0.0",
"node-fetch": "^2.3.0", "node-fetch": "^2.3.0",
@@ -2625,39 +2547,32 @@
} }
}, },
"@octokit/request-error": { "@octokit/request-error": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.0.tgz", "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.1.tgz",
"integrity": "sha512-rtYicB4Absc60rUv74Rjpzek84UbVHGHJRu4fNVlZ1mCcyUPPuzFfG9Rn6sjHrd95DEsmjSt1Axlc699ZlbDkw==", "integrity": "sha512-5lqBDJ9/TOehK82VvomQ6zFiZjPeSom8fLkFVLuYL3sKiIb5RB8iN/lenLkY7oBmyQcGP7FBMGiIZTO8jufaRQ==",
"requires": { "requires": {
"@octokit/types": "^2.0.0", "@octokit/types": "^4.0.1",
"deprecation": "^2.0.0", "deprecation": "^2.0.0",
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"@octokit/rest": { "@octokit/rest": {
"version": "17.5.1", "version": "17.9.2",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-17.5.1.tgz", "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-17.9.2.tgz",
"integrity": "sha512-0rGY7eo0cw8FYX7jAtUgfy3j+05zhs9JvkPFegx00HAaayodM1ixlHhCOB5yirGbsVOxbRIWVkvKc2yY9367gg==", "integrity": "sha512-UXxiE0HhGQAPB3WDHTEu7lYMHH2uRcs/9f26XyHpGGiiXht8hgHWEk6fA7WglwwEvnj8V7mkJOgIntnij132UA==",
"requires": { "requires": {
"@octokit/core": "^2.4.3", "@octokit/core": "^2.4.3",
"@octokit/plugin-paginate-rest": "^2.1.0", "@octokit/plugin-paginate-rest": "^2.2.0",
"@octokit/plugin-request-log": "^1.0.0", "@octokit/plugin-request-log": "^1.0.0",
"@octokit/plugin-rest-endpoint-methods": "3.7.1" "@octokit/plugin-rest-endpoint-methods": "^3.12.2"
} }
}, },
"@octokit/types": { "@octokit/types": {
"version": "2.16.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz", "resolved": "https://registry.npmjs.org/@octokit/types/-/types-4.0.2.tgz",
"integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==", "integrity": "sha512-+4X6qfhT/fk/5FD66395NrFLxCzD6FsGlpPwfwvnukdyfYbhiZB/FJltiT1XM5Q63rGGBSf9FPaNV3WpNHm54A==",
"requires": { "requires": {
"@types/node": ">= 8" "@types/node": ">= 8"
},
"dependencies": {
"@types/node": {
"version": "14.0.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.1.tgz",
"integrity": "sha512-FAYBGwC+W6F9+huFIDtn43cpy7+SzG+atzRiTfdp3inUKL2hXnd4rG8hylJLIh4+hqrQy1P17kvJByE/z825hA=="
}
} }
}, },
"@peculiar/asn1-schema": { "@peculiar/asn1-schema": {
@@ -3290,8 +3205,7 @@
"@types/node": { "@types/node": {
"version": "12.11.1", "version": "12.11.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz",
"integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==", "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A=="
"dev": true
}, },
"@types/promise.prototype.finally": { "@types/promise.prototype.finally": {
"version": "2.0.3", "version": "2.0.3",
@@ -3371,14 +3285,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 +5530,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 +6094,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 +9807,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 +10346,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 +11215,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 +16600,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

@@ -4,11 +4,10 @@
"description": "Cosmos Explorer", "description": "Cosmos Explorer",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@azure/cosmos": "3.6.3", "@azure/cosmos": "3.7.0",
"@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",
@@ -33,7 +32,7 @@
"@nteract/transform-plotly": "6.1.6", "@nteract/transform-plotly": "6.1.6",
"@nteract/transform-vdom": "4.0.11", "@nteract/transform-vdom": "4.0.11",
"@nteract/transform-vega": "7.0.6", "@nteract/transform-vega": "7.0.6",
"@octokit/rest": "17.5.1", "@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3", "@phosphor/widgets": "1.9.3",
"@uifabric/react-cards": "0.109.53", "@uifabric/react-cards": "0.109.53",
"@uifabric/styling": "7.11.2", "@uifabric/styling": "7.11.2",

View File

@@ -21,13 +21,15 @@ const _global = typeof self === "undefined" ? window : self;
export const tokenProvider = async (requestInfo: RequestInfo) => { export const tokenProvider = async (requestInfo: RequestInfo) => {
const { verb, resourceId, resourceType, headers } = requestInfo; const { verb, resourceId, resourceType, headers } = requestInfo;
if (config.platform === Platform.Emulator) { if (config.platform === Platform.Emulator) {
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey) as any; await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
} }
if (_masterKey) { if (_masterKey) {
// TODO Remove any. SDK expects a return value for tokenProvider, but we are mutating the header object instead. // TODO This SDK method mutates the headers object. Find a better one or fix the SDK.
return setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, _masterKey) as any; await setAuthorizationTokenHeaderUsingMasterKey(verb, resourceId, resourceType, headers, EmulatorMasterKey);
return decodeURIComponent(headers.authorization);
} }
if (_resourceToken) { if (_resourceToken) {
@@ -47,7 +49,9 @@ export const requestPlugin: Cosmos.Plugin<any> = async (requestContext, next) =>
export const endpoint = () => { export const endpoint = () => {
if (config.platform === Platform.Emulator) { if (config.platform === Platform.Emulator) {
return config.EMULATOR_ENDPOINT || window.parent.location.origin; // In worker scope, _global(self).parent does not exist
const location = _global.parent ? _global.parent.location : _global.location;
return config.EMULATOR_ENDPOINT || location.origin;
} }
return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint); return _endpoint || (_databaseAccount && _databaseAccount.properties && _databaseAccount.properties.documentEndpoint);
}; };

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

@@ -54,7 +54,8 @@ export class DialogComponent extends React.Component<DialogProps, {}> {
styles: { styles: {
title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT }, title: { fontSize: DIALOG_TITLE_FONT_SIZE, fontWeight: DIALOG_TITLE_FONT_WEIGHT },
subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE } subText: { fontSize: DIALOG_SUBTEXT_FONT_SIZE }
} },
showCloseButton: false
}, },
modalProps: { isBlocking: this.props.isModal }, modalProps: { isBlocking: this.props.isModal },
minWidth: DIALOG_MIN_WIDTH, minWidth: DIALOG_MIN_WIDTH,

View File

@@ -2,6 +2,7 @@
* React component for Switch Directory * React component for Switch Directory
*/ */
import _ from "underscore";
import * as React from "react"; import * as React from "react";
import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown"; import { Dropdown, IDropdownOption, IDropdownProps } from "office-ui-fabric-react/lib/Dropdown";
import { Tenant } from "../../../Contracts/DataModels"; import { Tenant } from "../../../Contracts/DataModels";
@@ -60,7 +61,7 @@ export class DefaultDirectoryDropdownComponent extends React.Component<DefaultDi
return; return;
} }
const selectedDirectory = this.props.directories.find(d => d.tenantId === option.key); const selectedDirectory = _.find(this.props.directories, d => d.tenantId === option.key);
if (!selectedDirectory) { if (!selectedDirectory) {
return; return;
} }

View File

@@ -1,3 +1,4 @@
import _ from "underscore";
import * as React from "react"; import * as React from "react";
import { DefaultButton, IButtonProps } from "office-ui-fabric-react/lib/Button"; import { DefaultButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
@@ -114,7 +115,7 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
} }
const buttonElement = e.currentTarget; const buttonElement = e.currentTarget;
const selectedDirectoryId = buttonElement.getElementsByClassName("directoryListItemId")[0].textContent; const selectedDirectoryId = buttonElement.getElementsByClassName("directoryListItemId")[0].textContent;
const selectedDirectory = this.props.directories.find(d => d.tenantId === selectedDirectoryId); const selectedDirectory = _.find(this.props.directories, d => d.tenantId === selectedDirectoryId);
this.props.onNewDirectorySelected(selectedDirectory); this.props.onNewDirectorySelected(selectedDirectory);
}; };

View File

@@ -93,7 +93,7 @@ export class AddRepoComponent extends React.Component<AddRepoComponentProps, Add
const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo); const repo = await this.props.getRepo(repoInfo.owner, repoInfo.repo);
if (repo) { if (repo) {
const item: RepoListItem = { const item: RepoListItem = {
key: GitHubUtils.toRepoFullName(repo.owner.login, repo.name), key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
repo, repo,
branches: [ branches: [
{ {

View File

@@ -18,7 +18,7 @@ import {
Text Text
} from "office-ui-fabric-react"; } from "office-ui-fabric-react";
import * as React from "react"; import * as React from "react";
import { IGitHubBranch } from "../../../GitHub/GitHubClient"; import { IGitHubBranch, IGitHubPageInfo } from "../../../GitHub/GitHubClient";
import { GitHubUtils } from "../../../Utils/GitHubUtils"; import { GitHubUtils } from "../../../Utils/GitHubUtils";
import { RepoListItem } from "./GitHubReposComponent"; import { RepoListItem } from "./GitHubReposComponent";
import { import {
@@ -41,6 +41,7 @@ export interface ReposListComponentProps {
export interface BranchesProps { export interface BranchesProps {
branches: IGitHubBranch[]; branches: IGitHubBranch[];
lastPageInfo?: IGitHubPageInfo;
hasMore: boolean; hasMore: boolean;
isLoading: boolean; isLoading: boolean;
loadMore: () => void; loadMore: () => void;
@@ -139,7 +140,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
} }
const checkboxProps: ICheckboxProps = { const checkboxProps: ICheckboxProps = {
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)), ...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
styles: ReposListCheckboxStyles, styles: ReposListCheckboxStyles,
defaultChecked: true, defaultChecked: true,
onChange: () => this.props.unpinRepo(item) onChange: () => this.props.unpinRepo(item)
@@ -153,7 +154,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
return <></>; return <></>;
} }
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)]; const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
const options: IDropdownOption[] = branchesProps.branches.map(branch => ({ const options: IDropdownOption[] = branchesProps.branches.map(branch => ({
key: branch.name, key: branch.name,
text: branch.name, text: branch.name,
@@ -222,7 +223,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element { private onRenderPinnedReposBranchesDropdownOption(option: IDropdownOption): JSX.Element {
const item: RepoListItem = option.data; const item: RepoListItem = option.data;
const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)]; const branchesProps = this.props.branchesProps[GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)];
if (option.index === ReposListComponent.FooterIndex) { if (option.index === ReposListComponent.FooterIndex) {
const linkProps: ILinkProps = { const linkProps: ILinkProps = {
@@ -267,7 +268,7 @@ export class ReposListComponent extends React.Component<ReposListComponentProps>
} }
const checkboxProps: ICheckboxProps = { const checkboxProps: ICheckboxProps = {
...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner.login, item.repo.name)), ...ReposListComponent.getCheckboxPropsForLabel(GitHubUtils.toRepoFullName(item.repo.owner, item.repo.name)),
styles: ReposListCheckboxStyles, styles: ReposListCheckboxStyles,
onChange: () => { onChange: () => {
const repoListItem = { ...item }; const repoListItem = { ...item };

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 {

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

@@ -2583,7 +2583,7 @@ export default class Explorer implements ViewModels.Explorer {
const item = NotebookUtil.createNotebookContentItem(name, path, "file"); const item = NotebookUtil.createNotebookContentItem(name, path, "file");
const parent = this.resourceTree.myNotebooksContentRoot; const parent = this.resourceTree.myNotebooksContentRoot;
if (parent && this.isNotebookEnabled() && this.notebookClient) { if (parent && parent.children && this.isNotebookEnabled() && this.notebookClient) {
if (this._filePathToImportAndOpen === path) { if (this._filePathToImportAndOpen === path) {
this._filePathToImportAndOpen = null; // we don't want to try opening this path again this._filePathToImportAndOpen = null; // we don't want to try opening this path again
} }
@@ -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

@@ -1,3 +1,4 @@
import _ from "underscore";
import * as React from "react"; import * as React from "react";
import * as ViewModels from "../../../Contracts/ViewModels"; import * as ViewModels from "../../../Contracts/ViewModels";
import { Observable } from "knockout"; import { Observable } from "knockout";
@@ -46,7 +47,7 @@ export class CommandBarUtil {
text: btn.commandButtonLabel || btn.tooltipText, text: btn.commandButtonLabel || btn.tooltipText,
"data-test": btn.commandButtonLabel || btn.tooltipText, "data-test": btn.commandButtonLabel || btn.tooltipText,
title: btn.tooltipText, title: btn.tooltipText,
name: "menuitem", name: btn.commandButtonLabel || btn.tooltipText,
disabled: btn.disabled, disabled: btn.disabled,
ariaLabel: btn.ariaLabel, ariaLabel: btn.ariaLabel,
buttonStyles: { buttonStyles: {
@@ -126,6 +127,9 @@ export class CommandBarUtil {
} }
if (btn.isDropdown) { if (btn.isDropdown) {
const selectedChild = _.find(btn.children, child => child.dropdownItemKey === btn.dropdownSelectedKey);
result.name = selectedChild?.commandButtonLabel || btn.dropdownPlaceholder;
const dropdownStyles: Partial<IDropdownStyles> = { const dropdownStyles: Partial<IDropdownStyles> = {
root: { margin: 5 }, root: { margin: 5 },
dropdown: { width: btn.dropdownWidth }, dropdown: { width: btn.dropdownWidth },

View File

@@ -1,5 +1,6 @@
import { ContentRef } from "@nteract/core";
import { CellId } from "@nteract/commutable"; import { CellId } from "@nteract/commutable";
import { ContentRef } from "@nteract/core";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK"; export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK";
export interface CloseNotebookAction { export interface CloseNotebookAction {
@@ -16,25 +17,6 @@ export const closeNotebook = (payload: { contentRef: ContentRef }): CloseNoteboo
}; };
}; };
export const UPDATE_LAST_MODIFIED = "UPDATE_LAST_MODIFIED";
export interface UpdateLastModifiedAction {
type: "UPDATE_LAST_MODIFIED";
payload: {
contentRef: ContentRef;
lastModified: string;
};
}
export const updateLastModified = (payload: {
contentRef: ContentRef;
lastModified: string;
}): UpdateLastModifiedAction => {
return {
type: UPDATE_LAST_MODIFIED,
payload
};
};
export const EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT = "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT"; export const EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT = "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
export interface ExecuteFocusedCellAndFocusNextAction { export interface ExecuteFocusedCellAndFocusNextAction {
type: "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT"; type: "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
@@ -81,3 +63,24 @@ export const setHoveredCell = (payload: { cellId: CellId }): SetHoveredCellActio
payload payload
}; };
}; };
export const TRACE_NOTEBOOK_TELEMETRY = "TRACE_NOTEBOOK_TELEMETRY";
export interface TraceNotebookTelemetryAction {
type: "TRACE_NOTEBOOK_TELEMETRY";
payload: {
action: Action;
actionModifier?: string;
data?: any;
};
}
export const traceNotebookTelemetry = (payload: {
action: Action;
actionModifier?: string;
data?: any;
}): TraceNotebookTelemetryAction => {
return {
type: TRACE_NOTEBOOK_TELEMETRY,
payload
};
};

View File

@@ -1,4 +1,4 @@
import { empty, merge, of, timer, interval, concat, Subject, Subscriber, Observable, Observer } from "rxjs"; import { empty, merge, of, timer, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { webSocket } from "rxjs/webSocket"; import { webSocket } from "rxjs/webSocket";
import { ActionsObservable, StateObservable } from "redux-observable"; import { ActionsObservable, StateObservable } from "redux-observable";
import { ofType } from "redux-observable"; import { ofType } from "redux-observable";
@@ -10,7 +10,6 @@ import {
map, map,
switchMap, switchMap,
take, take,
distinctUntilChanged,
filter, filter,
catchError, catchError,
first, first,
@@ -21,7 +20,6 @@ import {
AppState, AppState,
ServerConfig as JupyterServerConfig, ServerConfig as JupyterServerConfig,
JupyterHostRecordProps, JupyterHostRecordProps,
JupyterHostRecord,
RemoteKernelProps, RemoteKernelProps,
castToSessionId, castToSessionId,
createKernelRef, createKernelRef,
@@ -29,8 +27,7 @@ import {
ContentRef, ContentRef,
KernelInfo, KernelInfo,
actions, actions,
selectors, selectors
IContentProvider
} from "@nteract/core"; } from "@nteract/core";
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging"; import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
import { sessions, kernels } from "rx-jupyter"; import { sessions, kernels } from "rx-jupyter";
@@ -752,69 +749,6 @@ export const cleanKernelOnConnectionLostEpic = (
); );
}; };
/**
* Workaround for issue: https://github.com/nteract/nteract/issues/4583
* We reajust the property
* @param action$
* @param state$
*/
const adjustLastModifiedOnSaveEpic = (
action$: ActionsObservable<actions.SaveFulfilled>,
state$: StateObservable<AppState>,
dependencies: { contentProvider: IContentProvider }
): Observable<{} | CdbActions.UpdateLastModifiedAction> => {
return action$.pipe(
ofType(actions.SAVE_FULFILLED),
mergeMap(action => {
const pollDelayMs = 500;
const nbAttempts = 4;
// Retry updating last modified
const currentHost = selectors.currentHost(state$.value);
const serverConfig = selectors.serverConfig(currentHost as JupyterHostRecord);
const filepath = selectors.filepath(state$.value, { contentRef: action.payload.contentRef });
const content = selectors.content(state$.value, { contentRef: action.payload.contentRef });
const lastSaved = (content.lastSaved as any) as string;
const contentProvider = dependencies.contentProvider;
// Query until value is stable
return interval(pollDelayMs)
.pipe(take(nbAttempts))
.pipe(
mergeMap(x =>
contentProvider.get(serverConfig, filepath, { content: 0 }).pipe(
map(xhr => {
if (xhr.status !== 200 || typeof xhr.response === "string") {
return undefined;
}
const model = xhr.response;
const lastModified = model.last_modified;
if (lastModified === lastSaved) {
return undefined;
}
// Return last modified
return lastModified;
})
)
),
distinctUntilChanged(),
mergeMap(lastModified => {
if (!lastModified) {
return empty();
}
return of(
CdbActions.updateLastModified({
contentRef: action.payload.contentRef,
lastModified
})
);
})
);
})
);
};
/** /**
* Execute focused cell and focus next cell * Execute focused cell and focus next cell
* @param action$ * @param action$
@@ -917,7 +851,6 @@ export const allEpics = [
acquireKernelInfoEpic, acquireKernelInfoEpic,
handleKernelConnectionLostEpic, handleKernelConnectionLostEpic,
cleanKernelOnConnectionLostEpic, cleanKernelOnConnectionLostEpic,
adjustLastModifiedOnSaveEpic,
executeFocusedCellAndFocusNextEpic, executeFocusedCellAndFocusNextEpic,
closeUnsupportedMimetypesEpic, closeUnsupportedMimetypesEpic,
closeContentFailedToFetchEpic, closeContentFailedToFetchEpic,

View File

@@ -1,9 +1,10 @@
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
import { Action } from "redux";
import { Areas } from "../../../Common/Constants";
import TelemetryProcessor from "../../../Shared/Telemetry/TelemetryProcessor";
import * as cdbActions from "./actions"; import * as cdbActions from "./actions";
import { CdbRecord } from "./types"; import { CdbRecord } from "./types";
import { Action } from "redux";
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
export const coreReducer = (state: CoreRecord, action: Action) => { export const coreReducer = (state: CoreRecord, action: Action) => {
let typedAction; let typedAction;
switch (action.type) { switch (action.type) {
@@ -50,11 +51,6 @@ export const coreReducer = (state: CoreRecord, action: Action) => {
.setIn(path.concat("displayName"), kernelspecs.displayName) .setIn(path.concat("displayName"), kernelspecs.displayName)
.setIn(path.concat("language"), kernelspecs.language); .setIn(path.concat("language"), kernelspecs.language);
} }
case cdbActions.UPDATE_LAST_MODIFIED: {
typedAction = action as cdbActions.UpdateLastModifiedAction;
const path = ["entities", "contents", "byRef", typedAction.payload.contentRef, "lastSaved"];
return state.setIn(path, typedAction.payload.lastModified);
}
default: default:
return nteractReducers.core(state as any, action as any); return nteractReducers.core(state as any, action as any);
} }
@@ -75,6 +71,17 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
const typedAction = action as cdbActions.SetHoveredCellAction; const typedAction = action as cdbActions.SetHoveredCellAction;
return state.set("hoveredCellId", typedAction.payload.cellId); return state.set("hoveredCellId", typedAction.payload.cellId);
} }
case cdbActions.TRACE_NOTEBOOK_TELEMETRY: {
const typedAction = action as cdbActions.TraceNotebookTelemetryAction;
TelemetryProcessor.trace(typedAction.payload.action, typedAction.payload.actionModifier, {
...typedAction.payload.data,
databaseAccountName: state.databaseAccountName,
defaultExperience: state.defaultExperience,
dataExplorerArea: Areas.Notebook
});
return state;
}
} }
return state; return state;
}; };

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

@@ -1,8 +1,9 @@
import { actions, ContentRef, selectors } from "@nteract/core";
import React from "react"; import React from "react";
import { connect } from "react-redux"; import { connect } from "react-redux";
import { Dispatch } from "redux"; import { Dispatch } from "redux";
import { Action, ActionModifiers } from "../../../Shared/Telemetry/TelemetryConstants";
import { ContentRef, selectors, actions } from "@nteract/core"; import * as cdbActions from "../NotebookComponent/actions";
import { CdbAppState } from "../NotebookComponent/types"; import { CdbAppState } from "../NotebookComponent/types";
export interface PassedPromptProps { export interface PassedPromptProps {
@@ -83,7 +84,15 @@ const mapDispatchToProps = (
dispatch: Dispatch, dispatch: Dispatch,
{ id, contentRef }: { id: string; contentRef: ContentRef } { id, contentRef }: { id: string; contentRef: ContentRef }
): DispatchProps => ({ ): DispatchProps => ({
executeCell: () => dispatch(actions.executeCell({ id, contentRef })), executeCell: () => {
dispatch(actions.executeCell({ id, contentRef }));
dispatch(
cdbActions.traceNotebookTelemetry({
action: Action.ExecuteCellPromptBtn,
actionModifier: ActionModifiers.Mark
})
);
},
stopExecution: () => dispatch(actions.interruptKernel({})) stopExecution: () => dispatch(actions.interruptKernel({}))
}); });

View File

@@ -0,0 +1,127 @@
import { IGitHubRepo, IGitHubBranch } from "../../GitHub/GitHubClient";
export const SamplesRepo: IGitHubRepo = {
name: "cosmos-notebooks",
owner: "Azure-Samples",
private: false
};
export const SamplesBranch: IGitHubBranch = {
name: "master"
};
export const isSamplesCall = (owner: string, repo: string, branch?: string): boolean => {
return owner === SamplesRepo.owner && repo === SamplesRepo.name && (!branch || branch === SamplesBranch.name);
};
// GitHub API calls have a rate limit of 5000 requests per hour. So if we get high traffic on Data Explorer
// loading samples exceed that limit. Using this hard coded response for samples until we fix that.
export const SamplesContentsQueryResponse = {
repository: {
owner: {
login: "Azure-Samples"
},
name: "cosmos-notebooks",
isPrivate: false,
ref: {
name: "master",
target: {
history: {
nodes: [
{
oid: "cda7facb9e039b173f3376200c26c859896e7974",
message:
"Merge pull request #45 from Azure-Samples/users/deborahc/pythonSampleUpdates\n\nAdd bokeh version to notebook",
committer: {
date: "2020-05-28T11:28:01-07:00"
}
}
]
}
}
},
object: {
entries: [
{
name: ".github",
type: "tree",
object: {}
},
{
name: ".gitignore",
type: "blob",
object: {
oid: "3e759b75bf455ac809d0987d369aab89137b5689",
byteSize: 5582
}
},
{
name: "1. GettingStarted.ipynb",
type: "blob",
object: {
oid: "0732ff5366e4aefdc4c378c61cbd968664f0acec",
byteSize: 3933
}
},
{
name: "2. Visualization.ipynb",
type: "blob",
object: {
oid: "6b16b0740a77afdd38a95bc6c3ebd0f2f17d9465",
byteSize: 820317
}
},
{
name: "3. RequestUnits.ipynb",
type: "blob",
object: {
oid: "252b79a4adc81e9f2ffde453231b695d75e270e8",
byteSize: 9490
}
},
{
name: "4. Indexing.ipynb",
type: "blob",
object: {
oid: "e10dd67bd1c55c345226769e4f80e43659ef9cd5",
byteSize: 10394
}
},
{
name: "5. StoredProcedures.ipynb",
type: "blob",
object: {
oid: "949941949920de4d2d111149e2182e9657cc8134",
byteSize: 11818
}
},
{
name: "6. GlobalDistribution.ipynb",
type: "blob",
object: {
oid: "b91c31dacacbc9e35750d9054063dda4a5309f3b",
byteSize: 11375
}
},
{
name: "7. IoTAnomalyDetection.ipynb",
type: "blob",
object: {
oid: "82057ae52a67721a5966e2361317f5dfbd0ee595",
byteSize: 377939
}
},
{
name: "All_API_quickstarts",
type: "tree",
object: {}
},
{
name: "CSharp_quickstarts",
type: "tree",
object: {}
}
]
}
}
};

View File

@@ -155,19 +155,7 @@ function openPane(action: ActionContracts.OpenPane, explorer: ViewModels.Explore
} }
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: ViewModels.Explorer) { function openFile(action: ActionContracts.OpenSampleNotebook, explorer: ViewModels.Explorer) {
let path: string; explorer.handleOpenFileAction(decodeURIComponent(action.path));
if (action.hasOwnProperty("file")) {
// This is deprecated
const downloadUrl: string = (action as any).file.download_url;
path = downloadUrl.replace(
"raw.githubusercontent.com/Azure-Samples/cosmos-notebooks",
"github.com/Azure-Samples/cosmos-notebooks/blob"
); // convert raw download url to something which GitHubContentProvider understands
} else {
path = action.path;
}
explorer.handleOpenFileAction(path);
} }
function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string { function generateQueryText(action: ActionContracts.OpenQueryTab, partitionKeyProperty: string): string {

View File

@@ -1,3 +1,4 @@
import _ from "underscore";
import * as ko from "knockout"; import * as ko from "knockout";
import * as Constants from "../../Common/Constants"; import * as Constants from "../../Common/Constants";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
@@ -81,7 +82,7 @@ export class ClusterLibraryPane extends ContextualPaneBase {
private _onInstalledChanged = (libraryName: string, installed: boolean): void => { private _onInstalledChanged = (libraryName: string, installed: boolean): void => {
const items = this._clusterLibraryProps().libraryItems; const items = this._clusterLibraryProps().libraryItems;
const library = items.find(item => item.name === libraryName); const library = _.find(items, item => item.name === libraryName);
library.installed = installed; library.installed = installed;
this._clusterLibraryProps.valueHasMutated(); this._clusterLibraryProps.valueHasMutated();
}; };

View File

@@ -1,19 +1,20 @@
import _ from "underscore";
import { Areas, HttpStatusCodes } from "../../Common/Constants"; import { Areas, HttpStatusCodes } from "../../Common/Constants";
import { Logger } from "../../Common/Logger"; import { Logger } from "../../Common/Logger";
import * as ViewModels from "../../Contracts/ViewModels"; import * as ViewModels from "../../Contracts/ViewModels";
import { GitHubClient, IGitHubRepo } from "../../GitHub/GitHubClient"; import { GitHubClient, IGitHubPageInfo, IGitHubRepo } from "../../GitHub/GitHubClient";
import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient"; import { IPinnedRepo, JunoClient } from "../../Juno/JunoClient";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor"; import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { GitHubUtils } from "../../Utils/GitHubUtils"; import { GitHubUtils } from "../../Utils/GitHubUtils";
import { JunoUtils } from "../../Utils/JunoUtils";
import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils"; import { NotificationConsoleUtils } from "../../Utils/NotificationConsoleUtils";
import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent"; import { AuthorizeAccessComponent } from "../Controls/GitHub/AuthorizeAccessComponent";
import { GitHubReposComponentProps, RepoListItem, GitHubReposComponent } from "../Controls/GitHub/GitHubReposComponent"; import { GitHubReposComponent, GitHubReposComponentProps, RepoListItem } from "../Controls/GitHub/GitHubReposComponent";
import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter"; import { GitHubReposComponentAdapter } from "../Controls/GitHub/GitHubReposComponentAdapter";
import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent"; import { BranchesProps, PinnedReposProps, UnpinnedReposProps } from "../Controls/GitHub/ReposListComponent";
import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent"; import { ConsoleDataType } from "../Menus/NotificationConsole/NotificationConsoleComponent";
import { ContextualPaneBase } from "./ContextualPaneBase"; import { ContextualPaneBase } from "./ContextualPaneBase";
import { JunoUtils } from "../../Utils/JunoUtils";
export class GitHubReposPane extends ContextualPaneBase { export class GitHubReposPane extends ContextualPaneBase {
private static readonly PageSize = 30; private static readonly PageSize = 30;
@@ -29,6 +30,7 @@ export class GitHubReposPane extends ContextualPaneBase {
private gitHubReposAdapter: GitHubReposComponentAdapter; private gitHubReposAdapter: GitHubReposComponentAdapter;
private allGitHubRepos: IGitHubRepo[]; private allGitHubRepos: IGitHubRepo[];
private allGitHubReposLastPageInfo?: IGitHubPageInfo;
private pinnedReposUpdated: boolean; private pinnedReposUpdated: boolean;
constructor(options: ViewModels.GitHubReposPaneOptions) { constructor(options: ViewModels.GitHubReposPaneOptions) {
@@ -73,6 +75,7 @@ export class GitHubReposPane extends ContextualPaneBase {
this.gitHubReposAdapter = new GitHubReposComponentAdapter(this.gitHubReposProps); this.gitHubReposAdapter = new GitHubReposComponentAdapter(this.gitHubReposProps);
this.allGitHubRepos = []; this.allGitHubRepos = [];
this.allGitHubReposLastPageInfo = undefined;
this.pinnedReposUpdated = false; this.pinnedReposUpdated = false;
} }
@@ -115,6 +118,7 @@ export class GitHubReposPane extends ContextualPaneBase {
// Reset cached repos // Reset cached repos
this.allGitHubRepos = []; this.allGitHubRepos = [];
this.allGitHubReposLastPageInfo = undefined;
// Reset flags // Reset flags
this.pinnedReposUpdated = false; this.pinnedReposUpdated = false;
@@ -164,29 +168,28 @@ export class GitHubReposPane extends ContextualPaneBase {
const unpinnedGitHubRepos = this.allGitHubRepos.filter( const unpinnedGitHubRepos = this.allGitHubRepos.filter(
gitHubRepo => gitHubRepo =>
this.pinnedReposProps.repos.findIndex( this.pinnedReposProps.repos.findIndex(
pinnedRepo => pinnedRepo.key === GitHubUtils.toRepoFullName(gitHubRepo.owner.login, gitHubRepo.name) pinnedRepo => pinnedRepo.key === GitHubUtils.toRepoFullName(gitHubRepo.owner, gitHubRepo.name)
) === -1 ) === -1
); );
return unpinnedGitHubRepos.map(gitHubRepo => ({ return unpinnedGitHubRepos.map(gitHubRepo => ({
key: GitHubUtils.toRepoFullName(gitHubRepo.owner.login, gitHubRepo.name), key: GitHubUtils.toRepoFullName(gitHubRepo.owner, gitHubRepo.name),
repo: gitHubRepo, repo: gitHubRepo,
branches: [] branches: []
})); }));
} }
private async loadMoreBranches(repo: IGitHubRepo): Promise<void> { private async loadMoreBranches(repo: IGitHubRepo): Promise<void> {
const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner.login, repo.name)]; const branchesProps = this.branchesProps[GitHubUtils.toRepoFullName(repo.owner, repo.name)];
branchesProps.hasMore = true; branchesProps.hasMore = true;
branchesProps.isLoading = true; branchesProps.isLoading = true;
this.triggerRender(); this.triggerRender();
const nextPage = Math.floor(branchesProps.branches.length / GitHubReposPane.PageSize) + 1;
try { try {
const response = await this.gitHubClient.getBranchesAsync( const response = await this.gitHubClient.getBranchesAsync(
repo.owner.login, repo.owner,
repo.name, repo.name,
nextPage, GitHubReposPane.PageSize,
GitHubReposPane.PageSize branchesProps.lastPageInfo?.endCursor
); );
if (response.status !== HttpStatusCodes.OK) { if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when fetching branches`); throw new Error(`Received HTTP ${response.status} when fetching branches`);
@@ -194,6 +197,7 @@ export class GitHubReposPane extends ContextualPaneBase {
if (response.data) { if (response.data) {
branchesProps.branches = branchesProps.branches.concat(response.data); branchesProps.branches = branchesProps.branches.concat(response.data);
branchesProps.lastPageInfo = response.pageInfo;
} }
} catch (error) { } catch (error) {
const message = `Failed to fetch branches: ${error}`; const message = `Failed to fetch branches: ${error}`;
@@ -202,7 +206,7 @@ export class GitHubReposPane extends ContextualPaneBase {
} }
branchesProps.isLoading = false; branchesProps.isLoading = false;
branchesProps.hasMore = branchesProps.branches.length === GitHubReposPane.PageSize * nextPage; branchesProps.hasMore = branchesProps.lastPageInfo?.hasNextPage;
this.triggerRender(); this.triggerRender();
} }
@@ -211,15 +215,18 @@ export class GitHubReposPane extends ContextualPaneBase {
this.unpinnedReposProps.hasMore = true; this.unpinnedReposProps.hasMore = true;
this.triggerRender(); this.triggerRender();
const nextPage = Math.floor(this.allGitHubRepos.length / GitHubReposPane.PageSize) + 1;
try { try {
const response = await this.gitHubClient.getReposAsync(nextPage, GitHubReposPane.PageSize); const response = await this.gitHubClient.getReposAsync(
GitHubReposPane.PageSize,
this.allGitHubReposLastPageInfo?.endCursor
);
if (response.status !== HttpStatusCodes.OK) { if (response.status !== HttpStatusCodes.OK) {
throw new Error(`Received HTTP ${response.status} when fetching unpinned repos`); throw new Error(`Received HTTP ${response.status} when fetching unpinned repos`);
} }
if (response.data) { if (response.data) {
this.allGitHubRepos = this.allGitHubRepos.concat(response.data); this.allGitHubRepos = this.allGitHubRepos.concat(response.data);
this.allGitHubReposLastPageInfo = response.pageInfo;
this.unpinnedReposProps.repos = this.calculateUnpinnedRepos(); this.unpinnedReposProps.repos = this.calculateUnpinnedRepos();
} }
} catch (error) { } catch (error) {
@@ -229,7 +236,7 @@ export class GitHubReposPane extends ContextualPaneBase {
} }
this.unpinnedReposProps.isLoading = false; this.unpinnedReposProps.isLoading = false;
this.unpinnedReposProps.hasMore = this.allGitHubRepos.length === GitHubReposPane.PageSize * nextPage; this.unpinnedReposProps.hasMore = this.allGitHubReposLastPageInfo?.hasNextPage;
this.triggerRender(); this.triggerRender();
} }
@@ -253,7 +260,7 @@ export class GitHubReposPane extends ContextualPaneBase {
this.pinnedReposUpdated = true; this.pinnedReposUpdated = true;
const initialReposLength = this.pinnedReposProps.repos.length; const initialReposLength = this.pinnedReposProps.repos.length;
const existingRepo = this.pinnedReposProps.repos.find(repo => repo.key === item.key); const existingRepo = _.find(this.pinnedReposProps.repos, repo => repo.key === item.key);
if (existingRepo) { if (existingRepo) {
existingRepo.branches = item.branches; existingRepo.branches = item.branches;
} else { } else {
@@ -318,6 +325,7 @@ export class GitHubReposPane extends ContextualPaneBase {
if (!this.branchesProps[item.key]) { if (!this.branchesProps[item.key]) {
this.branchesProps[item.key] = { this.branchesProps[item.key] = {
branches: [], branches: [],
lastPageInfo: undefined,
hasMore: true, hasMore: true,
isLoading: true, isLoading: true,
loadMore: (): Promise<void> => this.loadMoreBranches(item.repo) loadMore: (): Promise<void> => this.loadMoreBranches(item.repo)
@@ -329,6 +337,7 @@ export class GitHubReposPane extends ContextualPaneBase {
private async refreshUnpinnedRepoListItems(): Promise<void> { private async refreshUnpinnedRepoListItems(): Promise<void> {
this.allGitHubRepos = []; this.allGitHubRepos = [];
this.allGitHubReposLastPageInfo = undefined;
this.unpinnedReposProps.repos = []; this.unpinnedReposProps.repos = [];
this.loadMoreUnpinnedRepos(); this.loadMoreUnpinnedRepos();
} }

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

@@ -32,6 +32,7 @@ import DocumentId from "./DocumentId";
import StoredProcedure from "./StoredProcedure"; import StoredProcedure from "./StoredProcedure";
import Trigger from "./Trigger"; import Trigger from "./Trigger";
import UserDefinedFunction from "./UserDefinedFunction"; import UserDefinedFunction from "./UserDefinedFunction";
import { config } from "../../Config";
export default class Collection implements ViewModels.Collection { export default class Collection implements ViewModels.Collection {
public nodeKind: string; public nodeKind: string;
@@ -1416,7 +1417,7 @@ export default class Collection implements ViewModels.Collection {
masterKey: CosmosClient.masterKey(), masterKey: CosmosClient.masterKey(),
endpoint: CosmosClient.endpoint(), endpoint: CosmosClient.endpoint(),
accessToken: CosmosClient.accessToken(), accessToken: CosmosClient.accessToken(),
platform: window.dataExplorerPlatform, platform: config.platform,
databaseAccount: CosmosClient.databaseAccount() databaseAccount: CosmosClient.databaseAccount()
} }
}; };

View File

@@ -14,7 +14,6 @@ import CollectionIcon from "../../../images/tree-collection.svg";
import DeleteIcon from "../../../images/delete.svg"; import DeleteIcon from "../../../images/delete.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg"; import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import RefreshIcon from "../../../images/refresh-cosmos.svg"; import RefreshIcon from "../../../images/refresh-cosmos.svg";
import { IGitHubRepo, IGitHubBranch } from "../../GitHub/GitHubClient";
import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg"; import NewNotebookIcon from "../../../images/notebook/Notebook-new.svg";
import FileIcon from "../../../images/notebook/file-cosmos.svg"; import FileIcon from "../../../images/notebook/file-cosmos.svg";
import { ArrayHashMap } from "../../Common/ArrayHashMap"; import { ArrayHashMap } from "../../Common/ArrayHashMap";
@@ -26,21 +25,11 @@ import TelemetryProcessor from "../../Shared/Telemetry/TelemetryProcessor";
import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants"; import { Action, ActionModifiers } from "../../Shared/Telemetry/TelemetryConstants";
import { Areas } from "../../Common/Constants"; import { Areas } from "../../Common/Constants";
import { GitHubUtils } from "../../Utils/GitHubUtils"; import { GitHubUtils } from "../../Utils/GitHubUtils";
import { SamplesRepo, SamplesBranch } from "../Notebook/NotebookSamples";
export class ResourceTreeAdapter implements ReactAdapter { export class ResourceTreeAdapter implements ReactAdapter {
private static readonly DataTitle = "DATA"; private static readonly DataTitle = "DATA";
private static readonly NotebooksTitle = "NOTEBOOKS"; private static readonly NotebooksTitle = "NOTEBOOKS";
private static readonly SamplesRepo: IGitHubRepo = {
name: "cosmos-notebooks",
owner: {
login: "Azure-Samples"
},
private: false
};
private static readonly SamplesBranch: IGitHubBranch = {
name: "master"
};
private static readonly PseudoDirPath = "PsuedoDir"; private static readonly PseudoDirPath = "PsuedoDir";
public parameters: ko.Observable<number>; public parameters: ko.Observable<number>;
@@ -103,12 +92,7 @@ export class ResourceTreeAdapter implements ReactAdapter {
this.sampleNotebooksContentRoot = { this.sampleNotebooksContentRoot = {
name: "Sample Notebooks (View Only)", name: "Sample Notebooks (View Only)",
path: GitHubUtils.toContentUri( path: GitHubUtils.toContentUri(SamplesRepo.owner, SamplesRepo.name, SamplesBranch.name, ""),
ResourceTreeAdapter.SamplesRepo.owner.login,
ResourceTreeAdapter.SamplesRepo.name,
ResourceTreeAdapter.SamplesBranch.name,
""
),
type: NotebookContentItemType.Directory type: NotebookContentItemType.Directory
}; };
refreshTasks.push( refreshTasks.push(

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

@@ -1,92 +1,110 @@
import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { GitHubClient, IGitHubBranch, IGitHubRepo } from "./GitHubClient"; import { GitHubClient, IGitHubFile } from "./GitHubClient";
import { SamplesRepo, SamplesBranch, SamplesContentsQueryResponse } from "../Explorer/Notebook/NotebookSamples";
const invalidTokenCallback = jest.fn(); const invalidTokenCallback = jest.fn();
// Use a dummy token to get around API rate limit (same as AZURESAMPLESCOSMOSDBPAT in webpack.config.js) // Use a dummy token to get around API rate limit (something which doesn't affect the API quota for AZURESAMPLESCOSMOSDBPAT in Config.ts)
const gitHubClient = new GitHubClient("99e38770e29b4a61d7c49f188780504efd35cc86", invalidTokenCallback); const gitHubClient = new GitHubClient("cd1906b9534362fab6ce45d6db6c76b59e55bc50", invalidTokenCallback);
const samplesRepo: IGitHubRepo = {
name: "cosmos-notebooks",
owner: {
login: "Azure-Samples"
},
private: false
};
const samplesBranch: IGitHubBranch = {
name: "master"
};
const sampleFilePath = ".gitignore";
const sampleDirPath = ".github";
describe.skip("GitHubClient", () => { const validateGitHubFile = (file: IGitHubFile) => {
expect(file.branch).toEqual(SamplesBranch);
expect(file.commit).toBeDefined();
expect(file.name).toBeDefined();
expect(file.path).toBeDefined();
expect(file.repo).toEqual(SamplesRepo);
expect(file.type).toBeDefined();
switch (file.type) {
case "blob":
expect(file.sha).toBeDefined();
expect(file.size).toBeDefined();
break;
case "tree":
expect(file.sha).toBeUndefined();
expect(file.size).toBeUndefined();
break;
default:
throw new Error(`Unsupported github file type: ${file.type}`);
}
};
describe("GitHubClient", () => {
it("getRepoAsync returns valid repo", async () => { it("getRepoAsync returns valid repo", async () => {
const response = await gitHubClient.getRepoAsync(samplesRepo.owner.login, samplesRepo.name); const response = await gitHubClient.getRepoAsync(SamplesRepo.owner, SamplesRepo.name);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response).toEqual({
expect(response.data.name).toBe(samplesRepo.name); status: HttpStatusCodes.OK,
expect(response.data.owner.login).toBe(samplesRepo.owner.login); data: SamplesRepo
});
}); });
it("getReposAsync returns repos for authenticated user", async () => { it("getReposAsync returns repos for authenticated user", async () => {
const response = await gitHubClient.getReposAsync(1, 1); const response = await gitHubClient.getReposAsync(1);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data).toBeDefined();
expect(response.data.length).toBe(1); expect(response.data.length).toBe(1);
expect(response.pageInfo).toBeDefined();
}); });
it("getBranchesAsync returns branches for a repo", async () => { it("getBranchesAsync returns branches for a repo", async () => {
const response = await gitHubClient.getBranchesAsync(samplesRepo.owner.login, samplesRepo.name, 1, 1); const response = await gitHubClient.getBranchesAsync(SamplesRepo.owner, SamplesRepo.name, 1);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.length).toBe(1); expect(response.data).toEqual([SamplesBranch]);
expect(response.pageInfo).toBeDefined();
}); });
it("getCommitsAsync returns commits for a file", async () => { it("getContentsAsync returns files in the repo", async () => {
const response = await gitHubClient.getCommitsAsync( const response = await gitHubClient.getContentsAsync(SamplesRepo.owner, SamplesRepo.name, SamplesBranch.name);
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
sampleFilePath,
1,
1
);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.length).toBe(1); expect(response.data).toBeDefined();
const data = response.data as IGitHubFile[];
expect(data.length).toBeGreaterThan(0);
data.forEach(content => validateGitHubFile(content));
}); });
it("getDirContentsAsync returns files in the repo", async () => { it("getContentsAsync returns files in a dir", async () => {
const response = await gitHubClient.getDirContentsAsync( const samplesDir = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "tree");
samplesRepo.owner.login, const response = await gitHubClient.getContentsAsync(
samplesRepo.name, SamplesRepo.owner,
samplesBranch.name, SamplesRepo.name,
"" SamplesBranch.name,
samplesDir.name
); );
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.length).toBeGreaterThan(0); expect(response.data).toBeDefined();
expect(response.data[0].repo).toEqual(samplesRepo);
expect(response.data[0].branch).toEqual(samplesBranch); const data = response.data as IGitHubFile[];
expect(data.length).toBeGreaterThan(0);
data.forEach(content => validateGitHubFile(content));
}); });
it("getDirContentsAsync returns files in a dir", async () => { it("getContentsAsync returns a file", async () => {
const response = await gitHubClient.getDirContentsAsync( const samplesFile = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "blob");
samplesRepo.owner.login, const response = await gitHubClient.getContentsAsync(
samplesRepo.name, SamplesRepo.owner,
samplesBranch.name, SamplesRepo.name,
sampleDirPath SamplesBranch.name,
samplesFile.name
); );
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.length).toBeGreaterThan(0); expect(response.data).toBeDefined();
expect(response.data[0].repo).toEqual(samplesRepo);
expect(response.data[0].branch).toEqual(samplesBranch); const file = response.data as IGitHubFile;
expect(file.type).toBe("blob");
validateGitHubFile(file);
expect(file.content).toBeUndefined();
}); });
it("getFileContentsAsync returns a file", async () => { it("getBlobAsync returns file content", async () => {
const response = await gitHubClient.getFileContentsAsync( const samplesFile = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "blob");
samplesRepo.owner.login, const response = await gitHubClient.getBlobAsync(SamplesRepo.owner, SamplesRepo.name, samplesFile.object.oid);
samplesRepo.name,
samplesBranch.name,
sampleFilePath
);
expect(response.status).toBe(HttpStatusCodes.OK); expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.path).toBe(sampleFilePath); expect(response.data).toBeDefined();
expect(response.data.repo).toEqual(samplesRepo); expect(typeof response.data).toBe("string");
expect(response.data.branch).toEqual(samplesBranch);
}); });
}); });

View File

@@ -1,163 +1,228 @@
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { RequestHeaders } from "@octokit/types";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { Logger } from "../Common/Logger";
import UrlUtility from "../Common/UrlUtility";
import { isSamplesCall, SamplesContentsQueryResponse } from "../Explorer/Notebook/NotebookSamples";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
export interface IGitHubPageInfo {
endCursor: string;
hasNextPage: boolean;
}
export interface IGitHubResponse<T> { export interface IGitHubResponse<T> {
status: number; status: number;
data: T; data: T;
pageInfo?: IGitHubPageInfo;
} }
export interface IGitHubRepo { export interface IGitHubRepo {
// API properties
name: string; name: string;
owner: { owner: string;
login: string;
};
private: boolean; private: boolean;
// Custom properties
children?: IGitHubFile[]; children?: IGitHubFile[];
} }
export interface IGitHubFile { export interface IGitHubFile {
// API properties type: "blob" | "tree";
type: "file" | "dir" | "symlink" | "submodule"; size?: number;
encoding?: string;
size: number;
name: string; name: string;
path: string; path: string;
content?: string; content?: string;
sha: string; sha?: string;
// Custom properties
children?: IGitHubFile[]; children?: IGitHubFile[];
repo?: IGitHubRepo; repo: IGitHubRepo;
branch?: IGitHubBranch; branch: IGitHubBranch;
commit: IGitHubCommit;
} }
export interface IGitHubCommit { export interface IGitHubCommit {
// API properties
sha: string; sha: string;
message: string; message: string;
committer: { commitDate: string;
date: string;
};
} }
export interface IGitHubBranch { export interface IGitHubBranch {
// API properties
name: string; name: string;
} }
export interface IGitHubUser { // graphql schema
// API properties interface Collection<T> {
login: string; pageInfo?: PageInfo;
nodes: T[];
}
interface Repository {
isPrivate: boolean;
name: string;
owner: {
login: string;
};
}
interface Ref {
name: string; name: string;
} }
interface History {
history: Collection<Commit>;
}
interface Commit {
committer: {
date: string;
};
message: string;
oid: string;
}
interface Tree extends Blob {
entries: TreeEntry[];
}
interface TreeEntry {
name: string;
type: string;
object: Blob;
}
interface Blob {
byteSize?: number;
oid?: string;
}
interface PageInfo {
endCursor: string;
hasNextPage: boolean;
}
// graphql queries and types
const repositoryQuery = `query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
owner {
login
}
name
isPrivate
}
}`;
type RepositoryQueryParams = {
owner: string;
repo: string;
};
type RepositoryQueryResponse = {
repository: Repository;
};
const repositoriesQuery = `query($pageSize: Int!, $endCursor: String) {
viewer {
repositories(first: $pageSize, after: $endCursor) {
pageInfo {
endCursor,
hasNextPage
}
nodes {
owner {
login
}
name
isPrivate
}
}
}
}`;
type RepositoriesQueryParams = {
pageSize: number;
endCursor?: string;
};
type RepositoriesQueryResponse = {
viewer: {
repositories: Collection<Repository>;
};
};
const branchesQuery = `query($owner: String!, $repo: String!, $refPrefix: String!, $pageSize: Int!, $endCursor: String) {
repository(owner: $owner, name: $repo) {
refs(refPrefix: $refPrefix, first: $pageSize, after: $endCursor) {
pageInfo {
endCursor,
hasNextPage
}
nodes {
name
}
}
}
}`;
type BranchesQueryParams = {
owner: string;
repo: string;
refPrefix: string;
pageSize: number;
endCursor?: string;
};
type BranchesQueryResponse = {
repository: {
refs: Collection<Ref>;
};
};
const contentsQuery = `query($owner: String!, $repo: String!, $ref: String!, $path: String, $objectExpression: String!) {
repository(owner: $owner, name: $repo) {
owner {
login
}
name
isPrivate
ref(qualifiedName: $ref) {
name
target {
... on Commit {
history(first: 1, path: $path) {
nodes {
oid
message
committer {
date
}
}
}
}
}
}
object(expression: $objectExpression) {
... on Blob {
oid
byteSize
}
... on Tree {
entries {
name
type
object {
... on Blob {
oid
byteSize
}
}
}
}
}
}
}`;
type ContentsQueryParams = {
owner: string;
repo: string;
ref: string;
path?: string;
objectExpression: string;
};
type ContentsQueryResponse = {
repository: Repository & { ref: Ref & { target: History } } & { object: Tree };
};
export class GitHubClient { export class GitHubClient {
private static readonly gitHubApiEndpoint = "https://api.github.com"; private static readonly SelfErrorCode = 599;
private static readonly samplesRepo: IGitHubRepo = {
name: "cosmos-notebooks",
private: false,
owner: {
login: "Azure-Samples"
}
};
private static readonly samplesBranch: IGitHubBranch = {
name: "master"
};
private static readonly samplesTopCommit: IGitHubCommit = {
sha: "41b964f442b638097a75a3f3b6a6451db05a12bf",
committer: {
date: "2020-05-19T05:03:30Z"
},
message: "Fixing formatting"
};
private static readonly samplesFiles: IGitHubFile[] = [
{
name: ".github",
path: ".github",
sha: "5e6794a8177a0c07a8719f6e1d7b41cce6f92e1e",
size: 0,
type: "dir"
},
{
name: ".gitignore",
path: ".gitignore",
sha: "3e759b75bf455ac809d0987d369aab89137b5689",
size: 5582,
type: "file"
},
{
name: "1. GettingStarted.ipynb",
path: "1. GettingStarted.ipynb",
sha: "0732ff5366e4aefdc4c378c61cbd968664f0acec",
size: 3933,
type: "file"
},
{
name: "2. Visualization.ipynb",
path: "2. Visualization.ipynb",
sha: "f480134ac4adf2f50ce5fe66836c6966749d3ca1",
size: 814261,
type: "file"
},
{
name: "3. RequestUnits.ipynb",
path: "3. RequestUnits.ipynb",
sha: "252b79a4adc81e9f2ffde453231b695d75e270e8",
size: 9490,
type: "file"
},
{
name: "4. Indexing.ipynb",
path: "4. Indexing.ipynb",
sha: "e10dd67bd1c55c345226769e4f80e43659ef9cd5",
size: 10394,
type: "file"
},
{
name: "5. StoredProcedures.ipynb",
path: "5. StoredProcedures.ipynb",
sha: "949941949920de4d2d111149e2182e9657cc8134",
size: 11818,
type: "file"
},
{
name: "6. GlobalDistribution.ipynb",
path: "6. GlobalDistribution.ipynb",
sha: "b91c31dacacbc9e35750d9054063dda4a5309f3b",
size: 11375,
type: "file"
},
{
name: "7. IoTAnomalyDetection.ipynb",
path: "7. IoTAnomalyDetection.ipynb",
sha: "82057ae52a67721a5966e2361317f5dfbd0ee595",
size: 377939,
type: "file"
},
{
name: "All_API_quickstarts",
path: "All_API_quickstarts",
sha: "07054293e6c8fc00771fccd0cde207f5c8053978",
size: 0,
type: "dir"
},
{
name: "CSharp_quickstarts",
path: "CSharp_quickstarts",
sha: "10e7f5704e6b56a40cac74bc39f15b7708954f52",
size: 0,
type: "dir"
}
];
private ocktokit: Octokit; private ocktokit: Octokit;
constructor(token: string, private errorCallback: (error: any) => void) { constructor(token: string, private errorCallback: (error: any) => void) {
@@ -169,167 +234,136 @@ export class GitHubClient {
} }
public async getRepoAsync(owner: string, repo: string): Promise<IGitHubResponse<IGitHubRepo>> { public async getRepoAsync(owner: string, repo: string): Promise<IGitHubResponse<IGitHubRepo>> {
if (GitHubClient.isSamplesCall(owner, repo)) { try {
const response = (await this.ocktokit.graphql(repositoryQuery, {
owner,
repo
} as RepositoryQueryParams)) as RepositoryQueryResponse;
return { return {
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
data: GitHubClient.samplesRepo data: GitHubClient.toGitHubRepo(response.repository)
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getRepoAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
}; };
} }
const response = await this.ocktokit.repos.get({
owner,
repo,
headers: GitHubClient.getDisableCacheHeaders()
});
let data: IGitHubRepo;
if (response.data) {
data = GitHubClient.toGitHubRepo(response.data);
}
return { status: response.status, data };
} }
public async getReposAsync(page: number, perPage: number): Promise<IGitHubResponse<IGitHubRepo[]>> { public async getReposAsync(pageSize: number, endCursor?: string): Promise<IGitHubResponse<IGitHubRepo[]>> {
const response = await this.ocktokit.repos.listForAuthenticatedUser({ try {
page, const response = (await this.ocktokit.graphql(repositoriesQuery, {
per_page: perPage, pageSize,
headers: GitHubClient.getDisableCacheHeaders() endCursor
}); } as RepositoriesQueryParams)) as RepositoriesQueryResponse;
let data: IGitHubRepo[]; return {
if (response.data) { status: HttpStatusCodes.OK,
data = []; data: response.viewer.repositories.nodes.map(repo => GitHubClient.toGitHubRepo(repo)),
response.data?.forEach((element: any) => data.push(GitHubClient.toGitHubRepo(element))); pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo)
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getReposAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
};
} }
return { status: response.status, data };
} }
public async getBranchesAsync( public async getBranchesAsync(
owner: string, owner: string,
repo: string, repo: string,
page: number, pageSize: number,
perPage: number endCursor?: string
): Promise<IGitHubResponse<IGitHubBranch[]>> { ): Promise<IGitHubResponse<IGitHubBranch[]>> {
const response = await this.ocktokit.repos.listBranches({ try {
owner, const response = (await this.ocktokit.graphql(branchesQuery, {
repo, owner,
page, repo,
per_page: perPage, refPrefix: "refs/heads/",
headers: GitHubClient.getDisableCacheHeaders() pageSize,
}); endCursor
} as BranchesQueryParams)) as BranchesQueryResponse;
let data: IGitHubBranch[];
if (response.data) {
data = [];
response.data?.forEach(element => data.push(GitHubClient.toGitHubBranch(element)));
}
return { status: response.status, data };
}
public async getCommitsAsync(
owner: string,
repo: string,
branch: string,
path: string,
page: number,
perPage: number
): Promise<IGitHubResponse<IGitHubCommit[]>> {
if (GitHubClient.isSamplesCall(owner, repo, branch) && path === "" && page === 1 && perPage === 1) {
return { return {
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
data: [GitHubClient.samplesTopCommit] data: response.repository.refs.nodes.map(ref => GitHubClient.toGitHubBranch(ref)),
pageInfo: GitHubClient.toGitHubPageInfo(response.repository.refs.pageInfo)
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getBranchesAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
}; };
} }
const response = await this.ocktokit.repos.listCommits({
owner,
repo,
sha: branch,
path,
page,
per_page: perPage,
headers: GitHubClient.getDisableCacheHeaders()
});
let data: IGitHubCommit[];
if (response.data) {
data = [];
response.data?.forEach(element =>
data.push(GitHubClient.toGitHubCommit({ ...element.commit, sha: element.sha }))
);
}
return { status: response.status, data };
}
public async getDirContentsAsync(
owner: string,
repo: string,
branch: string,
path: string
): Promise<IGitHubResponse<IGitHubFile[]>> {
return (await this.getContentsAsync(owner, repo, branch, path)) as IGitHubResponse<IGitHubFile[]>;
}
public async getFileContentsAsync(
owner: string,
repo: string,
branch: string,
path: string
): Promise<IGitHubResponse<IGitHubFile>> {
return (await this.getContentsAsync(owner, repo, branch, path)) as IGitHubResponse<IGitHubFile>;
} }
public async getContentsAsync( public async getContentsAsync(
owner: string, owner: string,
repo: string, repo: string,
branch: string, branch: string,
path: string path?: string
): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> { ): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
if (GitHubClient.isSamplesCall(owner, repo, branch) && path === "") { try {
let response: ContentsQueryResponse;
if (isSamplesCall(owner, repo, branch) && !path) {
response = SamplesContentsQueryResponse;
} else {
response = (await this.ocktokit.graphql(contentsQuery, {
owner,
repo,
ref: `refs/heads/${branch}`,
path: path || undefined,
objectExpression: `refs/heads/${branch}:${path || ""}`
} as ContentsQueryParams)) as ContentsQueryResponse;
}
let data: IGitHubFile | IGitHubFile[];
const entries = response.repository.object.entries;
const gitHubRepo = GitHubClient.toGitHubRepo(response.repository);
const gitHubBranch = GitHubClient.toGitHubBranch(response.repository.ref);
const gitHubCommit = GitHubClient.toGitHubCommit(response.repository.ref.target.history.nodes[0]);
if (Array.isArray(entries)) {
data = entries.map(entry =>
GitHubClient.toGitHubFile(
entry,
(path && UrlUtility.createUri(path, entry.name)) || entry.name,
gitHubRepo,
gitHubBranch,
gitHubCommit
)
);
} else {
data = GitHubClient.toGitHubFile(
{
name: NotebookUtil.getName(path),
type: "blob",
object: response.repository.object
},
path,
gitHubRepo,
gitHubBranch,
gitHubCommit
);
}
return { return {
status: HttpStatusCodes.OK, status: HttpStatusCodes.OK,
data: GitHubClient.samplesFiles.map(file => data
GitHubClient.toGitHubFile(file, GitHubClient.samplesRepo, GitHubClient.samplesBranch) };
) } catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getContentsAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
}; };
} }
const response = await this.ocktokit.repos.getContents({
owner,
repo,
path,
ref: branch,
headers: GitHubClient.getDisableCacheHeaders()
});
let data: IGitHubFile | IGitHubFile[];
if (response.data) {
const repoResponse = await this.getRepoAsync(owner, repo);
if (repoResponse.data) {
const fileRepo: IGitHubRepo = GitHubClient.toGitHubRepo(repoResponse.data);
const fileBranch: IGitHubBranch = { name: branch };
if (Array.isArray(response.data)) {
const contents: IGitHubFile[] = [];
response.data.forEach((element: any) =>
contents.push(GitHubClient.toGitHubFile(element, fileRepo, fileBranch))
);
data = contents;
} else {
data = GitHubClient.toGitHubFile(
{ ...response.data, type: response.data.type as "file" | "dir" | "symlink" | "submodule" },
fileRepo,
fileBranch
);
}
}
}
return { status: response.status, data };
} }
public async createOrUpdateFileAsync( public async createOrUpdateFileAsync(
@@ -372,7 +406,9 @@ export class GitHubClient {
owner, owner,
repo, repo,
ref, ref,
headers: GitHubClient.getDisableCacheHeaders() headers: {
"If-None-Match": "" // disable 60s cache
}
}); });
const currentTree = await this.ocktokit.git.getTree({ const currentTree = await this.ocktokit.git.getTree({
@@ -380,7 +416,9 @@ export class GitHubClient {
repo, repo,
tree_sha: currentRef.data.object.sha, tree_sha: currentRef.data.object.sha,
recursive: "1", recursive: "1",
headers: GitHubClient.getDisableCacheHeaders() headers: {
"If-None-Match": "" // disable 60s cache
}
}); });
// API infers tree from paths so we need to filter them out // API infers tree from paths so we need to filter them out
@@ -425,7 +463,7 @@ export class GitHubClient {
public async deleteFileAsync(file: IGitHubFile, message: string): Promise<IGitHubResponse<IGitHubCommit>> { public async deleteFileAsync(file: IGitHubFile, message: string): Promise<IGitHubResponse<IGitHubCommit>> {
const response = await this.ocktokit.repos.deleteFile({ const response = await this.ocktokit.repos.deleteFile({
owner: file.repo.owner.login, owner: file.repo.owner,
repo: file.repo.name, repo: file.repo.name,
path: file.path, path: file.path,
message, message,
@@ -441,10 +479,31 @@ export class GitHubClient {
return { status: response.status, data }; return { status: response.status, data };
} }
private initOctokit(token: string) { public async getBlobAsync(owner: string, repo: string, sha: string): Promise<IGitHubResponse<string>> {
const response = await this.ocktokit.git.getBlob({
owner,
repo,
file_sha: sha,
mediaType: {
format: "raw"
},
headers: {
"If-None-Match": "" // disable 60s cache
}
});
return { status: response.status, data: <string>(<unknown>response.data) };
}
private async initOctokit(token: string) {
this.ocktokit = new Octokit({ this.ocktokit = new Octokit({
auth: token, auth: token,
baseUrl: GitHubClient.gitHubApiEndpoint log: {
debug: () => {},
info: (message?: any) => GitHubClient.log(Logger.logInfo, message),
warn: (message?: any) => GitHubClient.log(Logger.logWarning, message),
error: (message?: any) => GitHubClient.log(Logger.logError, message)
}
}); });
this.ocktokit.hook.error("request", error => { this.ocktokit.hook.error("request", error => {
@@ -453,53 +512,69 @@ export class GitHubClient {
}); });
} }
private static getDisableCacheHeaders(): RequestHeaders { private static log(logger: (message: string, area: string) => void, message?: any) {
if (message) {
message = typeof message === "string" ? message : JSON.stringify(message);
logger(message, "GitHubClient.Octokit");
}
}
private static toGitHubRepo(object: Repository): IGitHubRepo {
return { return {
"If-None-Match": "" owner: object.owner.login,
name: object.name,
private: object.isPrivate
}; };
} }
private static toGitHubRepo(element: IGitHubRepo): IGitHubRepo { private static toGitHubBranch(object: Ref): IGitHubBranch {
return { return {
name: element.name, name: object.name
owner: {
login: element.owner.login
},
private: element.private
}; };
} }
private static toGitHubBranch(element: IGitHubBranch): IGitHubBranch { private static toGitHubCommit(object: {
message: string;
committer: {
date: string;
};
sha?: string;
oid?: string;
}): IGitHubCommit {
return { return {
name: element.name sha: object.sha || object.oid,
message: object.message,
commitDate: object.committer.date
}; };
} }
private static toGitHubCommit(element: IGitHubCommit): IGitHubCommit { private static toGitHubPageInfo(object: PageInfo): IGitHubPageInfo {
return { return {
sha: element.sha, endCursor: object.endCursor,
message: element.message, hasNextPage: object.hasNextPage
committer: {
date: element.committer.date
}
}; };
} }
private static toGitHubFile(element: IGitHubFile, repo: IGitHubRepo, branch: IGitHubBranch): IGitHubFile { private static toGitHubFile(
entry: TreeEntry,
path: string,
repo: IGitHubRepo,
branch: IGitHubBranch,
commit: IGitHubCommit
): IGitHubFile {
if (entry.type !== "blob" && entry.type !== "tree") {
throw new Error(`Unsupported file type: ${entry.type}`);
}
return { return {
type: element.type, type: entry.type,
encoding: element.encoding, name: entry.name,
size: element.size, path,
name: element.name,
path: element.path,
content: element.content,
sha: element.sha,
repo, repo,
branch branch,
commit,
size: entry.object?.byteSize,
sha: entry.object?.oid
}; };
} }
private static isSamplesCall(owner: string, repo: string, branch?: string): boolean {
return owner === "Azure-Samples" && repo === "cosmos-notebooks" && (!branch || branch === "master");
}
} }

View File

@@ -10,27 +10,30 @@ const gitHubContentProvider = new GitHubContentProvider({
gitHubClient, gitHubClient,
promptForCommitMsg: () => Promise.resolve("commit msg") promptForCommitMsg: () => Promise.resolve("commit msg")
}); });
const gitHubCommit: IGitHubCommit = {
sha: "sha",
message: "message",
commitDate: "date"
};
const sampleFile: IGitHubFile = { const sampleFile: IGitHubFile = {
type: "file", type: "blob",
encoding: "encoding",
size: 0, size: 0,
name: "name.ipynb", name: "name.ipynb",
path: "dir/name.ipynb", path: "dir/name.ipynb",
content: btoa(fixture), content: fixture,
sha: "sha", sha: "sha",
repo: { repo: {
owner: { owner: "owner",
login: "login"
},
name: "repo", name: "repo",
private: false private: false
}, },
branch: { branch: {
name: "branch" name: "branch"
} },
commit: gitHubCommit
}; };
const sampleGitHubUri = GitHubUtils.toContentUri( const sampleGitHubUri = GitHubUtils.toContentUri(
sampleFile.repo.owner.login, sampleFile.repo.owner,
sampleFile.repo.name, sampleFile.repo.name,
sampleFile.branch.name, sampleFile.branch.name,
sampleFile.path sampleFile.path
@@ -43,16 +46,9 @@ const sampleNotebookModel: IContent<"notebook"> = {
created: "", created: "",
last_modified: "date", last_modified: "date",
mimetype: "application/x-ipynb+json", mimetype: "application/x-ipynb+json",
content: sampleFile.content ? JSON.parse(atob(sampleFile.content)) : null, content: sampleFile.content ? JSON.parse(sampleFile.content) : null,
format: "json" format: "json"
}; };
const gitHubCommit: IGitHubCommit = {
sha: "sha",
message: "message",
committer: {
date: "date"
}
};
describe("GitHubContentProvider remove", () => { describe("GitHubContentProvider remove", () => {
it("errors on invalid path", async () => { it("errors on invalid path", async () => {
@@ -125,9 +121,6 @@ describe("GitHubContentProvider get", () => {
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue( spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile }) Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
); );
spyOn(GitHubClient.prototype, "getCommitsAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: [gitHubCommit] })
);
const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise(); const response = await gitHubContentProvider.get(null, sampleGitHubUri, {}).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
@@ -176,9 +169,6 @@ describe("GitHubContentProvider update", () => {
spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue( spyOn(GitHubClient.prototype, "renameFileAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit }) Promise.resolve({ status: HttpStatusCodes.OK, data: gitHubCommit })
); );
spyOn(GitHubClient.prototype, "getCommitsAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: [gitHubCommit] })
);
const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.update(null, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
@@ -215,18 +205,14 @@ describe("GitHubContentProvider create", () => {
spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue( spyOn(GitHubClient.prototype, "createOrUpdateFileAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit }) Promise.resolve({ status: HttpStatusCodes.Created, data: gitHubCommit })
); );
spyOn(GitHubClient.prototype, "getContentsAsync").and.returnValue(
Promise.resolve({ status: HttpStatusCodes.OK, data: sampleFile })
);
const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise(); const response = await gitHubContentProvider.create(null, sampleGitHubUri, sampleNotebookModel).toPromise();
expect(response).toBeDefined(); expect(response).toBeDefined();
expect(response.status).toBe(HttpStatusCodes.Created); expect(response.status).toBe(HttpStatusCodes.Created);
expect(gitHubClient.createOrUpdateFileAsync).toBeCalled(); expect(gitHubClient.createOrUpdateFileAsync).toBeCalled();
expect(gitHubClient.getContentsAsync).toBeCalled();
expect(response.response.type).toEqual(sampleNotebookModel.type); expect(response.response.type).toEqual(sampleNotebookModel.type);
expect(response.response.name).toEqual(sampleNotebookModel.name); expect(response.response.name).toBeDefined();
expect(response.response.path).toEqual(sampleNotebookModel.path); expect(response.response.path).toBeDefined();
expect(response.response.content).toBeUndefined(); expect(response.response.content).toBeUndefined();
}); });
}); });

View File

@@ -5,7 +5,7 @@ import { AjaxResponse } from "rxjs/ajax";
import { HttpStatusCodes } from "../Common/Constants"; import { HttpStatusCodes } from "../Common/Constants";
import { Logger } from "../Common/Logger"; import { Logger } from "../Common/Logger";
import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil"; import { NotebookUtil } from "../Explorer/Notebook/NotebookUtil";
import { GitHubClient, IGitHubFile, IGitHubResponse, IGitHubCommit } from "./GitHubClient"; import { GitHubClient, IGitHubFile, IGitHubResponse, IGitHubCommit, IGitHubBranch } from "./GitHubClient";
import { GitHubUtils } from "../Utils/GitHubUtils"; import { GitHubUtils } from "../Utils/GitHubUtils";
import UrlUtility from "../Common/UrlUtility"; import UrlUtility from "../Common/UrlUtility";
@@ -54,23 +54,14 @@ export class GitHubContentProvider implements IContentProvider {
throw new GitHubContentProviderError("Failed to get content", content.status); throw new GitHubContentProviderError("Failed to get content", content.status);
} }
const contentInfo = GitHubUtils.fromContentUri(uri); if (!Array.isArray(content.data) && !content.data.content && params.content !== 0) {
const commitResponse = await this.params.gitHubClient.getCommitsAsync( const file = content.data;
contentInfo.owner, file.content = (
contentInfo.repo, await this.params.gitHubClient.getBlobAsync(file.repo.owner, file.repo.name, file.sha)
contentInfo.branch, ).data;
contentInfo.path,
1,
1
);
if (commitResponse.status !== HttpStatusCodes.OK) {
throw new GitHubContentProviderError("Failed to get commit", commitResponse.status);
} }
return this.createSuccessAjaxResponse( return this.createSuccessAjaxResponse(HttpStatusCodes.OK, this.createContentModel(uri, content.data, params));
HttpStatusCodes.OK,
this.createContentModel(uri, content.data, commitResponse.data[0], params)
);
} catch (error) { } catch (error) {
Logger.logError(error, "GitHubContentProvider/get", error.errno); Logger.logError(error, "GitHubContentProvider/get", error.errno);
return this.createErrorAjaxResponse(error); return this.createErrorAjaxResponse(error);
@@ -90,26 +81,26 @@ export class GitHubContentProvider implements IContentProvider {
const gitHubFile = content.data as IGitHubFile; const gitHubFile = content.data as IGitHubFile;
const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename"); const commitMsg = await this.validateContentAndGetCommitMsg(content, "Rename", "Rename");
const newUri = model.path; const newUri = model.path;
const newPath = GitHubUtils.fromContentUri(newUri).path;
const response = await this.params.gitHubClient.renameFileAsync( const response = await this.params.gitHubClient.renameFileAsync(
gitHubFile.repo.owner.login, gitHubFile.repo.owner,
gitHubFile.repo.name, gitHubFile.repo.name,
gitHubFile.branch.name, gitHubFile.branch.name,
commitMsg, commitMsg,
gitHubFile.path, gitHubFile.path,
GitHubUtils.fromContentUri(newUri).path newPath
); );
if (response.status !== HttpStatusCodes.OK) { if (response.status !== HttpStatusCodes.OK) {
throw new GitHubContentProviderError("Failed to rename", response.status); throw new GitHubContentProviderError("Failed to rename", response.status);
} }
const updatedContentResponse = await this.getContent(model.path); gitHubFile.commit = response.data;
if (updatedContentResponse.status !== HttpStatusCodes.OK) { gitHubFile.path = newPath;
throw new GitHubContentProviderError("Failed to get content after renaming", updatedContentResponse.status); gitHubFile.name = NotebookUtil.getName(gitHubFile.path);
}
return this.createSuccessAjaxResponse( return this.createSuccessAjaxResponse(
HttpStatusCodes.OK, HttpStatusCodes.OK,
this.createContentModel(newUri, updatedContentResponse.data, response.data, { content: 0 }) this.createContentModel(newUri, gitHubFile, { content: 0 })
); );
} catch (error) { } catch (error) {
Logger.logError(error, "GitHubContentProvider/update", error.errno); Logger.logError(error, "GitHubContentProvider/update", error.errno);
@@ -169,14 +160,24 @@ export class GitHubContentProvider implements IContentProvider {
} }
const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path); const newUri = GitHubUtils.toContentUri(contentInfo.owner, contentInfo.repo, contentInfo.branch, path);
const newContentResponse = await this.getContent(newUri); const newGitHubFile: IGitHubFile = {
if (newContentResponse.status !== HttpStatusCodes.OK) { type: "blob",
throw new GitHubContentProviderError("Failed to get content after creating", newContentResponse.status); name: NotebookUtil.getName(newUri),
} path,
repo: {
owner: contentInfo.owner,
name: contentInfo.repo,
private: undefined
},
branch: {
name: contentInfo.branch
},
commit: response.data
};
return this.createSuccessAjaxResponse( return this.createSuccessAjaxResponse(
HttpStatusCodes.Created, HttpStatusCodes.Created,
this.createContentModel(newUri, newContentResponse.data, response.data, { content: 0 }) this.createContentModel(newUri, newGitHubFile, { content: 0 })
); );
} catch (error) { } catch (error) {
Logger.logError(error, "GitHubContentProvider/create", error.errno); Logger.logError(error, "GitHubContentProvider/create", error.errno);
@@ -209,7 +210,7 @@ export class GitHubContentProvider implements IContentProvider {
const gitHubFile = content.data as IGitHubFile; const gitHubFile = content.data as IGitHubFile;
const response = await this.params.gitHubClient.createOrUpdateFileAsync( const response = await this.params.gitHubClient.createOrUpdateFileAsync(
gitHubFile.repo.owner.login, gitHubFile.repo.owner,
gitHubFile.repo.name, gitHubFile.repo.name,
gitHubFile.branch.name, gitHubFile.branch.name,
gitHubFile.path, gitHubFile.path,
@@ -221,14 +222,11 @@ export class GitHubContentProvider implements IContentProvider {
throw new GitHubContentProviderError("Failed to update", response.status); throw new GitHubContentProviderError("Failed to update", response.status);
} }
const savedContentResponse = await this.getContent(uri); gitHubFile.commit = response.data;
if (savedContentResponse.status !== HttpStatusCodes.OK) {
throw new GitHubContentProviderError("Failed to get content after updating", savedContentResponse.status);
}
return this.createSuccessAjaxResponse( return this.createSuccessAjaxResponse(
HttpStatusCodes.OK, HttpStatusCodes.OK,
this.createContentModel(uri, savedContentResponse.data, response.data, { content: 0 }) this.createContentModel(uri, gitHubFile, { content: 0 })
); );
} catch (error) { } catch (error) {
Logger.logError(error, "GitHubContentProvider/update", error.errno); Logger.logError(error, "GitHubContentProvider/update", error.errno);
@@ -283,7 +281,7 @@ export class GitHubContentProvider implements IContentProvider {
return commitMsg; return commitMsg;
} }
private getContent(uri: string): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> { private async getContent(uri: string): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
const contentInfo = GitHubUtils.fromContentUri(uri); const contentInfo = GitHubUtils.fromContentUri(uri);
if (contentInfo) { if (contentInfo) {
const { owner, repo, branch, path } = contentInfo; const { owner, repo, branch, path } = contentInfo;
@@ -296,43 +294,37 @@ export class GitHubContentProvider implements IContentProvider {
private createContentModel( private createContentModel(
uri: string, uri: string,
content: IGitHubFile | IGitHubFile[], content: IGitHubFile | IGitHubFile[],
commit: IGitHubCommit,
params: Partial<IGetParams> params: Partial<IGetParams>
): IContent<FileType> { ): IContent<FileType> {
if (Array.isArray(content)) { if (Array.isArray(content)) {
return this.createDirectoryModel(uri, content, commit); return this.createDirectoryModel(uri, content);
} }
if (content.type !== "file") { if (content.type === "tree") {
return this.createDirectoryModel(uri, undefined, commit); return this.createDirectoryModel(uri, undefined);
} }
if (NotebookUtil.isNotebookFile(uri)) { if (NotebookUtil.isNotebookFile(uri)) {
return this.createNotebookModel(content, commit, params); return this.createNotebookModel(content, params);
} }
return this.createFileModel(content, commit, params); return this.createFileModel(content, params);
} }
private createDirectoryModel( private createDirectoryModel(uri: string, gitHubFiles: IGitHubFile[] | undefined): IContent<"directory"> {
uri: string,
gitHubFiles: IGitHubFile[] | undefined,
commit: IGitHubCommit
): IContent<"directory"> {
return { return {
name: GitHubUtils.fromContentUri(uri).path, name: NotebookUtil.getName(uri),
path: uri, path: uri,
type: "directory", type: "directory",
writable: true, // TODO: tamitta: we don't know this info here writable: true, // TODO: tamitta: we don't know this info here
created: "", // TODO: tamitta: we don't know this info here created: "", // TODO: tamitta: we don't know this info here
last_modified: commit.committer.date, last_modified: "", // TODO: tamitta: we don't know this info here
mimetype: undefined, mimetype: undefined,
content: gitHubFiles?.map( content: gitHubFiles?.map(
(file: IGitHubFile) => (file: IGitHubFile) =>
this.createContentModel( this.createContentModel(
GitHubUtils.toContentUri(file.repo.owner.login, file.repo.name, file.branch.name, file.path), GitHubUtils.toContentUri(file.repo.owner, file.repo.name, file.branch.name, file.path),
file, file,
commit,
{ {
content: 0 content: 0
} }
@@ -342,17 +334,12 @@ export class GitHubContentProvider implements IContentProvider {
}; };
} }
private createNotebookModel( private createNotebookModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"notebook"> {
gitHubFile: IGitHubFile, const content: Notebook = gitHubFile.content && params.content !== 0 ? JSON.parse(gitHubFile.content) : undefined;
commit: IGitHubCommit,
params: Partial<IGetParams>
): IContent<"notebook"> {
const content: Notebook =
gitHubFile.content && params.content !== 0 ? JSON.parse(atob(gitHubFile.content)) : undefined;
return { return {
name: gitHubFile.name, name: gitHubFile.name,
path: GitHubUtils.toContentUri( path: GitHubUtils.toContentUri(
gitHubFile.repo.owner.login, gitHubFile.repo.owner,
gitHubFile.repo.name, gitHubFile.repo.name,
gitHubFile.branch.name, gitHubFile.branch.name,
gitHubFile.path gitHubFile.path
@@ -360,23 +347,19 @@ export class GitHubContentProvider implements IContentProvider {
type: "notebook", type: "notebook",
writable: true, // TODO: tamitta: we don't know this info here writable: true, // TODO: tamitta: we don't know this info here
created: "", // TODO: tamitta: we don't know this info here created: "", // TODO: tamitta: we don't know this info here
last_modified: commit.committer.date, last_modified: gitHubFile.commit.commitDate,
mimetype: content ? "application/x-ipynb+json" : undefined, mimetype: content ? "application/x-ipynb+json" : undefined,
content, content,
format: content ? "json" : undefined format: content ? "json" : undefined
}; };
} }
private createFileModel( private createFileModel(gitHubFile: IGitHubFile, params: Partial<IGetParams>): IContent<"file"> {
gitHubFile: IGitHubFile, const content: string = gitHubFile.content && params.content !== 0 ? gitHubFile.content : undefined;
commit: IGitHubCommit,
params: Partial<IGetParams>
): IContent<"file"> {
const content: string = gitHubFile.content && params.content !== 0 ? atob(gitHubFile.content) : undefined;
return { return {
name: gitHubFile.name, name: gitHubFile.name,
path: GitHubUtils.toContentUri( path: GitHubUtils.toContentUri(
gitHubFile.repo.owner.login, gitHubFile.repo.owner,
gitHubFile.repo.name, gitHubFile.repo.name,
gitHubFile.branch.name, gitHubFile.branch.name,
gitHubFile.path gitHubFile.path
@@ -384,7 +367,7 @@ export class GitHubContentProvider implements IContentProvider {
type: "file", type: "file",
writable: true, // TODO: tamitta: we don't know this info here writable: true, // TODO: tamitta: we don't know this info here
created: "", // TODO: tamitta: we don't know this info here created: "", // TODO: tamitta: we don't know this info here
last_modified: commit.committer.date, last_modified: gitHubFile.commit.commitDate,
mimetype: content ? "text/plain" : undefined, mimetype: content ? "text/plain" : undefined,
content, content,
format: content ? "text" : undefined format: content ? "text" : undefined

View File

@@ -713,7 +713,7 @@ class HostedExplorer {
? storedDefaultTenantId.substring(DefaultDirectoryDropdownComponent.lastVisitedKey.length) ? storedDefaultTenantId.substring(DefaultDirectoryDropdownComponent.lastVisitedKey.length)
: storedDefaultTenantId; : storedDefaultTenantId;
let defaultTenant: Tenant = tenants.find(t => t.tenantId === storedDefaultTenantId); let defaultTenant: Tenant = _.find(tenants, t => t.tenantId === storedDefaultTenantId);
if (!defaultTenant) { if (!defaultTenant) {
defaultTenant = tenants[0]; defaultTenant = tenants[0];
LocalStorageUtility.setEntryString( LocalStorageUtility.setEntryString(
@@ -830,7 +830,7 @@ class HostedExplorer {
const storedAccountId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId); const storedAccountId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId);
const storedSubId = storedAccountId && storedAccountId.split("subscriptions/")[1].split("/")[0]; const storedSubId = storedAccountId && storedAccountId.split("subscriptions/")[1].split("/")[0];
let defaultSub = subscriptions.find(s => s.subscriptionId === storedSubId); let defaultSub = _.find(subscriptions, s => s.subscriptionId === storedSubId);
if (!defaultSub) { if (!defaultSub) {
defaultSub = subscriptions[0]; defaultSub = subscriptions[0];
} }
@@ -932,7 +932,7 @@ class HostedExplorer {
} }
let storedDefaultAccountId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId); let storedDefaultAccountId = LocalStorageUtility.getEntryString(StorageKey.DatabaseAccountId);
let defaultAccount = accounts.find(a => a.id === storedDefaultAccountId); let defaultAccount = _.find(accounts, a => a.id === storedDefaultAccountId);
if (!defaultAccount) { if (!defaultAccount) {
defaultAccount = accounts[0]; defaultAccount = accounts[0];

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

@@ -86,6 +86,7 @@ export enum Action {
CreateNewNotebook, CreateNewNotebook,
OpenSampleNotebook, OpenSampleNotebook,
ExecuteCell, ExecuteCell,
ExecuteCellPromptBtn,
ExecuteAllCells, ExecuteAllCells,
NotebookEnabled, NotebookEnabled,
NotebooksGitHubConnect, NotebooksGitHubConnect,

View File

@@ -7,6 +7,10 @@ export class GitHubUtils {
// Custom scheme for github content // Custom scheme for github content
private static readonly ContentUriPattern = /github:\/\/([^/]*)\/([^/]*)\/([^?]*)\?ref=(.*)/; private static readonly ContentUriPattern = /github:\/\/([^/]*)\/([^/]*)\/([^?]*)\?ref=(.*)/;
// https://github.com/<owner>/<repo>/blob/<branch>/<path>
// We need to support this until we move to newer scheme for quickstarts
private static readonly LegacyContentUriPattern = /https:\/\/github.com\/([^/]*)\/([^/]*)\/blob\/([^/]*)\/([^?]*)/;
public static toRepoFullName(owner: string, repo: string): string { public static toRepoFullName(owner: string, repo: string): string {
return `${owner}/${repo}`; return `${owner}/${repo}`;
} }
@@ -27,7 +31,7 @@ export class GitHubUtils {
public static fromContentUri( public static fromContentUri(
contentUri: string contentUri: string
): undefined | { owner: string; repo: string; branch: string; path: string } { ): undefined | { owner: string; repo: string; branch: string; path: string } {
const matches = contentUri.match(GitHubUtils.ContentUriPattern); let matches = contentUri.match(GitHubUtils.ContentUriPattern);
if (matches && matches.length > 4) { if (matches && matches.length > 4) {
return { return {
owner: matches[1], owner: matches[1],
@@ -37,6 +41,18 @@ export class GitHubUtils {
}; };
} }
matches = contentUri.match(GitHubUtils.LegacyContentUriPattern);
if (matches && matches.length > 4) {
console.log(`Using legacy github content uri scheme ${contentUri}`);
return {
owner: matches[1],
repo: matches[2],
branch: matches[3],
path: matches[4]
};
}
return undefined; return undefined;
} }

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,19 +40,21 @@ 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 {
return { return {
owner: item.repo.owner.login, owner: item.repo.owner,
name: item.repo.name, name: item.repo.name,
private: item.repo.private, private: item.repo.private,
branches: item.branches.map(element => ({ name: element.name })) branches: item.branches.map(element => ({ name: element.name }))
@@ -56,9 +63,7 @@ export class JunoUtils {
public static toGitHubRepo(pinnedRepo: IPinnedRepo): IGitHubRepo { public static toGitHubRepo(pinnedRepo: IPinnedRepo): IGitHubRepo {
return { return {
owner: { owner: pinnedRepo.owner,
login: pinnedRepo.owner
},
name: pinnedRepo.name, name: pinnedRepo.name,
private: pinnedRepo.private private: pinnedRepo.private
}; };

View File

@@ -1,5 +1,5 @@
import { DatabaseAccount } from "../../Contracts/DataModels"; import { DatabaseAccount } from "../../Contracts/DataModels";
import { PlatformType } from "../../PlatformType"; import { Platform } from "../../Config";
export interface StartUploadMessageParams { export interface StartUploadMessageParams {
files: FileList; files: FileList;
@@ -12,7 +12,7 @@ export interface DocumentClientParams {
masterKey: string; masterKey: string;
endpoint: string; endpoint: string;
accessToken: string; accessToken: string;
platform: PlatformType; platform: Platform;
databaseAccount: DatabaseAccount; databaseAccount: DatabaseAccount;
} }

View File

@@ -1,6 +1,7 @@
import "babel-polyfill"; import "babel-polyfill";
import { DocumentClientParams, UploadDetailsRecord, UploadDetails } from "./definitions"; import { DocumentClientParams, UploadDetailsRecord, UploadDetails } from "./definitions";
import { CosmosClient } from "../../Common/CosmosClient"; import { CosmosClient } from "../../Common/CosmosClient";
import { config } from "../../Config";
let numUploadsSuccessful = 0; let numUploadsSuccessful = 0;
let numUploadsFailed = 0; let numUploadsFailed = 0;
@@ -33,8 +34,7 @@ onmessage = (event: MessageEvent) => {
CosmosClient.endpoint(clientParams.endpoint); CosmosClient.endpoint(clientParams.endpoint);
CosmosClient.accessToken(clientParams.accessToken); CosmosClient.accessToken(clientParams.accessToken);
CosmosClient.databaseAccount(clientParams.databaseAccount); CosmosClient.databaseAccount(clientParams.databaseAccount);
self.dataExplorerPlatform = clientParams.platform; config.platform = clientParams.platform;
console.log(event);
if (!!files && files.length > 0) { if (!!files && files.length > 0) {
numFiles = files.length; numFiles = files.length;
for (let i = 0; i < numFiles; i++) { for (let i = 0; i < numFiles; i++) {
@@ -106,6 +106,7 @@ function createDocumentsFromFile(fileName: string, documentContent: string): voi
triggerCreateDocument(content); triggerCreateDocument(content);
} }
} catch (e) { } catch (e) {
console.log(e);
recordUploadDetailErrorForFile(fileName, e.message); recordUploadDetailErrorForFile(fileName, e.message);
transmitResultIfUploadComplete(); transmitResultIfUploadComplete();
} }

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"
}, },