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 {
@@ -20,7 +20,7 @@
display: "inline-block"; display: "inline-block";
margin: 10px; margin: 10px;
} }
.active, .downloadButton:hover { .active, .downloadButton:hover {
color: @BaseMedium; color: @BaseMedium;
} }

View File

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

View File

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

View File

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

View File

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

View File

@@ -1143,7 +1143,7 @@ export default class Explorer implements ViewModels.Explorer {
isModal: true, isModal: true,
visible: true, visible: true,
title: `Enable Azure Synapse Link on your Cosmos DB account`, title: `Enable Azure Synapse Link on your Cosmos DB account`,
subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads. subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads.
Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`, Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`,
primaryButtonText: "Enable Azure Synapse Link", primaryButtonText: "Enable Azure Synapse Link",
secondaryButtonText: "Cancel", secondaryButtonText: "Cancel",
@@ -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"
}, },