Compare commits

..

1 Commits

Author SHA1 Message Date
Steve Faulkner
82f7e0d4d1 Initial yaml change 2020-06-01 18:27:33 -05:00
61 changed files with 1235 additions and 1671 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.tsx
src/Explorer/Controls/NotebookViewer/NotebookMetadataComponent.tsx
src/NotebookViewer/NotebookViewer.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewer.tsx
src/Explorer/Controls/NotebookViewer/NotebookViewerComponent.tsx
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponent.tsx
src/Explorer/Controls/QueriesGridReactComponent/QueriesGridComponentAdapter.tsx

View File

@@ -67,6 +67,32 @@ jobs:
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
endtoendprodcassandra:
name: "End to End Tests Prod"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: 12.x
- name: Restore Cypress Binary Cache
uses: actions/cache@v2
with:
path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress-binary-cache
- name: End to End Tests
run: |
npm ci
npm start &
npm ci --prefix ./cypress
npm run test --prefix ./cypress
shell: bash
env:
EMULATOR_ENDPOINT: https://0.0.0.0:8081/
NODE_TLS_REJECT_UNAUTHORIZED: 0
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CONNECTION_STRING: ${{ secrets.CASSANDRA_CONNECTION_STRING }}
nuget:
name: Publish Nuget
needs: [build, test, endtoend]

324
package-lock.json generated
View File

@@ -5,9 +5,9 @@
"requires": true,
"dependencies": {
"@azure/cosmos": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.7.0.tgz",
"integrity": "sha512-3SRxnmy6NncdX5eYqGuRTack52hloS9YhQ0aOKwWJ8Z4dDSrVH3XB2Mcp/WokoIpVm0Bq5nUC8FsvLBZKfRkyg==",
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/@azure/cosmos/-/cosmos-3.6.3.tgz",
"integrity": "sha512-JoCDxl0TnL6EHL4xD3KC9r2bMivK13q1jl7h69wd/YFLlt3aBTTCehtAX+y4alNSENpL53XdRdw/cna0mI2XDw==",
"requires": {
"@types/debug": "^4.1.4",
"debug": "^4.1.1",
@@ -18,14 +18,7 @@
"priorityqueuejs": "^1.0.0",
"semaphore": "^1.0.5",
"tslib": "^1.10.0",
"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=="
}
"uuid": "^3.3.2"
}
},
"@azure/cosmos-language-service": {
@@ -1166,6 +1159,11 @@
"minimist": "^1.2.0"
}
},
"@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="
},
"@emotion/is-prop-valid": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz",
@@ -1606,6 +1604,96 @@
}
}
},
"@material-ui/core": {
"version": "4.9.10",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-4.9.10.tgz",
"integrity": "sha512-CQuZU9Y10RkwSdxjn785kw2EPcXhv5GKauuVQufR9LlD37kjfn21Im1yvr6wsUzn81oLhEvVPz727UWC0gbqxg==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/styles": "^4.9.10",
"@material-ui/system": "^4.9.10",
"@material-ui/types": "^5.0.1",
"@material-ui/utils": "^4.9.6",
"@types/react-transition-group": "^4.2.0",
"clsx": "^1.0.4",
"hoist-non-react-statics": "^3.3.2",
"popper.js": "^1.16.1-lts",
"prop-types": "^15.7.2",
"react-is": "^16.8.0",
"react-transition-group": "^4.3.0"
},
"dependencies": {
"dom-helpers": {
"version": "5.1.4",
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.1.4.tgz",
"integrity": "sha512-TjMyeVUvNEnOnhzs6uAn9Ya47GmMo3qq7m+Lr/3ON0Rs5kHvb8I+SQYjLUSYn7qhEm0QjW0yrBkvz9yOrwwz1A==",
"requires": {
"@babel/runtime": "^7.8.7",
"csstype": "^2.6.7"
}
},
"react-transition-group": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.1.tgz",
"integrity": "sha512-Djqr7OQ2aPUiYurhPalTrVy9ddmFCCzwhqQmtN+J3+3DzLO209Fdr70QrN8Z3DsglWql6iY1lDWAfpFiBtuKGw==",
"requires": {
"@babel/runtime": "^7.5.5",
"dom-helpers": "^5.0.1",
"loose-envify": "^1.4.0",
"prop-types": "^15.6.2"
}
}
}
},
"@material-ui/styles": {
"version": "4.9.14",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.9.14.tgz",
"integrity": "sha512-zecwWKgRU2VzdmutNovPB4s5LKI0TWyZKc/AHfPu9iY8tg4UoLjpa4Rn9roYrRfuTbBZHI6b0BXcQ8zkis0nzQ==",
"requires": {
"@babel/runtime": "^7.4.4",
"@emotion/hash": "^0.8.0",
"@material-ui/types": "^5.1.0",
"@material-ui/utils": "^4.9.6",
"clsx": "^1.0.4",
"csstype": "^2.5.2",
"hoist-non-react-statics": "^3.3.2",
"jss": "^10.0.3",
"jss-plugin-camel-case": "^10.0.3",
"jss-plugin-default-unit": "^10.0.3",
"jss-plugin-global": "^10.0.3",
"jss-plugin-nested": "^10.0.3",
"jss-plugin-props-sort": "^10.0.3",
"jss-plugin-rule-value-function": "^10.0.3",
"jss-plugin-vendor-prefixer": "^10.0.3",
"prop-types": "^15.7.2"
}
},
"@material-ui/system": {
"version": "4.9.14",
"resolved": "https://registry.npmjs.org/@material-ui/system/-/system-4.9.14.tgz",
"integrity": "sha512-oQbaqfSnNlEkXEziDcJDDIy8pbvwUmZXWNqlmIwDqr/ZdCK8FuV3f4nxikUh7hvClKV2gnQ9djh5CZFTHkZj3w==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.9.6",
"csstype": "^2.5.2",
"prop-types": "^15.7.2"
}
},
"@material-ui/types": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/@material-ui/types/-/types-5.1.0.tgz",
"integrity": "sha512-7cqRjrY50b8QzRSYyhSpx4WRw2YuO0KKIGQEVk5J8uoz2BanawykgZGoWEqKm7pVIbzFDN0SpPcVV4IhOFkl8A=="
},
"@material-ui/utils": {
"version": "4.9.12",
"resolved": "https://registry.npmjs.org/@material-ui/utils/-/utils-4.9.12.tgz",
"integrity": "sha512-/0rgZPEOcZq5CFA4+4n6Q6zk7fi8skHhH2Bcra8R3epoJEYy5PL55LuMazPtPH1oKeRausDV/Omz4BbgFsn1HQ==",
"requires": {
"@babel/runtime": "^7.4.4",
"prop-types": "^15.7.2",
"react-is": "^16.8.0"
}
},
"@microsoft/applicationinsights-analytics-js": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/@microsoft/applicationinsights-analytics-js/-/applicationinsights-analytics-js-2.5.4.tgz",
@@ -2429,42 +2517,32 @@
}
},
"@octokit/auth-token": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.1.tgz",
"integrity": "sha512-NB81O5h39KfHYGtgfWr2booRxp2bWOJoqbWwbyUg2hw6h35ArWYlAST5B3XwAkbdcx13yt84hFXyFP5X0QToWA==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.0.tgz",
"integrity": "sha512-eoOVMjILna7FVQf96iWc3+ZtE/ZT6y8ob8ZzcqKY1ibSQCnu4O/B7pJvzMx5cyZ/RjAff6DAdEb0O0Cjcxidkg==",
"requires": {
"@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/types": "^2.0.0"
}
},
"@octokit/core": {
"version": "2.5.3",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-2.5.3.tgz",
"integrity": "sha512-23AHK9xBW0v79Ck8h5U+5iA4MW7aosqv+Yr6uZXolVGNzzHwryNH5wM386/6+etiKUTwLFZTqyMU9oQpIBZcFA==",
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@octokit/core/-/core-2.5.0.tgz",
"integrity": "sha512-uvzmkemQrBgD8xuGbjhxzJN1darJk9L2cS+M99cHrDG2jlSVpxNJVhoV86cXdYBqdHCc9Z995uLCczaaHIYA6Q==",
"requires": {
"@octokit/auth-token": "^2.4.0",
"@octokit/graphql": "^4.3.1",
"@octokit/request": "^5.4.0",
"@octokit/types": "^4.0.1",
"@octokit/types": "^2.0.0",
"before-after-hook": "^2.1.0",
"universal-user-agent": "^5.0.0"
}
},
"@octokit/endpoint": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.2.tgz",
"integrity": "sha512-xs1mmCEZ2y4shXCpFjNq3UbmNR+bLzxtZim2L0zfEtj9R6O6kc4qLDvYw66hvO6lUsYzPTM5hMkltbuNAbRAcQ==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.1.tgz",
"integrity": "sha512-pOPHaSz57SFT/m3R5P8MUu4wLPszokn5pXcB/pzavLTQf2jbU+6iayTvzaY6/BiotuRS0qyEUkx3QglT4U958A==",
"requires": {
"@octokit/types": "^4.0.1",
"@octokit/types": "^2.11.1",
"is-plain-object": "^3.0.0",
"universal-user-agent": "^5.0.0"
},
@@ -2485,21 +2563,21 @@
}
},
"@octokit/graphql": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.0.tgz",
"integrity": "sha512-StJWfn0M1QfhL3NKBz31e1TdDNZrHLLS57J2hin92SIfzlOVBuUaRkp31AGkGOAFOAVtyEX6ZiZcsjcJDjeb5g==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.4.0.tgz",
"integrity": "sha512-Du3hAaSROQ8EatmYoSAJjzAz3t79t9Opj/WY1zUgxVUGfIKn0AEjg+hlOLscF6fv6i/4y/CeUvsWgIfwMkTccw==",
"requires": {
"@octokit/request": "^5.3.0",
"@octokit/types": "^4.0.1",
"@octokit/types": "^2.0.0",
"universal-user-agent": "^5.0.0"
}
},
"@octokit/plugin-paginate-rest": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.1.tgz",
"integrity": "sha512-/tHpIF2XpN40AyhIq295YRjb4g7Q5eKob0qM3thYJ0Z+CgmNsWKM/fWse/SUR8+LdprP1O4ZzSKQE+71TCwK+w==",
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.2.0.tgz",
"integrity": "sha512-KoNxC3PLNar8UJwR+1VMQOw2IoOrrFdo5YOiDKnBhpVbKpw+zkBKNMNKwM44UWL25Vkn0Sl3nYIEGKY+gW5ebw==",
"requires": {
"@octokit/types": "^4.0.1"
"@octokit/types": "^2.12.1"
}
},
"@octokit/plugin-request-log": {
@@ -2508,22 +2586,22 @@
"integrity": "sha512-ywoxP68aOT3zHCLgWZgwUJatiENeHE7xJzYjfz8WI0goynp96wETBF+d95b8g/uL4QmS6owPVlaxiz3wyMAzcw=="
},
"@octokit/plugin-rest-endpoint-methods": {
"version": "3.12.3",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-3.12.3.tgz",
"integrity": "sha512-9nrVDP1tBd7EtobGr5hZcYGTM0kBNmIvPJazrUd5OJO0NZWiQaQOqAnzApmC9cZ4o7RempV21ScpWkKGhrT51A==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-3.7.1.tgz",
"integrity": "sha512-YOlcE3bbk2ohaOVdRj9ww7AUYfmnS9hwJJGSj3/rFlNfMGOId4G8dLlhghXpdNSn05H0FRoI94UlFUKnn30Cyw==",
"requires": {
"@octokit/types": "^4.0.0",
"@octokit/types": "^2.11.1",
"deprecation": "^2.3.1"
}
},
"@octokit/request": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.4.tgz",
"integrity": "sha512-vqv1lz41c6VTxUvF9nM+a6U+vvP3vGk7drDpr0DVQg4zyqlOiKVrY17DLD6de5okj+YLHKcoqaUZTBtlNZ1BtQ==",
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.4.2.tgz",
"integrity": "sha512-zKdnGuQ2TQ2vFk9VU8awFT4+EYf92Z/v3OlzRaSh4RIP0H6cvW1BFPXq4XYvNez+TPQjqN+0uSkCYnMFFhcFrw==",
"requires": {
"@octokit/endpoint": "^6.0.1",
"@octokit/request-error": "^2.0.0",
"@octokit/types": "^4.0.1",
"@octokit/types": "^2.11.1",
"deprecation": "^2.0.0",
"is-plain-object": "^3.0.0",
"node-fetch": "^2.3.0",
@@ -2547,32 +2625,39 @@
}
},
"@octokit/request-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.1.tgz",
"integrity": "sha512-5lqBDJ9/TOehK82VvomQ6zFiZjPeSom8fLkFVLuYL3sKiIb5RB8iN/lenLkY7oBmyQcGP7FBMGiIZTO8jufaRQ==",
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.0.tgz",
"integrity": "sha512-rtYicB4Absc60rUv74Rjpzek84UbVHGHJRu4fNVlZ1mCcyUPPuzFfG9Rn6sjHrd95DEsmjSt1Axlc699ZlbDkw==",
"requires": {
"@octokit/types": "^4.0.1",
"@octokit/types": "^2.0.0",
"deprecation": "^2.0.0",
"once": "^1.4.0"
}
},
"@octokit/rest": {
"version": "17.9.2",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-17.9.2.tgz",
"integrity": "sha512-UXxiE0HhGQAPB3WDHTEu7lYMHH2uRcs/9f26XyHpGGiiXht8hgHWEk6fA7WglwwEvnj8V7mkJOgIntnij132UA==",
"version": "17.5.1",
"resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-17.5.1.tgz",
"integrity": "sha512-0rGY7eo0cw8FYX7jAtUgfy3j+05zhs9JvkPFegx00HAaayodM1ixlHhCOB5yirGbsVOxbRIWVkvKc2yY9367gg==",
"requires": {
"@octokit/core": "^2.4.3",
"@octokit/plugin-paginate-rest": "^2.2.0",
"@octokit/plugin-paginate-rest": "^2.1.0",
"@octokit/plugin-request-log": "^1.0.0",
"@octokit/plugin-rest-endpoint-methods": "^3.12.2"
"@octokit/plugin-rest-endpoint-methods": "3.7.1"
}
},
"@octokit/types": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-4.0.2.tgz",
"integrity": "sha512-+4X6qfhT/fk/5FD66395NrFLxCzD6FsGlpPwfwvnukdyfYbhiZB/FJltiT1XM5Q63rGGBSf9FPaNV3WpNHm54A==",
"version": "2.16.2",
"resolved": "https://registry.npmjs.org/@octokit/types/-/types-2.16.2.tgz",
"integrity": "sha512-O75k56TYvJ8WpAakWwYRN8Bgu60KrmX0z1KqFp1kNiFNkgW+JW+9EBKZ+S33PU6SLvbihqd+3drvPxKK68Ee8Q==",
"requires": {
"@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": {
@@ -3205,7 +3290,8 @@
"@types/node": {
"version": "12.11.1",
"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": {
"version": "2.0.3",
@@ -3285,6 +3371,14 @@
"@types/react": "*"
}
},
"@types/react-transition-group": {
"version": "4.2.4",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.4.tgz",
"integrity": "sha512-8DMUaDqh0S70TjkqU0DxOu80tFUiiaS9rxkWip/nb7gtvAsbqOXm02UCmR8zdcjWujgeYPiPNTVpVpKzUDotwA==",
"requires": {
"@types/react": "*"
}
},
"@types/shallowequal": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@types/shallowequal/-/shallowequal-1.1.1.tgz",
@@ -5530,6 +5624,11 @@
"integrity": "sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=",
"dev": true
},
"clsx": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.1.0.tgz",
"integrity": "sha512-3avwM37fSK5oP6M5rQ9CNe99lwxhXDOeSWVPAOYF6OazUTgZCMb0yWlJpmdD74REy1gkEaFiub2ULv4fq9GUhA=="
},
"co": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@@ -6094,6 +6193,15 @@
"postcss-value-parser": "^3.3.0"
}
},
"css-vendor": {
"version": "2.0.8",
"resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz",
"integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==",
"requires": {
"@babel/runtime": "^7.8.3",
"is-in-browser": "^1.0.2"
}
},
"css-what": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/css-what/-/css-what-2.1.3.tgz",
@@ -9807,6 +9915,11 @@
}
}
},
"hyphenate-style-name": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz",
"integrity": "sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ=="
},
"iconv-lite": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@@ -10346,6 +10459,11 @@
"resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz",
"integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw=="
},
"is-in-browser": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz",
"integrity": "sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU="
},
"is-number": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz",
@@ -11215,6 +11333,83 @@
"verror": "1.10.0"
}
},
"jss": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss/-/jss-10.1.1.tgz",
"integrity": "sha512-Xz3qgRUFlxbWk1czCZibUJqhVPObrZHxY3FPsjCXhDld4NOj1BgM14Ir5hVm+Qr6OLqVljjGvoMcCdXNOAbdkQ==",
"requires": {
"@babel/runtime": "^7.3.1",
"csstype": "^2.6.5",
"is-in-browser": "^1.1.3",
"tiny-warning": "^1.0.2"
}
},
"jss-plugin-camel-case": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.1.1.tgz",
"integrity": "sha512-MDIaw8FeD5uFz1seQBKz4pnvDLnj5vIKV5hXSVdMaAVq13xR6SVTVWkIV/keyTs5txxTvzGJ9hXoxgd1WTUlBw==",
"requires": {
"@babel/runtime": "^7.3.1",
"hyphenate-style-name": "^1.0.3",
"jss": "10.1.1"
}
},
"jss-plugin-default-unit": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.1.1.tgz",
"integrity": "sha512-UkeVCA/b3QEA4k0nIKS4uWXDCNmV73WLHdh2oDGZZc3GsQtlOCuiH3EkB/qI60v2MiCq356/SYWsDXt21yjwdg==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-global": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.1.1.tgz",
"integrity": "sha512-VBG3wRyi3Z8S4kMhm8rZV6caYBegsk+QnQZSVmrWw6GVOT/Z4FA7eyMu5SdkorDlG/HVpHh91oFN56O4R9m2VA==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-nested": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.1.1.tgz",
"integrity": "sha512-ozEu7ZBSVrMYxSDplPX3H82XHNQk2DQEJ9TEyo7OVTPJ1hEieqjDFiOQOxXEj9z3PMqkylnUbvWIZRDKCFYw5Q==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1",
"tiny-warning": "^1.0.2"
}
},
"jss-plugin-props-sort": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.1.1.tgz",
"integrity": "sha512-g/joK3eTDZB4pkqpZB38257yD4LXB0X15jxtZAGbUzcKAVUHPl9Jb47Y7lYmiGsShiV4YmQRqG1p2DHMYoK91g==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-rule-value-function": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.1.1.tgz",
"integrity": "sha512-ClV1lvJ3laU9la1CUzaDugEcwnpjPTuJ0yGy2YtcU+gG/w9HMInD5vEv7xKAz53Bk4WiJm5uLOElSEshHyhKNw==",
"requires": {
"@babel/runtime": "^7.3.1",
"jss": "10.1.1"
}
},
"jss-plugin-vendor-prefixer": {
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.1.1.tgz",
"integrity": "sha512-09MZpQ6onQrhaVSF6GHC4iYifQ7+4YC/tAP6D4ZWeZotvCMq1mHLqNKRIaqQ2lkgANjlEot2JnVi1ktu4+L4pw==",
"requires": {
"@babel/runtime": "^7.3.1",
"css-vendor": "^2.0.7",
"jss": "10.1.1"
}
},
"jsx-ast-utils": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",
@@ -16600,6 +16795,11 @@
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
"optional": true
},
"tiny-warning": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA=="
},
"tinycolor2": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.1.tgz",

View File

@@ -4,10 +4,11 @@
"description": "Cosmos Explorer",
"main": "index.js",
"dependencies": {
"@azure/cosmos": "3.7.0",
"@azure/cosmos": "3.6.3",
"@azure/cosmos-language-service": "0.0.4",
"@jupyterlab/services": "4.2.0",
"@jupyterlab/terminal": "1.2.1",
"@material-ui/core": "4.9.10",
"@microsoft/applicationinsights-web": "2.5.4",
"@nteract/commutable": "7.1.4",
"@nteract/connected-components": "6.7.8",
@@ -32,7 +33,7 @@
"@nteract/transform-plotly": "6.1.6",
"@nteract/transform-vdom": "4.0.11",
"@nteract/transform-vega": "7.0.6",
"@octokit/rest": "17.9.2",
"@octokit/rest": "17.5.1",
"@phosphor/widgets": "1.9.3",
"@uifabric/react-cards": "0.109.53",
"@uifabric/styling": "7.11.2",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,3 @@
import _ from "underscore";
import * as React from "react";
import { DefaultButton, IButtonProps } from "office-ui-fabric-react/lib/Button";
@@ -115,7 +114,7 @@ export class DirectoryListComponent extends React.Component<DirectoryListProps,
}
const buttonElement = e.currentTarget;
const selectedDirectoryId = buttonElement.getElementsByClassName("directoryListItemId")[0].textContent;
const selectedDirectory = _.find(this.props.directories, d => d.tenantId === selectedDirectoryId);
const selectedDirectory = this.props.directories.find(d => d.tenantId === selectedDirectoryId);
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);
if (repo) {
const item: RepoListItem = {
key: GitHubUtils.toRepoFullName(repo.owner, repo.name),
key: GitHubUtils.toRepoFullName(repo.owner.login, repo.name),
repo,
branches: [
{

View File

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

View File

@@ -1,18 +0,0 @@
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,28 +0,0 @@
// 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

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

View File

@@ -1,77 +0,0 @@
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

@@ -1,356 +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 { 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

@@ -1,54 +0,0 @@
// 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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1143,7 +1143,7 @@ export default class Explorer implements ViewModels.Explorer {
isModal: true,
visible: true,
title: `Enable Azure Synapse Link on your Cosmos DB account`,
subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads.
subText: `Enable Azure Synapse Link to perform near real time analytical analytics on this account, without impacting the performance of your transactional workloads.
Azure Synapse Link brings together Cosmos Db Analytical Store and Synapse Analytics`,
primaryButtonText: "Enable Azure Synapse Link",
secondaryButtonText: "Cancel",
@@ -2583,7 +2583,7 @@ export default class Explorer implements ViewModels.Explorer {
const item = NotebookUtil.createNotebookContentItem(name, path, "file");
const parent = this.resourceTree.myNotebooksContentRoot;
if (parent && parent.children && this.isNotebookEnabled() && this.notebookClient) {
if (parent && this.isNotebookEnabled() && this.notebookClient) {
if (this._filePathToImportAndOpen === path) {
this._filePathToImportAndOpen = null; // we don't want to try opening this path again
}
@@ -3278,12 +3278,7 @@ export default class Explorer implements ViewModels.Explorer {
newTab.onTabClick();
}
public openNotebookViewer(
notebookUrl: string,
notebookMetadata: DataModels.NotebookMetadata,
onNotebookMetadataChange: (newNotebookMetadata: DataModels.NotebookMetadata) => Promise<void>,
isLikedNotebook: boolean
) {
public openNotebookViewer(notebookUrl: string, notebookMetadata: DataModels.NotebookMetadata) {
const notebookName = path.basename(notebookUrl);
const title = notebookName;
const hashLocation = notebookUrl;
@@ -3325,9 +3320,7 @@ export default class Explorer implements ViewModels.Explorer {
openedTabs: this.openedTabs(),
notebookUrl: notebookUrl,
notebookName: notebookName,
notebookMetadata: notebookMetadata,
onNotebookMetadataChange: onNotebookMetadataChange,
isLikedNotebook: isLikedNotebook
notebookMetadata: notebookMetadata
});
this.openedTabs.push(newTab);

View File

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

View File

@@ -1,6 +1,5 @@
import { CellId } from "@nteract/commutable";
import { ContentRef } from "@nteract/core";
import { Action } from "../../../Shared/Telemetry/TelemetryConstants";
import { CellId } from "@nteract/commutable";
export const CLOSE_NOTEBOOK = "CLOSE_NOTEBOOK";
export interface CloseNotebookAction {
@@ -17,6 +16,25 @@ 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 interface ExecuteFocusedCellAndFocusNextAction {
type: "EXECUTE_FOCUSED_CELL_AND_FOCUS_NEXT";
@@ -63,24 +81,3 @@ export const setHoveredCell = (payload: { cellId: CellId }): SetHoveredCellActio
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, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { empty, merge, of, timer, interval, concat, Subject, Subscriber, Observable, Observer } from "rxjs";
import { webSocket } from "rxjs/webSocket";
import { ActionsObservable, StateObservable } from "redux-observable";
import { ofType } from "redux-observable";
@@ -10,6 +10,7 @@ import {
map,
switchMap,
take,
distinctUntilChanged,
filter,
catchError,
first,
@@ -20,6 +21,7 @@ import {
AppState,
ServerConfig as JupyterServerConfig,
JupyterHostRecordProps,
JupyterHostRecord,
RemoteKernelProps,
castToSessionId,
createKernelRef,
@@ -27,7 +29,8 @@ import {
ContentRef,
KernelInfo,
actions,
selectors
selectors,
IContentProvider
} from "@nteract/core";
import { message, JupyterMessage, Channels, createMessage, childOf, ofMessageType } from "@nteract/messaging";
import { sessions, kernels } from "rx-jupyter";
@@ -749,6 +752,69 @@ 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
* @param action$
@@ -851,6 +917,7 @@ export const allEpics = [
acquireKernelInfoEpic,
handleKernelConnectionLostEpic,
cleanKernelOnConnectionLostEpic,
adjustLastModifiedOnSaveEpic,
executeFocusedCellAndFocusNextEpic,
closeUnsupportedMimetypesEpic,
closeContentFailedToFetchEpic,

View File

@@ -1,10 +1,9 @@
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 { CdbRecord } from "./types";
import { Action } from "redux";
import { actions, CoreRecord, reducers as nteractReducers } from "@nteract/core";
export const coreReducer = (state: CoreRecord, action: Action) => {
let typedAction;
switch (action.type) {
@@ -51,6 +50,11 @@ export const coreReducer = (state: CoreRecord, action: Action) => {
.setIn(path.concat("displayName"), kernelspecs.displayName)
.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:
return nteractReducers.core(state as any, action as any);
}
@@ -71,17 +75,6 @@ export const cdbReducer = (state: CdbRecord, action: Action) => {
const typedAction = action as cdbActions.SetHoveredCellAction;
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;
};

View File

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

View File

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

View File

@@ -1,127 +0,0 @@
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,7 +155,19 @@ function openPane(action: ActionContracts.OpenPane, explorer: ViewModels.Explore
}
function openFile(action: ActionContracts.OpenSampleNotebook, explorer: ViewModels.Explorer) {
explorer.handleOpenFileAction(decodeURIComponent(action.path));
let path: string;
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 {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,9 @@
@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 "bootstrap/dist/css/bootstrap.css";
import { CosmosClient } from "../Common/CosmosClient";
import { GalleryViewerComponent } from "../Explorer/Controls/NotebookGallery/GalleryViewerComponent";
import "./GalleryViewer.less";
import { GalleryViewerComponent } from "./GalleryViewerComponent";
import { JunoUtils } from "../Utils/JunoUtils";
import { initializeIcons } from "office-ui-fabric-react/lib/Icons";
const onInit = async () => {
initializeIcons();
const officialSamplesData = await JunoUtils.getOfficialSampleNotebooks(CosmosClient.authorizationToken());
const officialSamplesData = await JunoUtils.getOfficialSampleNotebooks();
const galleryViewerComponent = new GalleryViewerComponent({
officialSamplesData: officialSamplesData,
likedNotebookData: undefined,

View File

@@ -0,0 +1,207 @@
/**
* 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,110 +1,92 @@
import ko from "knockout";
import { HttpStatusCodes } from "../Common/Constants";
import { GitHubClient, IGitHubFile } from "./GitHubClient";
import { SamplesRepo, SamplesBranch, SamplesContentsQueryResponse } from "../Explorer/Notebook/NotebookSamples";
import { GitHubClient, IGitHubBranch, IGitHubRepo } from "./GitHubClient";
const invalidTokenCallback = jest.fn();
// 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("cd1906b9534362fab6ce45d6db6c76b59e55bc50", invalidTokenCallback);
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}`);
}
// Use a dummy token to get around API rate limit (same as AZURESAMPLESCOSMOSDBPAT in webpack.config.js)
const gitHubClient = new GitHubClient("99e38770e29b4a61d7c49f188780504efd35cc86", 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("GitHubClient", () => {
describe.skip("GitHubClient", () => {
it("getRepoAsync returns valid repo", async () => {
const response = await gitHubClient.getRepoAsync(SamplesRepo.owner, SamplesRepo.name);
expect(response).toEqual({
status: HttpStatusCodes.OK,
data: SamplesRepo
});
const response = await gitHubClient.getRepoAsync(samplesRepo.owner.login, samplesRepo.name);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.name).toBe(samplesRepo.name);
expect(response.data.owner.login).toBe(samplesRepo.owner.login);
});
it("getReposAsync returns repos for authenticated user", async () => {
const response = await gitHubClient.getReposAsync(1);
const response = await gitHubClient.getReposAsync(1, 1);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data).toBeDefined();
expect(response.data.length).toBe(1);
expect(response.pageInfo).toBeDefined();
});
it("getBranchesAsync returns branches for a repo", async () => {
const response = await gitHubClient.getBranchesAsync(SamplesRepo.owner, SamplesRepo.name, 1);
const response = await gitHubClient.getBranchesAsync(samplesRepo.owner.login, samplesRepo.name, 1, 1);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data).toEqual([SamplesBranch]);
expect(response.pageInfo).toBeDefined();
expect(response.data.length).toBe(1);
});
it("getContentsAsync returns files in the repo", async () => {
const response = await gitHubClient.getContentsAsync(SamplesRepo.owner, SamplesRepo.name, SamplesBranch.name);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data).toBeDefined();
const data = response.data as IGitHubFile[];
expect(data.length).toBeGreaterThan(0);
data.forEach(content => validateGitHubFile(content));
});
it("getContentsAsync returns files in a dir", async () => {
const samplesDir = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "tree");
const response = await gitHubClient.getContentsAsync(
SamplesRepo.owner,
SamplesRepo.name,
SamplesBranch.name,
samplesDir.name
it("getCommitsAsync returns commits for a file", async () => {
const response = await gitHubClient.getCommitsAsync(
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
sampleFilePath,
1,
1
);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data).toBeDefined();
const data = response.data as IGitHubFile[];
expect(data.length).toBeGreaterThan(0);
data.forEach(content => validateGitHubFile(content));
expect(response.data.length).toBe(1);
});
it("getContentsAsync returns a file", async () => {
const samplesFile = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "blob");
const response = await gitHubClient.getContentsAsync(
SamplesRepo.owner,
SamplesRepo.name,
SamplesBranch.name,
samplesFile.name
it("getDirContentsAsync returns files in the repo", async () => {
const response = await gitHubClient.getDirContentsAsync(
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
""
);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data).toBeDefined();
const file = response.data as IGitHubFile;
expect(file.type).toBe("blob");
validateGitHubFile(file);
expect(file.content).toBeUndefined();
expect(response.data.length).toBeGreaterThan(0);
expect(response.data[0].repo).toEqual(samplesRepo);
expect(response.data[0].branch).toEqual(samplesBranch);
});
it("getBlobAsync returns file content", async () => {
const samplesFile = SamplesContentsQueryResponse.repository.object.entries.find(file => file.type === "blob");
const response = await gitHubClient.getBlobAsync(SamplesRepo.owner, SamplesRepo.name, samplesFile.object.oid);
it("getDirContentsAsync returns files in a dir", async () => {
const response = await gitHubClient.getDirContentsAsync(
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
sampleDirPath
);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data).toBeDefined();
expect(typeof response.data).toBe("string");
expect(response.data.length).toBeGreaterThan(0);
expect(response.data[0].repo).toEqual(samplesRepo);
expect(response.data[0].branch).toEqual(samplesBranch);
});
it("getFileContentsAsync returns a file", async () => {
const response = await gitHubClient.getFileContentsAsync(
samplesRepo.owner.login,
samplesRepo.name,
samplesBranch.name,
sampleFilePath
);
expect(response.status).toBe(HttpStatusCodes.OK);
expect(response.data.path).toBe(sampleFilePath);
expect(response.data.repo).toEqual(samplesRepo);
expect(response.data.branch).toEqual(samplesBranch);
});
});

View File

@@ -1,228 +1,163 @@
import { Octokit } from "@octokit/rest";
import { RequestHeaders } from "@octokit/types";
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> {
status: number;
data: T;
pageInfo?: IGitHubPageInfo;
}
export interface IGitHubRepo {
name: string;
owner: string;
private: boolean;
children?: IGitHubFile[];
}
export interface IGitHubFile {
type: "blob" | "tree";
size?: number;
name: string;
path: string;
content?: string;
sha?: string;
children?: IGitHubFile[];
repo: IGitHubRepo;
branch: IGitHubBranch;
commit: IGitHubCommit;
}
export interface IGitHubCommit {
sha: string;
message: string;
commitDate: string;
}
export interface IGitHubBranch {
name: string;
}
// graphql schema
interface Collection<T> {
pageInfo?: PageInfo;
nodes: T[];
}
interface Repository {
isPrivate: boolean;
// API properties
name: string;
owner: {
login: string;
};
private: boolean;
// Custom properties
children?: IGitHubFile[];
}
interface Ref {
export interface IGitHubFile {
// API properties
type: "file" | "dir" | "symlink" | "submodule";
encoding?: string;
size: number;
name: string;
path: string;
content?: string;
sha: string;
// Custom properties
children?: IGitHubFile[];
repo?: IGitHubRepo;
branch?: IGitHubBranch;
}
interface History {
history: Collection<Commit>;
}
interface Commit {
export interface IGitHubCommit {
// API properties
sha: string;
message: string;
committer: {
date: string;
};
message: string;
oid: string;
}
interface Tree extends Blob {
entries: TreeEntry[];
}
interface TreeEntry {
export interface IGitHubBranch {
// API properties
name: string;
type: string;
object: Blob;
}
interface Blob {
byteSize?: number;
oid?: string;
export interface IGitHubUser {
// API properties
login: string;
name: 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 {
private static readonly SelfErrorCode = 599;
private static readonly gitHubApiEndpoint = "https://api.github.com";
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;
constructor(token: string, private errorCallback: (error: any) => void) {
@@ -234,136 +169,167 @@ export class GitHubClient {
}
public async getRepoAsync(owner: string, repo: string): Promise<IGitHubResponse<IGitHubRepo>> {
try {
const response = (await this.ocktokit.graphql(repositoryQuery, {
owner,
repo
} as RepositoryQueryParams)) as RepositoryQueryResponse;
if (GitHubClient.isSamplesCall(owner, repo)) {
return {
status: HttpStatusCodes.OK,
data: GitHubClient.toGitHubRepo(response.repository)
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getRepoAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
data: GitHubClient.samplesRepo
};
}
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(pageSize: number, endCursor?: string): Promise<IGitHubResponse<IGitHubRepo[]>> {
try {
const response = (await this.ocktokit.graphql(repositoriesQuery, {
pageSize,
endCursor
} as RepositoriesQueryParams)) as RepositoriesQueryResponse;
public async getReposAsync(page: number, perPage: number): Promise<IGitHubResponse<IGitHubRepo[]>> {
const response = await this.ocktokit.repos.listForAuthenticatedUser({
page,
per_page: perPage,
headers: GitHubClient.getDisableCacheHeaders()
});
return {
status: HttpStatusCodes.OK,
data: response.viewer.repositories.nodes.map(repo => GitHubClient.toGitHubRepo(repo)),
pageInfo: GitHubClient.toGitHubPageInfo(response.viewer.repositories.pageInfo)
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getReposAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
};
let data: IGitHubRepo[];
if (response.data) {
data = [];
response.data?.forEach((element: any) => data.push(GitHubClient.toGitHubRepo(element)));
}
return { status: response.status, data };
}
public async getBranchesAsync(
owner: string,
repo: string,
pageSize: number,
endCursor?: string
page: number,
perPage: number
): Promise<IGitHubResponse<IGitHubBranch[]>> {
try {
const response = (await this.ocktokit.graphql(branchesQuery, {
owner,
repo,
refPrefix: "refs/heads/",
pageSize,
endCursor
} as BranchesQueryParams)) as BranchesQueryResponse;
const response = await this.ocktokit.repos.listBranches({
owner,
repo,
page,
per_page: perPage,
headers: GitHubClient.getDisableCacheHeaders()
});
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 {
status: HttpStatusCodes.OK,
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
data: [GitHubClient.samplesTopCommit]
};
}
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(
owner: string,
repo: string,
branch: string,
path?: string
path: string
): Promise<IGitHubResponse<IGitHubFile | IGitHubFile[]>> {
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
);
}
if (GitHubClient.isSamplesCall(owner, repo, branch) && path === "") {
return {
status: HttpStatusCodes.OK,
data
};
} catch (error) {
GitHubClient.log(Logger.logError, `GitHubClient.getContentsAsync failed: ${error}`);
return {
status: GitHubClient.SelfErrorCode,
data: undefined
data: GitHubClient.samplesFiles.map(file =>
GitHubClient.toGitHubFile(file, GitHubClient.samplesRepo, GitHubClient.samplesBranch)
)
};
}
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(
@@ -406,9 +372,7 @@ export class GitHubClient {
owner,
repo,
ref,
headers: {
"If-None-Match": "" // disable 60s cache
}
headers: GitHubClient.getDisableCacheHeaders()
});
const currentTree = await this.ocktokit.git.getTree({
@@ -416,9 +380,7 @@ export class GitHubClient {
repo,
tree_sha: currentRef.data.object.sha,
recursive: "1",
headers: {
"If-None-Match": "" // disable 60s cache
}
headers: GitHubClient.getDisableCacheHeaders()
});
// API infers tree from paths so we need to filter them out
@@ -463,7 +425,7 @@ export class GitHubClient {
public async deleteFileAsync(file: IGitHubFile, message: string): Promise<IGitHubResponse<IGitHubCommit>> {
const response = await this.ocktokit.repos.deleteFile({
owner: file.repo.owner,
owner: file.repo.owner.login,
repo: file.repo.name,
path: file.path,
message,
@@ -479,31 +441,10 @@ export class GitHubClient {
return { status: response.status, data };
}
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) {
private initOctokit(token: string) {
this.ocktokit = new Octokit({
auth: token,
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)
}
baseUrl: GitHubClient.gitHubApiEndpoint
});
this.ocktokit.hook.error("request", error => {
@@ -512,69 +453,53 @@ export class GitHubClient {
});
}
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 {
private static getDisableCacheHeaders(): RequestHeaders {
return {
owner: object.owner.login,
name: object.name,
private: object.isPrivate
"If-None-Match": ""
};
}
private static toGitHubBranch(object: Ref): IGitHubBranch {
private static toGitHubRepo(element: IGitHubRepo): IGitHubRepo {
return {
name: object.name
name: element.name,
owner: {
login: element.owner.login
},
private: element.private
};
}
private static toGitHubCommit(object: {
message: string;
committer: {
date: string;
};
sha?: string;
oid?: string;
}): IGitHubCommit {
private static toGitHubBranch(element: IGitHubBranch): IGitHubBranch {
return {
sha: object.sha || object.oid,
message: object.message,
commitDate: object.committer.date
name: element.name
};
}
private static toGitHubPageInfo(object: PageInfo): IGitHubPageInfo {
private static toGitHubCommit(element: IGitHubCommit): IGitHubCommit {
return {
endCursor: object.endCursor,
hasNextPage: object.hasNextPage
sha: element.sha,
message: element.message,
committer: {
date: element.committer.date
}
};
}
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}`);
}
private static toGitHubFile(element: IGitHubFile, repo: IGitHubRepo, branch: IGitHubBranch): IGitHubFile {
return {
type: entry.type,
name: entry.name,
path,
type: element.type,
encoding: element.encoding,
size: element.size,
name: element.name,
path: element.path,
content: element.content,
sha: element.sha,
repo,
branch,
commit,
size: entry.object?.byteSize,
sha: entry.object?.oid
branch
};
}
private static isSamplesCall(owner: string, repo: string, branch?: string): boolean {
return owner === "Azure-Samples" && repo === "cosmos-notebooks" && (!branch || branch === "master");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,10 +7,6 @@ export class GitHubUtils {
// Custom scheme for github content
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 {
return `${owner}/${repo}`;
}
@@ -31,7 +27,7 @@ export class GitHubUtils {
public static fromContentUri(
contentUri: string
): undefined | { owner: string; repo: string; branch: string; path: string } {
let matches = contentUri.match(GitHubUtils.ContentUriPattern);
const matches = contentUri.match(GitHubUtils.ContentUriPattern);
if (matches && matches.length > 4) {
return {
owner: matches[1],
@@ -41,18 +37,6 @@ 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;
}

View File

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

View File

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

View File

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

View File

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