├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── binder-on-pr.yaml │ ├── build.yml │ ├── check-release.yml │ ├── packaging.yml │ ├── prep-release.yml │ ├── publish-changelog.yml │ └── publish-release.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── .stylelintrc ├── .travis.yml ├── .yarnrc.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASE.md ├── binder ├── environment.yml └── postBuild ├── gitception.png ├── install.json ├── jupyter-config ├── jupyter_notebook_config.d │ └── jupyterlab_github.json └── jupyter_server_config.d │ └── jupyterlab_github.json ├── jupyterlab_github ├── __init__.py └── api │ └── api.yaml ├── package.json ├── pyproject.toml ├── schema └── drive.json ├── setup.py ├── src ├── browser.ts ├── contents.ts ├── github.ts ├── index.ts └── svg.d.ts ├── style ├── base.css ├── binder.svg ├── index.css ├── index.js ├── octocat-dark.svg ├── octocat-light.svg └── octocat_error.png ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | **/*.d.ts 5 | tests 6 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | 👋 Hi! This file was autogenerated by tslint-to-eslint-config. 3 | https://github.com/typescript-eslint/tslint-to-eslint-config 4 | 5 | It represents the closest reasonable ESLint configuration to this 6 | project's original TSLint configuration. 7 | 8 | We recommend eventually switching this configuration to extend from 9 | the recommended rulesets in typescript-eslint. 10 | https://github.com/typescript-eslint/tslint-to-eslint-config/blob/master/docs/FAQs.md 11 | 12 | Happy linting! 💖 13 | */ 14 | module.exports = { 15 | env: { 16 | browser: true, 17 | es6: true 18 | }, 19 | extends: [ 20 | 'eslint:recommended', 21 | 'plugin:@typescript-eslint/eslint-recommended', 22 | 'plugin:@typescript-eslint/recommended', 23 | 'plugin:prettier/recommended' 24 | ], 25 | ignorePatterns: ['node_modules', 'dist', 'coverage', '**/*.d.ts', 'tests'], 26 | parser: '@typescript-eslint/parser', 27 | parserOptions: { 28 | project: 'tsconfig.json', 29 | sourceType: 'module' 30 | }, 31 | plugins: [ 32 | 'eslint-plugin-import', 33 | 'eslint-plugin-no-null', 34 | '@typescript-eslint' 35 | ], 36 | root: true, 37 | rules: { 38 | '@babel/object-curly-spacing': 'off', 39 | '@babel/semi': 'off', 40 | '@typescript-eslint/adjacent-overload-signatures': 'error', 41 | '@typescript-eslint/ban-ts-comment': 'error', 42 | '@typescript-eslint/ban-types': 'error', 43 | '@typescript-eslint/block-spacing': 'off', 44 | '@typescript-eslint/brace-style': 'off', 45 | '@typescript-eslint/comma-dangle': 'off', 46 | '@typescript-eslint/comma-spacing': 'off', 47 | '@typescript-eslint/consistent-type-assertions': 'error', 48 | '@typescript-eslint/dot-notation': 'off', 49 | '@typescript-eslint/explicit-function-return-type': 'off', 50 | '@typescript-eslint/explicit-member-accessibility': [ 51 | 'off', 52 | { 53 | accessibility: 'explicit' 54 | } 55 | ], 56 | '@typescript-eslint/explicit-module-boundary-types': 'off', 57 | '@typescript-eslint/func-call-spacing': 'off', 58 | '@typescript-eslint/key-spacing': 'off', 59 | '@typescript-eslint/keyword-spacing': 'off', 60 | '@typescript-eslint/lines-around-comment': 'off', 61 | '@typescript-eslint/member-delimiter-style': [ 62 | 'error', 63 | { 64 | multiline: { 65 | delimiter: 'semi', 66 | requireLast: true 67 | }, 68 | singleline: { 69 | delimiter: 'semi', 70 | requireLast: false 71 | } 72 | } 73 | ], 74 | '@typescript-eslint/member-ordering': 'off', 75 | '@typescript-eslint/naming-convention': [ 76 | 'error', 77 | { 78 | selector: 'variable', 79 | format: ['camelCase', 'UPPER_CASE'], 80 | leadingUnderscore: 'allow', 81 | trailingUnderscore: 'forbid' 82 | } 83 | ], 84 | '@typescript-eslint/no-array-constructor': 'error', 85 | '@typescript-eslint/no-empty-function': 'error', 86 | '@typescript-eslint/no-empty-interface': 'error', 87 | '@typescript-eslint/no-explicit-any': 'off', 88 | '@typescript-eslint/no-extra-non-null-assertion': 'error', 89 | '@typescript-eslint/no-extra-parens': 'off', 90 | '@typescript-eslint/no-extra-semi': 'off', 91 | '@typescript-eslint/no-inferrable-types': 'off', 92 | '@typescript-eslint/no-loss-of-precision': 'error', 93 | '@typescript-eslint/no-misused-new': 'error', 94 | '@typescript-eslint/no-namespace': 'off', 95 | '@typescript-eslint/no-non-null-asserted-optional-chain': 'error', 96 | '@typescript-eslint/no-non-null-assertion': 'warn', 97 | '@typescript-eslint/no-require-imports': 'off', 98 | '@typescript-eslint/no-shadow': [ 99 | 'off', 100 | { 101 | hoist: 'all' 102 | } 103 | ], 104 | '@typescript-eslint/no-this-alias': 'error', 105 | '@typescript-eslint/no-unnecessary-type-constraint': 'error', 106 | '@typescript-eslint/no-unused-expressions': 'error', 107 | '@typescript-eslint/no-unused-vars': [ 108 | 'warn', 109 | { 110 | args: 'none' 111 | } 112 | ], 113 | '@typescript-eslint/no-use-before-define': 'off', 114 | '@typescript-eslint/no-var-requires': 'error', 115 | '@typescript-eslint/object-curly-spacing': 'off', 116 | '@typescript-eslint/prefer-as-const': 'error', 117 | '@typescript-eslint/prefer-namespace-keyword': 'error', 118 | '@typescript-eslint/quotes': [ 119 | 'error', 120 | 'single', 121 | { 122 | avoidEscape: true 123 | } 124 | ], 125 | '@typescript-eslint/semi': ['error', 'always'], 126 | '@typescript-eslint/space-before-blocks': 'off', 127 | '@typescript-eslint/space-before-function-paren': 'off', 128 | '@typescript-eslint/space-infix-ops': 'off', 129 | '@typescript-eslint/triple-slash-reference': 'error', 130 | '@typescript-eslint/type-annotation-spacing': 'off', 131 | '@typescript-eslint/typedef': 'off', 132 | 'array-bracket-newline': 'off', 133 | 'array-bracket-spacing': 'off', 134 | 'array-element-newline': 'off', 135 | 'arrow-body-style': 'off', 136 | 'arrow-parens': 'off', 137 | 'arrow-spacing': 'off', 138 | 'babel/object-curly-spacing': 'off', 139 | 'babel/quotes': 'off', 140 | 'babel/semi': 'off', 141 | 'block-spacing': 'off', 142 | 'brace-style': ['error', '1tbs'], 143 | 'comma-dangle': 'off', 144 | 'comma-spacing': 'off', 145 | 'comma-style': 'off', 146 | 'computed-property-spacing': 'off', 147 | 'constructor-super': 'error', 148 | curly: 'error', 149 | 'default-case': 'error', 150 | 'dot-location': 'off', 151 | 'dot-notation': 'off', 152 | 'eol-last': 'error', 153 | eqeqeq: ['error', 'smart'], 154 | 'flowtype/boolean-style': 'off', 155 | 'flowtype/delimiter-dangle': 'off', 156 | 'flowtype/generic-spacing': 'off', 157 | 'flowtype/object-type-curly-spacing': 'off', 158 | 'flowtype/object-type-delimiter': 'off', 159 | 'flowtype/quotes': 'off', 160 | 'flowtype/semi': 'off', 161 | 'flowtype/space-after-type-colon': 'off', 162 | 'flowtype/space-before-generic-bracket': 'off', 163 | 'flowtype/space-before-type-colon': 'off', 164 | 'flowtype/union-intersection-spacing': 'off', 165 | 'for-direction': 'error', 166 | 'func-call-spacing': 'off', 167 | 'function-call-argument-newline': 'off', 168 | 'function-paren-newline': 'off', 169 | 'generator-star': 'off', 170 | 'generator-star-spacing': 'off', 171 | 'getter-return': 'error', 172 | 'guard-for-in': 'off', 173 | 'id-denylist': [ 174 | 'error', 175 | 'any', 176 | 'Number', 177 | 'number', 178 | 'String', 179 | 'string', 180 | 'Boolean', 181 | 'boolean', 182 | 'Undefined', 183 | 'undefined' 184 | ], 185 | 'id-match': 'error', 186 | 'implicit-arrow-linebreak': 'off', 187 | 'import/no-default-export': 'off', 188 | indent: 'off', 189 | 'indent-legacy': 'off', 190 | 'jsx-quotes': 'off', 191 | 'key-spacing': 'off', 192 | 'keyword-spacing': 'off', 193 | 'linebreak-style': 'off', 194 | 'lines-around-comment': 'off', 195 | 'max-len': 'off', 196 | 'multiline-ternary': 'off', 197 | 'new-parens': 'error', 198 | 'newline-per-chained-call': 'off', 199 | 'no-array-constructor': 'off', 200 | 'no-arrow-condition': 'off', 201 | 'no-async-promise-executor': 'error', 202 | 'no-bitwise': 'error', 203 | 'no-caller': 'error', 204 | 'no-case-declarations': 'error', 205 | 'no-class-assign': 'error', 206 | 'no-comma-dangle': 'off', 207 | 'no-compare-neg-zero': 'error', 208 | 'no-cond-assign': 'error', 209 | 'no-confusing-arrow': 'off', 210 | 'no-console': [ 211 | 'error', 212 | { 213 | allow: [ 214 | 'log', 215 | 'warn', 216 | 'dir', 217 | 'timeLog', 218 | 'assert', 219 | 'clear', 220 | 'count', 221 | 'countReset', 222 | 'group', 223 | 'groupEnd', 224 | 'table', 225 | 'dirxml', 226 | 'error', 227 | 'groupCollapsed', 228 | 'Console', 229 | 'profile', 230 | 'profileEnd', 231 | 'timeStamp', 232 | 'context' 233 | ] 234 | } 235 | ], 236 | 'no-const-assign': 'error', 237 | 'no-constant-condition': 'error', 238 | 'no-control-regex': 'error', 239 | 'no-debugger': 'error', 240 | 'no-delete-var': 'error', 241 | 'no-dupe-args': 'error', 242 | 'no-dupe-class-members': 'error', 243 | 'no-dupe-else-if': 'error', 244 | 'no-dupe-keys': 'error', 245 | 'no-duplicate-case': 'error', 246 | 'no-empty': 'error', 247 | 'no-empty-character-class': 'error', 248 | 'no-empty-function': 'off', 249 | 'no-empty-pattern': 'error', 250 | 'no-eval': 'error', 251 | 'no-ex-assign': 'error', 252 | 'no-extra-boolean-cast': 'error', 253 | 'no-extra-parens': 'off', 254 | 'no-extra-semi': 'off', 255 | 'no-fallthrough': 'error', 256 | 'no-floating-decimal': 'off', 257 | 'no-func-assign': 'error', 258 | 'no-global-assign': 'error', 259 | 'no-import-assign': 'error', 260 | 'no-inner-declarations': 'error', 261 | 'no-invalid-regexp': 'error', 262 | 'no-invalid-this': 'error', 263 | 'no-irregular-whitespace': 'error', 264 | 'no-loss-of-precision': 'off', 265 | 'no-misleading-character-class': 'error', 266 | 'no-mixed-operators': 'off', 267 | 'no-mixed-spaces-and-tabs': 'off', 268 | 'no-multi-spaces': 'off', 269 | 'no-multiple-empty-lines': 'off', 270 | 'no-new-symbol': 'error', 271 | 'no-new-wrappers': 'error', 272 | 'no-nonoctal-decimal-escape': 'error', 273 | 'no-null/no-null': 'off', 274 | 'no-obj-calls': 'error', 275 | 'no-octal': 'error', 276 | 'no-prototype-builtins': 'error', 277 | 'no-redeclare': 'error', 278 | 'no-regex-spaces': 'error', 279 | 'no-reserved-keys': 'off', 280 | 'no-self-assign': 'error', 281 | 'no-setter-return': 'error', 282 | 'no-shadow': 'off', 283 | 'no-shadow-restricted-names': 'error', 284 | 'no-space-before-semi': 'off', 285 | 'no-spaced-func': 'off', 286 | 'no-sparse-arrays': 'error', 287 | 'no-tabs': 'off', 288 | 'no-this-before-super': 'error', 289 | 'no-trailing-spaces': 'error', 290 | 'no-undef': 'error', 291 | 'no-underscore-dangle': 'off', 292 | 'no-unexpected-multiline': 'off', 293 | 'no-unreachable': 'error', 294 | 'no-unsafe-finally': 'error', 295 | 'no-unsafe-negation': 'error', 296 | 'no-unsafe-optional-chaining': 'error', 297 | 'no-unused-expressions': 'off', 298 | 'no-unused-labels': 'error', 299 | 'no-unused-vars': 'off', 300 | 'no-use-before-define': 'off', 301 | 'no-useless-backreference': 'error', 302 | 'no-useless-catch': 'error', 303 | 'no-useless-escape': 'error', 304 | 'no-var': 'error', 305 | 'no-whitespace-before-property': 'off', 306 | 'no-with': 'error', 307 | 'no-wrap-func': 'off', 308 | 'nonblock-statement-body-position': 'off', 309 | 'object-curly-newline': 'off', 310 | 'object-curly-spacing': 'off', 311 | 'object-property-newline': 'off', 312 | 'one-var': ['error', 'never'], 313 | 'one-var-declaration-per-line': 'off', 314 | 'operator-linebreak': 'off', 315 | 'padded-blocks': 'off', 316 | 'prefer-arrow-callback': 'error', 317 | 'prettier/prettier': 'error', 318 | 'quote-props': 'off', 319 | quotes: 'off', 320 | radix: 'error', 321 | 'react/jsx-child-element-spacing': 'off', 322 | 'react/jsx-closing-bracket-location': 'off', 323 | 'react/jsx-closing-tag-location': 'off', 324 | 'react/jsx-curly-newline': 'off', 325 | 'react/jsx-curly-spacing': 'off', 326 | 'react/jsx-equals-spacing': 'off', 327 | 'react/jsx-first-prop-new-line': 'off', 328 | 'react/jsx-indent': 'off', 329 | 'react/jsx-indent-props': 'off', 330 | 'react/jsx-max-props-per-line': 'off', 331 | 'react/jsx-newline': 'off', 332 | 'react/jsx-one-expression-per-line': 'off', 333 | 'react/jsx-props-no-multi-spaces': 'off', 334 | 'react/jsx-space-before-closing': 'off', 335 | 'react/jsx-tag-spacing': 'off', 336 | 'react/jsx-wrap-multilines': 'off', 337 | 'require-yield': 'error', 338 | 'rest-spread-spacing': 'off', 339 | semi: 'off', 340 | 'semi-spacing': 'off', 341 | 'semi-style': 'off', 342 | 'space-after-function-name': 'off', 343 | 'space-after-keywords': 'off', 344 | 'space-before-blocks': 'off', 345 | 'space-before-function-paren': 'off', 346 | 'space-before-function-parentheses': 'off', 347 | 'space-before-keywords': 'off', 348 | 'space-in-brackets': 'off', 349 | 'space-in-parens': 'off', 350 | 'space-infix-ops': 'off', 351 | 'space-return-throw-case': 'off', 352 | 'space-unary-ops': 'off', 353 | 'space-unary-word-ops': 'off', 354 | 'spaced-comment': [ 355 | 'error', 356 | 'always', 357 | { 358 | markers: ['/'] 359 | } 360 | ], 361 | 'standard/array-bracket-even-spacing': 'off', 362 | 'standard/computed-property-even-spacing': 'off', 363 | 'standard/object-curly-even-spacing': 'off', 364 | 'switch-colon-spacing': 'off', 365 | 'template-curly-spacing': 'off', 366 | 'template-tag-spacing': 'off', 367 | 'unicode-bom': 'off', 368 | 'unicorn/empty-brace-spaces': 'off', 369 | 'unicorn/no-nested-ternary': 'off', 370 | 'unicorn/number-literal-case': 'off', 371 | 'use-isnan': 'error', 372 | 'valid-typeof': 'error', 373 | 'vue/array-bracket-newline': 'off', 374 | 'vue/array-bracket-spacing': 'off', 375 | 'vue/arrow-spacing': 'off', 376 | 'vue/block-spacing': 'off', 377 | 'vue/block-tag-newline': 'off', 378 | 'vue/brace-style': 'off', 379 | 'vue/comma-dangle': 'off', 380 | 'vue/comma-spacing': 'off', 381 | 'vue/comma-style': 'off', 382 | 'vue/dot-location': 'off', 383 | 'vue/func-call-spacing': 'off', 384 | 'vue/html-closing-bracket-newline': 'off', 385 | 'vue/html-closing-bracket-spacing': 'off', 386 | 'vue/html-end-tags': 'off', 387 | 'vue/html-indent': 'off', 388 | 'vue/html-quotes': 'off', 389 | 'vue/html-self-closing': 'off', 390 | 'vue/key-spacing': 'off', 391 | 'vue/keyword-spacing': 'off', 392 | 'vue/max-attributes-per-line': 'off', 393 | 'vue/max-len': 'off', 394 | 'vue/multiline-html-element-content-newline': 'off', 395 | 'vue/multiline-ternary': 'off', 396 | 'vue/mustache-interpolation-spacing': 'off', 397 | 'vue/no-extra-parens': 'off', 398 | 'vue/no-multi-spaces': 'off', 399 | 'vue/no-spaces-around-equal-signs-in-attribute': 'off', 400 | 'vue/object-curly-newline': 'off', 401 | 'vue/object-curly-spacing': 'off', 402 | 'vue/object-property-newline': 'off', 403 | 'vue/operator-linebreak': 'off', 404 | 'vue/quote-props': 'off', 405 | 'vue/script-indent': 'off', 406 | 'vue/singleline-html-element-content-newline': 'off', 407 | 'vue/space-in-parens': 'off', 408 | 'vue/space-infix-ops': 'off', 409 | 'vue/space-unary-ops': 'off', 410 | 'vue/template-curly-spacing': 'off', 411 | 'wrap-iife': 'off', 412 | 'wrap-regex': 'off', 413 | 'yield-star-spacing': 'off' 414 | } 415 | }; 416 | -------------------------------------------------------------------------------- /.github/workflows/binder-on-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Binder Badge 2 | on: 3 | pull_request_target: 4 | types: [opened] 5 | 6 | permissions: 7 | pull-requests: write 8 | 9 | jobs: 10 | binder: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: comment on PR with Binder link 14 | uses: actions/github-script@v3 15 | with: 16 | github-token: ${{secrets.GITHUB_TOKEN}} 17 | script: | 18 | var PR_HEAD_USERREPO = process.env.PR_HEAD_USERREPO; 19 | var PR_HEAD_REF = process.env.PR_HEAD_REF; 20 | github.issues.createComment({ 21 | issue_number: context.issue.number, 22 | owner: context.repo.owner, 23 | repo: context.repo.repo, 24 | body: `[![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/${PR_HEAD_USERREPO}/${PR_HEAD_REF}?urlpath=lab) :point_left: Launch a binder notebook on branch _${PR_HEAD_USERREPO}/${PR_HEAD_REF}_` 25 | }) 26 | env: 27 | PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} 28 | PR_HEAD_USERREPO: ${{ github.event.pull_request.head.repo.full_name }} 29 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | defaults: 12 | run: 13 | shell: bash -l {0} 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v3 22 | 23 | - name: Base Setup 24 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 25 | 26 | - name: Install JupyterLab 27 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 28 | 29 | - name: Lint the extension 30 | run: | 31 | set -eux 32 | jlpm 33 | jlpm run lint:check 34 | 35 | - name: Build the extension 36 | run: | 37 | set -eux 38 | python -m pip install . 39 | 40 | jupyter labextension list 41 | jupyter labextension list 2>&1 | grep -ie "@jupyterlab/github.*OK" 42 | 43 | python -m jupyterlab.browser_check 44 | -------------------------------------------------------------------------------- /.github/workflows/check-release.yml: -------------------------------------------------------------------------------- 1 | name: Check Release 2 | on: 3 | push: 4 | branches: [ main ] 5 | pull_request: 6 | branches: ["*"] 7 | 8 | jobs: 9 | check_release: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Base Setup 17 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 18 | 19 | - name: Check Release 20 | uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 21 | with: 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Upload Distributions 25 | uses: actions/upload-artifact@v3 26 | with: 27 | name: jupyterlab_github-releaser-dist-${{ github.run_number }} 28 | path: .jupyter_releaser_checkout/dist 29 | -------------------------------------------------------------------------------- /.github/workflows/packaging.yml: -------------------------------------------------------------------------------- 1 | name: Packaging 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: '*' 8 | 9 | env: 10 | PIP_DISABLE_PIP_VERSION_CHECK: 1 11 | 12 | defaults: 13 | run: 14 | shell: bash -l {0} 15 | 16 | jobs: 17 | build: 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v3 23 | 24 | - name: Base Setup 25 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 26 | 27 | - name: Install dependencies 28 | run: python -m pip install -U "jupyterlab>=4.0.0,<5" 29 | 30 | - name: Package the extension 31 | run: | 32 | set -eux 33 | pip install build 34 | python -m build 35 | pip uninstall -y "jupyterlab_github" jupyterlab 36 | 37 | - name: Upload extension packages 38 | uses: actions/upload-artifact@v3 39 | with: 40 | name: extension-artifacts 41 | path: ./dist/jupyterlab_github* 42 | if-no-files-found: error 43 | 44 | install: 45 | runs-on: ${{ matrix.os }}-latest 46 | needs: [build] 47 | strategy: 48 | fail-fast: false 49 | matrix: 50 | os: [ubuntu, macos, windows] 51 | python: ['3.8', '3.11'] 52 | include: 53 | - python: '3.8' 54 | dist: 'jupyterlab_github*.tar.gz' 55 | - python: '3.11' 56 | dist: 'jupyterlab_github*.whl' 57 | - os: windows 58 | py_cmd: python 59 | - os: macos 60 | py_cmd: python3 61 | - os: ubuntu 62 | py_cmd: python 63 | 64 | steps: 65 | - name: Install Python 66 | uses: actions/setup-python@v4 67 | with: 68 | python-version: ${{ matrix.python }} 69 | architecture: 'x64' 70 | 71 | - uses: actions/download-artifact@v3 72 | with: 73 | name: extension-artifacts 74 | path: ./dist 75 | 76 | - name: Install prerequisites 77 | run: | 78 | ${{ matrix.py_cmd }} -m pip install pip wheel 79 | 80 | - name: Install package 81 | run: | 82 | cd dist 83 | ${{ matrix.py_cmd }} -m pip install -vv ${{ matrix.dist }} 84 | 85 | - name: Validate environment 86 | run: | 87 | ${{ matrix.py_cmd }} -m pip freeze 88 | ${{ matrix.py_cmd }} -m pip check 89 | 90 | - name: Validate install 91 | run: | 92 | jupyter labextension list 93 | jupyter labextension list 2>&1 | grep -ie "@jupyterlab/github.*OK" 94 | -------------------------------------------------------------------------------- /.github/workflows/prep-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 1: Prep Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | version_spec: 6 | description: "New Version Specifier" 7 | default: "next" 8 | required: false 9 | branch: 10 | description: "The branch to target" 11 | required: false 12 | post_version_spec: 13 | description: "Post Version Specifier" 14 | required: false 15 | silent: 16 | description: "Set a placeholder in the changelog and don't publish the release." 17 | required: false 18 | type: boolean 19 | since: 20 | description: "Use PRs with activity since this date or git reference" 21 | required: false 22 | since_last_stable: 23 | description: "Use PRs with activity since the last stable git tag" 24 | required: false 25 | type: boolean 26 | jobs: 27 | prep_release: 28 | runs-on: ubuntu-latest 29 | permissions: 30 | contents: write 31 | steps: 32 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 33 | 34 | - name: Prep Release 35 | id: prep-release 36 | uses: jupyter-server/jupyter_releaser/.github/actions/prep-release@v2 37 | with: 38 | token: ${{ secrets.GITHUB_TOKEN }} 39 | version_spec: ${{ github.event.inputs.version_spec }} 40 | silent: ${{ github.event.inputs.silent }} 41 | post_version_spec: ${{ github.event.inputs.post_version_spec }} 42 | target: ${{ github.event.inputs.target }} 43 | branch: ${{ github.event.inputs.branch }} 44 | since: ${{ github.event.inputs.since }} 45 | since_last_stable: ${{ github.event.inputs.since_last_stable }} 46 | 47 | - name: "** Next Step **" 48 | run: | 49 | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" 50 | -------------------------------------------------------------------------------- /.github/workflows/publish-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Publish Changelog" 2 | on: 3 | release: 4 | types: [published] 5 | 6 | workflow_dispatch: 7 | inputs: 8 | branch: 9 | description: "The branch to target" 10 | required: false 11 | 12 | jobs: 13 | publish_changelog: 14 | runs-on: ubuntu-latest 15 | environment: release 16 | steps: 17 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 18 | 19 | - uses: actions/create-github-app-token@v1 20 | id: app-token 21 | with: 22 | app-id: ${{ vars.APP_ID }} 23 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 24 | 25 | - name: Publish changelog 26 | id: publish-changelog 27 | uses: jupyter-server/jupyter_releaser/.github/actions/publish-changelog@v2 28 | with: 29 | token: ${{ steps.app-token.outputs.token }} 30 | branch: ${{ github.event.inputs.branch }} 31 | 32 | - name: "** Next Step **" 33 | run: | 34 | echo "Merge the changelog update PR: ${{ steps.publish-changelog.outputs.pr_url }}" 35 | -------------------------------------------------------------------------------- /.github/workflows/publish-release.yml: -------------------------------------------------------------------------------- 1 | name: "Step 2: Publish Release" 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | branch: 6 | description: "The target branch" 7 | required: false 8 | release_url: 9 | description: "The URL of the draft GitHub release" 10 | required: false 11 | steps_to_skip: 12 | description: "Comma separated list of steps to skip" 13 | required: false 14 | 15 | jobs: 16 | publish_release: 17 | runs-on: ubuntu-latest 18 | environment: release 19 | permissions: 20 | id-token: write 21 | steps: 22 | - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 23 | 24 | - uses: actions/create-github-app-token@v1 25 | id: app-token 26 | with: 27 | app-id: ${{ vars.APP_ID }} 28 | private-key: ${{ secrets.APP_PRIVATE_KEY }} 29 | 30 | - name: Populate Release 31 | id: populate-release 32 | uses: jupyter-server/jupyter_releaser/.github/actions/populate-release@v2 33 | with: 34 | token: ${{ steps.app-token.outputs.token }} 35 | branch: ${{ github.event.inputs.branch }} 36 | release_url: ${{ github.event.inputs.release_url }} 37 | steps_to_skip: ${{ github.event.inputs.steps_to_skip }} 38 | 39 | - name: Finalize Release 40 | id: finalize-release 41 | uses: jupyter-server/jupyter_releaser/.github/actions/finalize-release@v2 42 | with: 43 | token: ${{ steps.app-token.outputs.token }} 44 | release_url: ${{ steps.populate-release.outputs.release_url }} 45 | 46 | - name: "** Next Step **" 47 | if: ${{ success() }} 48 | run: | 49 | echo "Verify the final release" 50 | echo ${{ steps.finalize-release.outputs.release_url }} 51 | 52 | - name: "** Failure Message **" 53 | if: ${{ failure() }} 54 | run: | 55 | echo "Failed to Publish the Draft Release Url:" 56 | echo ${{ steps.populate-release.outputs.release_url }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | dist/ 3 | *.egg-info/ 4 | __pycache__/ 5 | lib/ 6 | test/build/* 7 | node_modules/ 8 | .pytest_cache/ 9 | .vscode/ 10 | .idea/ 11 | .ipynb_checkpoints/ 12 | npm-debug.log 13 | package-lock.json 14 | .yarn 15 | *.tsbuildinfo 16 | *.stylelintcache 17 | jupyterlab_github/labextension 18 | 19 | # Version file handled by hatch 20 | jupyterlab_github/_version.py 21 | 22 | # Created by https://www.gitignore.io/api/python 23 | # Edit at https://www.gitignore.io/?templates=python 24 | 25 | ### Python ### 26 | # Byte-compiled / optimized / DLL files 27 | *.py[cod] 28 | *$py.class 29 | 30 | # C extensions 31 | *.so 32 | 33 | # Distribution / packaging 34 | .Python 35 | build/ 36 | develop-eggs/ 37 | dist/ 38 | downloads/ 39 | eggs/ 40 | .eggs/ 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | wheels/ 46 | pip-wheel-metadata/ 47 | share/python-wheels/ 48 | .installed.cfg 49 | *.egg 50 | MANIFEST 51 | 52 | # PyInstaller 53 | # Usually these files are written by a python script from a template 54 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 55 | *.manifest 56 | *.spec 57 | 58 | # Installer logs 59 | pip-log.txt 60 | pip-delete-this-directory.txt 61 | 62 | # Unit test / coverage reports 63 | htmlcov/ 64 | .tox/ 65 | .nox/ 66 | .coverage 67 | .coverage.* 68 | .cache 69 | nosetests.xml 70 | coverage.xml 71 | *.cover 72 | .hypothesis/ 73 | .pytest_cache/ 74 | 75 | # Translations 76 | *.mo 77 | *.pot 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # pyenv 89 | .python-version 90 | 91 | # celery beat schedule file 92 | celerybeat-schedule 93 | 94 | # SageMath parsed files 95 | *.sage.py 96 | 97 | # Spyder project settings 98 | .spyderproject 99 | .spyproject 100 | 101 | # Rope project settings 102 | .ropeproject 103 | 104 | # Mr Developer 105 | .mr.developer.cfg 106 | .project 107 | .pydevproject 108 | 109 | # mkdocs documentation 110 | /site 111 | 112 | # mypy 113 | .mypy_cache/ 114 | .dmypy.json 115 | dmypy.json 116 | 117 | # Pyre type checker 118 | .pyre/ 119 | 120 | # End of https://www.gitignore.io/api/python 121 | 122 | # OSX files 123 | .DS_Store 124 | 125 | # envrc 126 | .envrc 127 | 128 | # direnv 129 | .direnv 130 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | jupyterlab_github 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none", 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /.stylelintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "stylelint-config-recommended", 4 | "stylelint-config-standard", 5 | "stylelint-prettier/recommended" 6 | ], 7 | "rules": { 8 | "no-empty-source": null, 9 | "selector-class-pattern": null, 10 | "property-no-vendor-prefix": null, 11 | "selector-no-vendor-prefix": null, 12 | "value-no-vendor-prefix": null 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '14' 4 | sudo: false 5 | notifications: 6 | email: false 7 | before_install: 8 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O ~/miniconda.sh; 9 | - bash ~/miniconda.sh -b -p $HOME/miniconda 10 | - export PATH="$HOME/miniconda/bin:$PATH" 11 | - pip install jupyterlab 12 | install: 13 | - jlpm install 14 | - jlpm build 15 | - jupyter labextension install . 16 | script: 17 | - echo "Build successful" 18 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | enableImmutableInstalls: false 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## 4.0.0 6 | 7 | ([Full Changelog](https://github.com/jupyterlab/jupyterlab-github/compare/v3.0.1...5cff452e669571d6da9ad5874368b9fe793d567a)) 8 | 9 | ### Maintenance and upkeep improvements 10 | 11 | - Rename default branch to `main` [#148](https://github.com/jupyterlab/jupyterlab-github/pull/148) ([@jtpio](https://github.com/jtpio)) 12 | - Upgrade extension to JupyterLab 4 [#145](https://github.com/jupyterlab/jupyterlab-github/pull/145) ([@mahendrapaipuri](https://github.com/mahendrapaipuri)) 13 | - Optional ILayoutRestorer [#128](https://github.com/jupyterlab/jupyterlab-github/pull/128) ([@jtpio](https://github.com/jtpio)) 14 | - Add the release environment to the release workflow [#152](https://github.com/jupyterlab/jupyterlab-github/pull/152) ([@jtpio](https://github.com/jtpio)) 15 | - Fix publish workflow [#150](https://github.com/jupyterlab/jupyterlab-github/pull/150) ([@jtpio](https://github.com/jtpio)) 16 | 17 | ### Contributors to this release 18 | 19 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2021-11-27&to=2023-08-03&type=c)) 20 | 21 | [@github-actions](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Agithub-actions+updated%3A2021-11-27..2023-08-03&type=Issues) | [@jtpio](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Ajtpio+updated%3A2021-11-27..2023-08-03&type=Issues) | [@mahendrapaipuri](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Amahendrapaipuri+updated%3A2021-11-27..2023-08-03&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Awelcome+updated%3A2021-11-27..2023-08-03&type=Issues) 22 | 23 | 24 | 25 | ## v3.0.1 26 | 27 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/bdf7f93...fe08169)) 28 | 29 | ### Merged PRs 30 | 31 | - Release 3.0.1 to pick up updated readme file [#127](https://github.com/jupyterlab/jupyterlab-github/pull/127) ([@krassowski](https://github.com/krassowski)) 32 | - Prepare 3.0.0 release to PyPI with prebuilt extension [#126](https://github.com/jupyterlab/jupyterlab-github/pull/126) ([@krassowski](https://github.com/krassowski)) 33 | - Bump browserslist from 4.16.3 to 4.17.4 [#124](https://github.com/jupyterlab/jupyterlab-github/pull/124) ([@dependabot](https://github.com/dependabot)) 34 | - Bump glob-parent from 5.1.1 to 5.1.2 [#123](https://github.com/jupyterlab/jupyterlab-github/pull/123) ([@dependabot](https://github.com/dependabot)) 35 | - Bump postcss from 7.0.35 to 7.0.39 [#122](https://github.com/jupyterlab/jupyterlab-github/pull/122) ([@dependabot](https://github.com/dependabot)) 36 | - Bump ws from 7.2.1 to 7.5.5 [#121](https://github.com/jupyterlab/jupyterlab-github/pull/121) ([@dependabot](https://github.com/dependabot)) 37 | - Bump normalize-url from 4.5.0 to 4.5.1 [#120](https://github.com/jupyterlab/jupyterlab-github/pull/120) ([@dependabot](https://github.com/dependabot)) 38 | - Bump tar from 6.1.0 to 6.1.11 [#119](https://github.com/jupyterlab/jupyterlab-github/pull/119) ([@dependabot](https://github.com/dependabot)) 39 | - Bump path-parse from 1.0.6 to 1.0.7 [#117](https://github.com/jupyterlab/jupyterlab-github/pull/117) ([@dependabot](https://github.com/dependabot)) 40 | - Bump hosted-git-info from 2.8.8 to 2.8.9 [#114](https://github.com/jupyterlab/jupyterlab-github/pull/114) ([@dependabot](https://github.com/dependabot)) 41 | - Bump lodash from 4.17.15 to 4.17.21 [#113](https://github.com/jupyterlab/jupyterlab-github/pull/113) ([@dependabot](https://github.com/dependabot)) 42 | - JupyterLab 3.0 [#112](https://github.com/jupyterlab/jupyterlab-github/pull/112) ([@khwj](https://github.com/khwj)) 43 | - Bump node-fetch from 2.6.0 to 2.6.1 [#108](https://github.com/jupyterlab/jupyterlab-github/pull/108) ([@dependabot](https://github.com/dependabot)) 44 | - Jupyterlab 2.0 [#98](https://github.com/jupyterlab/jupyterlab-github/pull/98) ([@ian-r-rose](https://github.com/ian-r-rose)) 45 | 46 | ### Contributors to this release 47 | 48 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2019-07-26&to=2021-11-27&type=c)) 49 | 50 | [@dependabot](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adependabot+updated%3A2019-07-26..2021-11-27&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2019-07-26..2021-11-27&type=Issues) | [@jhgoebbert](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Ajhgoebbert+updated%3A2019-07-26..2021-11-27&type=Issues) | [@khwj](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Akhwj+updated%3A2019-07-26..2021-11-27&type=Issues) | [@krassowski](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Akrassowski+updated%3A2019-07-26..2021-11-27&type=Issues) | [@welcome](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Awelcome+updated%3A2019-07-26..2021-11-27&type=Issues) 51 | 52 | ## v1.0.1 53 | 54 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/a7f02c9...bdf7f93)) 55 | 56 | ### Bugs fixed 57 | 58 | - The repo-to-directory conversion should include the owner in the path. [#89](https://github.com/jupyterlab/jupyterlab-github/pull/89) ([@ian-r-rose](https://github.com/ian-r-rose)) 59 | 60 | ### Other merged PRs 61 | 62 | - Bump lodash from 4.17.11 to 4.17.14 [#88](https://github.com/jupyterlab/jupyterlab-github/pull/88) ([@dependabot](https://github.com/dependabot)) 63 | - Bump lodash.mergewith from 4.6.1 to 4.6.2 [#87](https://github.com/jupyterlab/jupyterlab-github/pull/87) ([@dependabot](https://github.com/dependabot)) 64 | 65 | ### Contributors to this release 66 | 67 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2019-07-08&to=2019-07-26&type=c)) 68 | 69 | [@dependabot](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adependabot+updated%3A2019-07-08..2019-07-26&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2019-07-08..2019-07-26&type=Issues) 70 | 71 | ## v1.0.0 72 | 73 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/fb157f7...a7f02c9)) 74 | 75 | ### Maintenance and upkeep improvements 76 | 77 | - Simplify input logic for username selector, avoids weird swapping of DOM elements. [#78](https://github.com/jupyterlab/jupyterlab-github/pull/78) ([@ian-r-rose](https://github.com/ian-r-rose)) 78 | - Update for JLab 1.0 [#77](https://github.com/jupyterlab/jupyterlab-github/pull/77) ([@ian-r-rose](https://github.com/ian-r-rose)) 79 | 80 | ### Other merged PRs 81 | 82 | - Remove deprecated OAuth authentication. [#86](https://github.com/jupyterlab/jupyterlab-github/pull/86) ([@ian-r-rose](https://github.com/ian-r-rose)) 83 | - Bump js-yaml from 3.12.0 to 3.13.1 [#85](https://github.com/jupyterlab/jupyterlab-github/pull/85) ([@dependabot](https://github.com/dependabot)) 84 | - Update to JupyterLab 1.0.0-alpha.6 [#82](https://github.com/jupyterlab/jupyterlab-github/pull/82) ([@lresende](https://github.com/lresende)) 85 | - Remove duplicated text [#81](https://github.com/jupyterlab/jupyterlab-github/pull/81) ([@gcbeltramini](https://github.com/gcbeltramini)) 86 | 87 | ### Contributors to this release 88 | 89 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-10-05&to=2019-07-08&type=c)) 90 | 91 | [@dependabot](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adependabot+updated%3A2018-10-05..2019-07-08&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-10-05..2019-07-08&type=Issues) | [@gcbeltramini](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Agcbeltramini+updated%3A2018-10-05..2019-07-08&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-10-05..2019-07-08&type=Issues) | [@lresende](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Alresende+updated%3A2018-10-05..2019-07-08&type=Issues) 92 | 93 | ## v0.10.0 94 | 95 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/ebd6d9a...fb157f7)) 96 | 97 | ### Enhancements made 98 | 99 | - Allow setting the accessToken from JupyterLab [#70](https://github.com/jupyterlab/jupyterlab-github/pull/70) ([@dhirschfeld](https://github.com/dhirschfeld)) 100 | 101 | ### Maintenance and upkeep improvements 102 | 103 | - Updates for JupyterLab v0.35. [#73](https://github.com/jupyterlab/jupyterlab-github/pull/73) ([@ian-r-rose](https://github.com/ian-r-rose)) 104 | - Updates for JupyterLab 0.34 [#67](https://github.com/jupyterlab/jupyterlab-github/pull/67) ([@ian-r-rose](https://github.com/ian-r-rose)) 105 | 106 | ### Contributors to this release 107 | 108 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-08-18&to=2018-10-05&type=c)) 109 | 110 | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-08-18..2018-10-05&type=Issues) | [@ellisonbg](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aellisonbg+updated%3A2018-08-18..2018-10-05&type=Issues) | [@firasm](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Afirasm+updated%3A2018-08-18..2018-10-05&type=Issues) | [@fm75](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Afm75+updated%3A2018-08-18..2018-10-05&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-08-18..2018-10-05&type=Issues) 111 | 112 | ## v0.9.0 113 | 114 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/a1bdbac...ebd6d9a)) 115 | 116 | ### Maintenance and upkeep improvements 117 | 118 | - Updates for 0.33 [#60](https://github.com/jupyterlab/jupyterlab-github/pull/60) ([@ian-r-rose](https://github.com/ian-r-rose)) 119 | 120 | ### Other merged PRs 121 | 122 | - Update .gitignore [#65](https://github.com/jupyterlab/jupyterlab-github/pull/65) ([@dhirschfeld](https://github.com/dhirschfeld)) 123 | 124 | ### Contributors to this release 125 | 126 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-07-25&to=2018-08-18&type=c)) 127 | 128 | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-07-25..2018-08-18&type=Issues) | [@ellisonbg](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aellisonbg+updated%3A2018-07-25..2018-08-18&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-07-25..2018-08-18&type=Issues) 129 | 130 | ## v0.8.0 131 | 132 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/08ca51c...a1bdbac)) 133 | 134 | ### Merged PRs 135 | 136 | - copy request for get_next_page [#63](https://github.com/jupyterlab/jupyterlab-github/pull/63) ([@ktong](https://github.com/ktong)) 137 | - Include LICENSE file in wheels [#58](https://github.com/jupyterlab/jupyterlab-github/pull/58) ([@toddrme2178](https://github.com/toddrme2178)) 138 | 139 | ### Contributors to this release 140 | 141 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-05-14&to=2018-07-25&type=c)) 142 | 143 | [@gabrielvrl](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Agabrielvrl+updated%3A2018-05-14..2018-07-25&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-05-14..2018-07-25&type=Issues) | [@ktong](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aktong+updated%3A2018-05-14..2018-07-25&type=Issues) | [@michaelaye](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Amichaelaye+updated%3A2018-05-14..2018-07-25&type=Issues) | [@toddrme2178](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Atoddrme2178+updated%3A2018-05-14..2018-07-25&type=Issues) 144 | 145 | ## v0.7.2 146 | 147 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/7a9550d...08ca51c)) 148 | 149 | ### Bugs fixed 150 | 151 | - Fix problem with redirects in Safari by removing spurious slash. [#56](https://github.com/jupyterlab/jupyterlab-github/pull/56) ([@ian-r-rose](https://github.com/ian-r-rose)) 152 | 153 | ### Contributors to this release 154 | 155 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-04-23&to=2018-05-14&type=c)) 156 | 157 | [@gabrielvrl](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Agabrielvrl+updated%3A2018-04-23..2018-05-14&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-04-23..2018-05-14&type=Issues) | [@michaelaye](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Amichaelaye+updated%3A2018-04-23..2018-05-14&type=Issues) 158 | 159 | ## v0.7.1 160 | 161 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/66433b2...7a9550d)) 162 | 163 | ### Merged PRs 164 | 165 | - Add ability to configure the GitHub base url [#52](https://github.com/jupyterlab/jupyterlab-github/pull/52) ([@dhirschfeld](https://github.com/dhirschfeld)) 166 | 167 | ### Contributors to this release 168 | 169 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-04-20&to=2018-04-23&type=c)) 170 | 171 | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-04-20..2018-04-23&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-04-20..2018-04-23&type=Issues) 172 | 173 | ## v0.7.0 174 | 175 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/2fd67f2...66433b2)) 176 | 177 | ### Enhancements made 178 | 179 | - Allow setting a default repository at launch. [#50](https://github.com/jupyterlab/jupyterlab-github/pull/50) ([@ian-r-rose](https://github.com/ian-r-rose)) 180 | - Allow for private repos to be viewed if the user is authenticated. [#48](https://github.com/jupyterlab/jupyterlab-github/pull/48) ([@ian-r-rose](https://github.com/ian-r-rose)) 181 | - Change to using access tokens and make the api url configurable. [#47](https://github.com/jupyterlab/jupyterlab-github/pull/47) ([@dhirschfeld](https://github.com/dhirschfeld)) 182 | 183 | ### Bugs fixed 184 | 185 | - Conditionally use err.response. [#51](https://github.com/jupyterlab/jupyterlab-github/pull/51) ([@ian-r-rose](https://github.com/ian-r-rose)) 186 | 187 | ### Maintenance and upkeep improvements 188 | 189 | - Updates for JupyterLab v0.32 [#44](https://github.com/jupyterlab/jupyterlab-github/pull/44) ([@ian-r-rose](https://github.com/ian-r-rose)) 190 | 191 | ### Other merged PRs 192 | 193 | - Update docs to include access_token instructions. [#49](https://github.com/jupyterlab/jupyterlab-github/pull/49) ([@ian-r-rose](https://github.com/ian-r-rose)) 194 | 195 | ### Contributors to this release 196 | 197 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-04-13&to=2018-04-20&type=c)) 198 | 199 | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-04-13..2018-04-20&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-04-13..2018-04-20&type=Issues) 200 | 201 | ## v0.6.0 202 | 203 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/7e506c2...2fd67f2)) 204 | 205 | ### Maintenance and upkeep improvements 206 | 207 | - Updates for JupyterLab v0.32 [#44](https://github.com/jupyterlab/jupyterlab-github/pull/44) ([@ian-r-rose](https://github.com/ian-r-rose)) 208 | 209 | ### Other merged PRs 210 | 211 | - Use textencoder [#41](https://github.com/jupyterlab/jupyterlab-github/pull/41) ([@ian-r-rose](https://github.com/ian-r-rose)) 212 | 213 | ### Contributors to this release 214 | 215 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-03-23&to=2018-04-13&type=c)) 216 | 217 | [@beenje](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Abeenje+updated%3A2018-03-23..2018-04-13&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2018-03-23..2018-04-13&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-03-23..2018-04-13&type=Issues) 218 | 219 | ## v0.5.1 220 | 221 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/429c6fa...7e506c2)) 222 | 223 | ### Enhancements made 224 | 225 | - Binder tree handler [#33](https://github.com/jupyterlab/jupyterlab-github/pull/33) ([@ian-r-rose](https://github.com/ian-r-rose)) 226 | 227 | ### Bugs fixed 228 | 229 | - Fix decoding of base64 strings to Unicode. [#40](https://github.com/jupyterlab/jupyterlab-github/pull/40) ([@ian-r-rose](https://github.com/ian-r-rose)) 230 | - Binder tree handler [#33](https://github.com/jupyterlab/jupyterlab-github/pull/33) ([@ian-r-rose](https://github.com/ian-r-rose)) 231 | 232 | ### Maintenance and upkeep improvements 233 | 234 | - Update keyword [#37](https://github.com/jupyterlab/jupyterlab-github/pull/37) ([@ian-r-rose](https://github.com/ian-r-rose)) 235 | - Update packaging to use new conf.d approach in Notebook 5.3 [#32](https://github.com/jupyterlab/jupyterlab-github/pull/32) ([@ian-r-rose](https://github.com/ian-r-rose)) 236 | - Updates for JupyterLab 0.31rc2 [#31](https://github.com/jupyterlab/jupyterlab-github/pull/31) ([@ian-r-rose](https://github.com/ian-r-rose)) 237 | 238 | ### Other merged PRs 239 | 240 | - Update README.md [#38](https://github.com/jupyterlab/jupyterlab-github/pull/38) ([@andersy005](https://github.com/andersy005)) 241 | - Add package metadata for discovery compatibility [#36](https://github.com/jupyterlab/jupyterlab-github/pull/36) ([@vidartf](https://github.com/vidartf)) 242 | 243 | ### Contributors to this release 244 | 245 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2018-01-10&to=2018-03-23&type=c)) 246 | 247 | [@andersy005](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aandersy005+updated%3A2018-01-10..2018-03-23&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2018-01-10..2018-03-23&type=Issues) | [@jasongrout](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Ajasongrout+updated%3A2018-01-10..2018-03-23&type=Issues) | [@vidartf](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Avidartf+updated%3A2018-01-10..2018-03-23&type=Issues) 248 | 249 | ## v0.5.0 250 | 251 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/c587338...429c6fa)) 252 | 253 | ### Maintenance and upkeep improvements 254 | 255 | - Use new path parsing. [#29](https://github.com/jupyterlab/jupyterlab-github/pull/29) ([@ian-r-rose](https://github.com/ian-r-rose)) 256 | - Updates for JLab 0.31 [#27](https://github.com/jupyterlab/jupyterlab-github/pull/27) ([@ian-r-rose](https://github.com/ian-r-rose)) 257 | - Update services [#26](https://github.com/jupyterlab/jupyterlab-github/pull/26) ([@ian-r-rose](https://github.com/ian-r-rose)) 258 | 259 | ### Other merged PRs 260 | 261 | - Fix capitalization of JupyterLab in README [#28](https://github.com/jupyterlab/jupyterlab-github/pull/28) ([@willingc](https://github.com/willingc)) 262 | 263 | ### Contributors to this release 264 | 265 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-12-05&to=2018-01-10&type=c)) 266 | 267 | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-12-05..2018-01-10&type=Issues) | [@willingc](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Awillingc+updated%3A2017-12-05..2018-01-10&type=Issues) 268 | 269 | ## v0.3.1 270 | 271 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/e86e0e0...c587338)) 272 | 273 | ### Bugs fixed 274 | 275 | - Fix for opening empty file. [#25](https://github.com/jupyterlab/jupyterlab-github/pull/25) ([@ian-r-rose](https://github.com/ian-r-rose)) 276 | 277 | ### Other merged PRs 278 | 279 | - Updates for JupyterLab 0.30 [#24](https://github.com/jupyterlab/jupyterlab-github/pull/24) ([@ian-r-rose](https://github.com/ian-r-rose)) 280 | 281 | ### Contributors to this release 282 | 283 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-12-04&to=2017-12-05&type=c)) 284 | 285 | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-12-04..2017-12-05&type=Issues) 286 | 287 | ## v0.3.0 288 | 289 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/dbee1d2...e86e0e0)) 290 | 291 | ### Merged PRs 292 | 293 | - Updates for JupyterLab 0.30 [#24](https://github.com/jupyterlab/jupyterlab-github/pull/24) ([@ian-r-rose](https://github.com/ian-r-rose)) 294 | - Handle symlinks, and show an error if the user tries to open a submodule [#21](https://github.com/jupyterlab/jupyterlab-github/pull/21) ([@ian-r-rose](https://github.com/ian-r-rose)) 295 | - Org in path [#20](https://github.com/jupyterlab/jupyterlab-github/pull/20) ([@ian-r-rose](https://github.com/ian-r-rose)) 296 | - Add mybinder badge [#19](https://github.com/jupyterlab/jupyterlab-github/pull/19) ([@ian-r-rose](https://github.com/ian-r-rose)) 297 | 298 | ### Contributors to this release 299 | 300 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-11-10&to=2017-12-04&type=c)) 301 | 302 | [@choldgraf](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Acholdgraf+updated%3A2017-11-10..2017-12-04&type=Issues) | [@dhirschfeld](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Adhirschfeld+updated%3A2017-11-10..2017-12-04&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-11-10..2017-12-04&type=Issues) | [@michaelaye](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Amichaelaye+updated%3A2017-11-10..2017-12-04&type=Issues) | [@willingc](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Awillingc+updated%3A2017-11-10..2017-12-04&type=Issues) 303 | 304 | ## v0.2.0 305 | 306 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/d994409...dbee1d2)) 307 | 308 | ### Merged PRs 309 | 310 | - Updating to jupyterlab 0.29.x [#16](https://github.com/jupyterlab/jupyterlab-github/pull/16) ([@tkinz27](https://github.com/tkinz27)) 311 | - Use smaller error panel image to cut down on bundle size. [#14](https://github.com/jupyterlab/jupyterlab-github/pull/14) ([@ian-r-rose](https://github.com/ian-r-rose)) 312 | - Use base_url so that the extension works in a JupyterHub setting. [#13](https://github.com/jupyterlab/jupyterlab-github/pull/13) ([@ian-r-rose](https://github.com/ian-r-rose)) 313 | 314 | ### Contributors to this release 315 | 316 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-10-25&to=2017-11-10&type=c)) 317 | 318 | [@athornton](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aathornton+updated%3A2017-10-25..2017-11-10&type=Issues) | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-10-25..2017-11-10&type=Issues) | [@tkinz27](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Atkinz27+updated%3A2017-10-25..2017-11-10&type=Issues) 319 | 320 | ## v0.1.1 321 | 322 | ([full changelog](https://github.com/jupyterlab/jupyterlab-github/compare/22d487a...d994409)) 323 | 324 | ### Merged PRs 325 | 326 | - Escape URLs [#11](https://github.com/jupyterlab/jupyterlab-github/pull/11) ([@ian-r-rose](https://github.com/ian-r-rose)) 327 | - Decode response body to utf-8 for python 3.5 compatibility. [#10](https://github.com/jupyterlab/jupyterlab-github/pull/10) ([@ian-r-rose](https://github.com/ian-r-rose)) 328 | 329 | ### Contributors to this release 330 | 331 | ([GitHub contributors page for this release](https://github.com/jupyterlab/jupyterlab-github/graphs/contributors?from=2017-10-21&to=2017-10-25&type=c)) 332 | 333 | [@ian-r-rose](https://github.com/search?q=repo%3Ajupyterlab%2Fjupyterlab-github+involves%3Aian-r-rose+updated%3A2017-10-21..2017-10-25&type=Issues) 334 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Project Jupyter Contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JupyterLab GitHub 2 | 3 | [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/jupyterlab/jupyterlab-github/main?urlpath=lab) 4 | 5 | A JupyterLab extension for accessing GitHub repositories. 6 | 7 | ### What this extension is 8 | 9 | When you install this extension, an additional filebrowser tab will be added 10 | to the left area of JupyterLab. This filebrowser allows you to select GitHub 11 | organizations and users, browse their repositories, and open the files in those 12 | repositories. If those files are notebooks, you can run them just as you would 13 | any other notebook. You can also attach a kernel to text files and run those. 14 | Basically, you should be able to open any file in a repository that JupyterLab can handle. 15 | 16 | Here is a screenshot of the plugin opening this very file on GitHub: 17 | ![gitception](https://raw.githubusercontent.com/jupyterlab/jupyterlab-github/main/gitception.png 'Gitception') 18 | 19 | ### What this extension is not 20 | 21 | This is not an extension that provides full GitHub access, such as 22 | saving files, making commits, forking repositories, etc. 23 | For it to be so, it would need to more-or-less reinvent the GitHub website, 24 | which represents a huge increase in complexity for the extension. 25 | 26 | ### A note on rate-limiting 27 | 28 | This extension has both a client-side component (that is, JavaScript that is bundled 29 | with JupyterLab), and a server-side component (that is, Python code that is added 30 | to the Jupyter server). This extension _will_ work with out the server extension, 31 | with a major caveat: when making unauthenticated requests to GitHub 32 | (as we must do to get repository data), GitHub imposes fairly strict rate-limits 33 | on how many requests we can make. As such, you are likely to hit that limit 34 | within a few minutes of work. You will then have to wait up to an hour to regain access. 35 | 36 | For that reason, we recommend that you take the time and effort to set up the server 37 | extension as well as the lab extension, which will allow you to access higher rate-limits. 38 | This process is described in the [installation](#Installation) section. 39 | 40 | ## Prerequisites 41 | 42 | - JupyterLab > 3.0 43 | - A GitHub account for the server extension 44 | 45 | ## Installation 46 | 47 | As discussed above, this extension has both a server extension and a lab extension. 48 | Both extensions will be installed by default when installing from PyPI, but you may 49 | have only lab extension installed if you used the Extension Manager in JupyterLab. 50 | 51 | We recommend completing the steps described below as to not be rate-limited. 52 | The purpose of the server extension is to add GitHub credentials that you will need to acquire 53 | from https://github.com/settings/developers, and then to proxy your request to GitHub. 54 | 55 | For JupyterLab version older than 3 please see the instructions on the 56 | [2.x branch](https://github.com/jupyterlab/jupyterlab-github/tree/2.x). 57 | 58 | ### 1. Installing both server and prebuilt lab extension 59 | 60 | #### JupyterLab 4.x 61 | 62 | To install the both the server extension and (prebuilt) lab extension, enter the following in your terminal: 63 | 64 | ```bash 65 | pip install jupyterlab-github 66 | ``` 67 | 68 | #### JupyterLab 3.x 69 | 70 | We need to pin the extension version to `3.0.1` for making it work on the JupyterLab 3.x. 71 | 72 | ```bash 73 | pip install 'jupyterlab-github==3.0.1' 74 | ``` 75 | 76 | After restarting JupyterLab, the extension should work, and you can experience 77 | the joys of being rate-limited first-hand! 78 | 79 | ### 2. Getting your credentials from GitHub 80 | 81 | There are two approaches to getting credentials from GitHub: 82 | (1) you can get an access token, (2) you can register an OAuth app. 83 | The second approach is not recommended, and will be removed in a future release. 84 | 85 | #### Getting an access token (**recommended**) 86 | 87 | You can get an access token by following these steps: 88 | 89 | 1. [Verify](https://help.github.com/articles/verifying-your-email-address) your email address with GitHub. 90 | 1. Go to your account settings on GitHub and select "Developer Settings" from the left panel. 91 | 1. On the left, select "Personal access tokens" 92 | 1. Click the "Generate new token" button, and enter your password. 93 | 1. Give the token a description, and check the "**repo**" scope box. 94 | 1. Click "Generate token" 95 | 1. You should be given a string which will be your access token. 96 | 97 | Remember that this token is effectively a password for your GitHub account. 98 | _Do not_ share it online or check the token into version control, 99 | as people can use it to access all of your data on GitHub. 100 | 101 | #### Setting up an OAuth application (**deprecated**) 102 | 103 | This approach to authenticating with GitHub is deprecated, and will be removed in a future release. 104 | New users should use the access token approach. 105 | You can register an OAuth application with GitHub by following these steps: 106 | 107 | 1. Log into your GitHub account. 108 | 1. Go to https://github.com/settings/developers and select the "OAuth Apps" tab on the left. 109 | 1. Click the "New OAuth App" button. 110 | 1. Fill out a name, homepage URL, description, and callback URL in the form. 111 | This extension does not actually use OAuth, so these values actually _do not matter much_, 112 | you just need to enter them to register the application. 113 | 1. Click the "Register application" button. 114 | 1. You should be taken to a new page with the new application information. 115 | If you see fields showing "Client ID" and "Client Secret", congratulations! 116 | These are the strings we need, and you have successfuly set up the application. 117 | 118 | It is important to note that the "Client Secret" string is, as the name suggests, a secret. 119 | _Do not_ share this value online, as people may be able to use it to impersonate you on GitHub. 120 | 121 | ### 3. Enabling and configuring the server extension 122 | 123 | The server extension will be enabled by default on new JupyterLab installations 124 | if you installed it with pip. If you used Extension Manager in JupyterLab, 125 | please uninstall the extension and install it again with the instructions from point (1). 126 | 127 | Confirm that the server extension is installed and enabled with: 128 | 129 | ```bash 130 | jupyter server extension list 131 | ``` 132 | 133 | you should see the following: 134 | 135 | ``` 136 | - Validating jupyterlab_github... 137 | jupyterlab_github 4.0.0 OK 138 | ``` 139 | 140 | On some older installations (e.g. old JupyterHub versions) which use jupyter 141 | `notebook` server instead of the new `jupyter-server`, the extension needs to 142 | show up on the legacy `serverextensions` list (note: no space between _server_ and _extension_): 143 | 144 | ```bash 145 | jupyter serverextension list 146 | ``` 147 | 148 | If the extension is not enabled run: 149 | 150 | ```bash 151 | jupyter server extension enable jupyterlab_github 152 | ``` 153 | 154 | or if using the legacy `notebook` server: 155 | 156 | ```bash 157 | jupyter serverextension enable jupyterlab_github 158 | ``` 159 | 160 | You now need to add the credentials you got from GitHub 161 | to your server configuration file. Instructions for generating a configuration 162 | file can be found [here](https://jupyter-server.readthedocs.io/en/stable/users/configuration.html#configuring-a-jupyter-server). 163 | Once you have identified this file, add the following lines to it: 164 | 165 | ```python 166 | c.GitHubConfig.access_token = '< YOUR_ACCESS_TOKEN >' 167 | ``` 168 | 169 | where "`< YOUR_ACCESS_TOKEN >`" is the string value you obtained above. 170 | If you generated an OAuth app, instead enter the following: 171 | 172 | ```python 173 | c.GitHubConfig.client_id = '< YOUR_CLIENT_ID >' 174 | c.GitHubConfig.client_secret = '< YOUR_CLIENT_SECRET >' 175 | ``` 176 | 177 | where "`< YOUR_CLIENT_ID >`" and "`< YOUR_CLIENT_SECRET >`" are the app values you obtained above. 178 | 179 | With this, you should be done! Launch JupyterLab and look for the GitHub tab on the left! 180 | 181 | ## Customization 182 | 183 | You can set the plugin to start showing a particular repository at launch time. 184 | Open the "Advanced Settings" editor in the Settings menu, 185 | and under the GitHub settings add 186 | 187 | ```json 188 | { 189 | "defaultRepo": "owner/repository" 190 | } 191 | ``` 192 | 193 | where `owner` is the GitHub user/org, 194 | and `repository` is the name of the repository you want to open. 195 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a Release 2 | 3 | The recommended way to make a release is to use [`jupyter_releaser`](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html). 4 | -------------------------------------------------------------------------------- /binder/environment.yml: -------------------------------------------------------------------------------- 1 | # a mybinder.org-ready environment for demoing jupyterlab-github 2 | # this environment may also be used locally on Linux/MacOS/Windows, e.g. 3 | # 4 | # conda env update --file binder/environment.yml 5 | # conda activate jupyterlab-github-demo 6 | # 7 | name: jupyterlab-github-demo 8 | 9 | channels: 10 | - conda-forge 11 | 12 | dependencies: 13 | # runtime dependencies 14 | - python >=3.8,<3.11.0 15 | - jupyterlab >=4,<5.0.0a0 16 | # labextension build dependencies 17 | - nodejs >=18,<19 18 | - pip 19 | - wheel 20 | # additional packages for demos 21 | # - ipywidgets 22 | 23 | -------------------------------------------------------------------------------- /binder/postBuild: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """ perform a development install of jupyterlab-github 3 | On Binder, this will run _after_ the environment has been fully created from 4 | the environment.yml in this directory. 5 | This script should also run locally on Linux/MacOS/Windows: 6 | python3 binder/postBuild 7 | """ 8 | import subprocess 9 | import sys 10 | from pathlib import Path 11 | 12 | 13 | ROOT = Path.cwd() 14 | 15 | def _(*args, **kwargs): 16 | """ Run a command, echoing the args 17 | fails hard if something goes wrong 18 | """ 19 | print("\n\t", " ".join(args), "\n") 20 | return_code = subprocess.call(args, **kwargs) 21 | if return_code != 0: 22 | print("\nERROR", return_code, " ".join(args)) 23 | sys.exit(return_code) 24 | 25 | # verify the environment is self-consistent before even starting 26 | _(sys.executable, "-m", "pip", "check") 27 | 28 | # install the labextension 29 | _(sys.executable, "-m", "pip", "install", "-e", ".") 30 | _(sys.executable, "-m", "jupyter", "labextension", "develop", "--overwrite", ".") 31 | 32 | # verify the environment the extension didn't break anything 33 | _(sys.executable, "-m", "pip", "check") 34 | 35 | # initially list installed extensions to determine if there are any surprises 36 | _("jupyter", "labextension", "list") 37 | 38 | 39 | print("JupyterLab with jupyterlab-github is ready to run with:\n") 40 | print("\tjupyter lab\n") 41 | -------------------------------------------------------------------------------- /gitception.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyterlab-github/6ab46858aa5dc0b93510b8fdb52e9066178847ea/gitception.png -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "jupyterlab_github", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package jupyterlab_github" 5 | } 6 | -------------------------------------------------------------------------------- /jupyter-config/jupyter_notebook_config.d/jupyterlab_github.json: -------------------------------------------------------------------------------- 1 | { 2 | "NotebookApp": { 3 | "nbserver_extensions": { 4 | "jupyterlab_github": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jupyter-config/jupyter_server_config.d/jupyterlab_github.json: -------------------------------------------------------------------------------- 1 | { 2 | "ServerApp": { 3 | "jpserver_extensions": { 4 | "jupyterlab_github": true 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /jupyterlab_github/__init__.py: -------------------------------------------------------------------------------- 1 | import re, json, copy 2 | 3 | from tornado import web 4 | from tornado.httputil import url_concat 5 | from tornado.httpclient import AsyncHTTPClient, HTTPRequest, HTTPError 6 | 7 | from traitlets import Unicode, Bool 8 | from traitlets.config import Configurable 9 | 10 | from jupyter_server.utils import url_path_join, url_escape 11 | from jupyter_server.base.handlers import APIHandler 12 | 13 | from ._version import __version__ 14 | 15 | 16 | link_regex = re.compile(r'<([^>]*)>;\s*rel="([\w]*)\"') 17 | 18 | 19 | class GitHubConfig(Configurable): 20 | """ 21 | Allows configuration of access to the GitHub api 22 | """ 23 | allow_client_side_access_token = Bool( 24 | False, 25 | help=( 26 | "If True the access token specified in the JupyterLab settings " 27 | "will take precedence. If False the token specified in JupyterLab " 28 | "will be ignored. Storing your access token in the client can " 29 | "present a security risk so be careful if enabling this setting." 30 | ) 31 | ).tag(config=True) 32 | 33 | api_url = Unicode( 34 | 'https://api.github.com', 35 | help="The url for the GitHub api" 36 | ).tag(config=True) 37 | 38 | access_token = Unicode( 39 | '', 40 | help="A personal access token for GitHub." 41 | ).tag(config=True) 42 | 43 | validate_cert = Bool( 44 | True, 45 | help=( 46 | "Whether to validate the servers' SSL certificate on requests " 47 | "made to the GitHub api. In general this is a bad idea so only " 48 | "disable SSL validation if you know what you are doing!" 49 | ) 50 | ).tag(config=True) 51 | 52 | 53 | class GitHubHandler(APIHandler): 54 | """ 55 | A proxy for the GitHub API v3. 56 | 57 | The purpose of this proxy is to provide authentication to the API requests 58 | which allows for a higher rate limit. Without this, the rate limit on 59 | unauthenticated calls is so limited as to be practically useless. 60 | """ 61 | 62 | client = AsyncHTTPClient() 63 | 64 | @web.authenticated 65 | async def get(self, path): 66 | """ 67 | Proxy API requests to GitHub, adding authentication parameter(s) if 68 | they have been set. 69 | """ 70 | 71 | # Get access to the notebook config object 72 | c = GitHubConfig(config=self.config) 73 | try: 74 | query = self.request.query_arguments 75 | params = {key: query[key][0].decode() for key in query} 76 | api_path = url_path_join(c.api_url, url_escape(path)) 77 | params['per_page'] = 100 78 | 79 | access_token = params.pop('access_token', None) 80 | if access_token and c.allow_client_side_access_token == True: 81 | token = access_token 82 | elif access_token and c.allow_client_side_access_token == False: 83 | msg = ( 84 | "Client side (JupyterLab) access tokens have been " 85 | "disabled for security reasons.\nPlease remove your " 86 | "access token from JupyterLab and instead add it to " 87 | "your notebook configuration file:\n" 88 | "c.GitHubConfig.access_token = ''\n" 89 | ) 90 | raise HTTPError(403, msg) 91 | elif c.access_token != '': 92 | # Preferentially use the config access_token if set 93 | token = c.access_token 94 | else: 95 | token = '' 96 | 97 | api_path = url_concat(api_path, params) 98 | 99 | request = HTTPRequest( 100 | api_path, 101 | validate_cert=c.validate_cert, 102 | user_agent='JupyterLab GitHub', 103 | headers={"Authorization": "token {}".format(token)} 104 | ) 105 | response = await self.client.fetch(request) 106 | data = json.loads(response.body.decode('utf-8')) 107 | 108 | # Check if we need to paginate results. 109 | # If so, get pages until all the results 110 | # are loaded into the data buffer. 111 | next_page_path = self._maybe_get_next_page_path(response) 112 | while next_page_path: 113 | request = copy.copy(request) 114 | request.url = next_page_path 115 | response = await self.client.fetch(request) 116 | next_page_path = self._maybe_get_next_page_path(response) 117 | data.extend(json.loads(response.body.decode('utf-8'))) 118 | 119 | # Send the results back. 120 | self.finish(json.dumps(data)) 121 | 122 | except HTTPError as err: 123 | self.set_status(err.code) 124 | message = err.response.body if err.response else str(err.code) 125 | self.finish(message) 126 | 127 | def _maybe_get_next_page_path(self, response): 128 | # If there is a 'Link' header in the response, we 129 | # need to paginate. 130 | link_headers = response.headers.get_list('Link') 131 | next_page_path = None 132 | if link_headers: 133 | links = {} 134 | matched = link_regex.findall(link_headers[0]) 135 | for match in matched: 136 | links[match[1]] = match[0] 137 | next_page_path = links.get('next', None) 138 | 139 | return next_page_path 140 | 141 | 142 | def _jupyter_labextension_paths(): 143 | return [ 144 | { 145 | "src": "labextension", 146 | "dest": "@jupyterlab/github", 147 | } 148 | ] 149 | 150 | 151 | def _jupyter_server_extension_paths(): 152 | return [{ 153 | 'module': 'jupyterlab_github' 154 | }] 155 | 156 | 157 | def load_jupyter_server_extension(nb_server_app): 158 | """ 159 | Called when the extension is loaded. 160 | 161 | Args: 162 | nb_server_app (NotebookWebApplication): handle to the Notebook webserver instance. 163 | """ 164 | web_app = nb_server_app.web_app 165 | base_url = web_app.settings['base_url'] 166 | endpoint = url_path_join(base_url, 'github') 167 | handlers = [(endpoint + "(.*)", GitHubHandler)] 168 | web_app.add_handlers('.*$', handlers) 169 | -------------------------------------------------------------------------------- /jupyterlab_github/api/api.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: JupyterLab GitHub proxy 4 | description: Proxies GitHub API requests from JupyterLab to GitHub, optionally adding credentials to the request. 5 | version: 0.1.0 6 | 7 | paths: 8 | /github/{apiPath}: 9 | get: 10 | summary: Gets the resource at the apiPath for the GitHub API v3. 11 | parameters: 12 | - name: apiPath 13 | in: path 14 | required: true 15 | description: API path for GitHub v3. 16 | schema: 17 | type: string 18 | format: uri 19 | responses: 20 | '200': 21 | description: OK 22 | '404': 23 | description: Not found 24 | '403': 25 | description: Not authorized 26 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@jupyterlab/github", 3 | "version": "4.0.0", 4 | "description": "JupyterLab viewer for GitHub repositories", 5 | "keywords": [ 6 | "github", 7 | "jupyter", 8 | "jupyterlab", 9 | "jupyterlab-extension" 10 | ], 11 | "homepage": "https://github.com/jupyterlab/jupyterlab-github", 12 | "bugs": { 13 | "url": "https://github.com/jupyterlab/jupyterlab-github/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/jupyterlab/jupyterlab-github.git" 18 | }, 19 | "license": "BSD-3-Clause", 20 | "author": { 21 | "name": "Ian Rose", 22 | "email": "jupyter@googlegroups.com" 23 | }, 24 | "files": [ 25 | "lib/*/*d.ts", 26 | "lib/*/*.js", 27 | "lib/*.d.ts", 28 | "lib/*.js", 29 | "schema/*.json", 30 | "style/*.*", 31 | "style/index.js" 32 | ], 33 | "main": "lib/index.js", 34 | "types": "lib/index.d.ts", 35 | "directories": { 36 | "lib": "lib/" 37 | }, 38 | "scripts": { 39 | "build": "jlpm run build:lib && jlpm run build:labextension:dev", 40 | "build:labextension": "jupyter labextension build .", 41 | "build:labextension:dev": "jupyter labextension build --development True .", 42 | "build:lib": "tsc", 43 | "build:prod": "jlpm run build:lib && jlpm run build:labextension", 44 | "build:test": "cd test && ./build-tests.sh", 45 | "clean": "jlpm run clean:lib", 46 | "clean:all": "jlpm run clean:lib && jlpm run clean:labextension", 47 | "clean:labextension": "rimraf jupyterlab_github/labextension", 48 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 49 | "lint": "jlpm && jlpm prettier && jlpm eslint && jlpm stylelint", 50 | "lint:check": "jlpm prettier:check && jlpm eslint:check && jlpm stylelint:check", 51 | "eslint": "eslint . --ext .ts,.tsx --fix", 52 | "eslint:check": "eslint . --ext .ts,.tsx", 53 | "prettier": "prettier --write \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 54 | "prettier:check": "prettier --list-different \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 55 | "stylelint": "jlpm stylelint:check --fix", 56 | "stylelint:check": "stylelint --cache \"style/*.css\"", 57 | "stylelint:files": "stylelint --fix", 58 | "install:extension": "jupyter labextension develop --overwrite .", 59 | "precommit": "lint-staged", 60 | "test": "cd test && ./run-tests.sh", 61 | "watch": "run-p watch:src watch:labextension", 62 | "watch:labextension": "jupyter labextension watch .", 63 | "watch:src": "tsc -w" 64 | }, 65 | "lint-staged": { 66 | "**/*{.ts,.tsx,.css,.json,.md}": [ 67 | "prettier --write", 68 | "git add" 69 | ] 70 | }, 71 | "dependencies": { 72 | "@jupyterlab/application": "^4.0.0", 73 | "@jupyterlab/apputils": "^4.0.0", 74 | "@jupyterlab/coreutils": "^6.0.0", 75 | "@jupyterlab/docmanager": "^4.0.0", 76 | "@jupyterlab/docregistry": "^4.0.0", 77 | "@jupyterlab/filebrowser": "^4.0.0", 78 | "@jupyterlab/services": "^7.0.0", 79 | "@jupyterlab/settingregistry": "^4.0.0", 80 | "@jupyterlab/ui-components": "^4.0.0", 81 | "@lumino/algorithm": "^2.0.0", 82 | "@lumino/messaging": "^2.0.0", 83 | "@lumino/signaling": "^2.0.0", 84 | "@lumino/widgets": "^2.0.0", 85 | "base64-js": "^1.5.0" 86 | }, 87 | "devDependencies": { 88 | "@jupyterlab/builder": "^4.0.0", 89 | "@types/base64-js": "^1.3.0", 90 | "@types/text-encoding": "^0.0.35", 91 | "@typescript-eslint/eslint-plugin": "^5.55.0", 92 | "@typescript-eslint/eslint-plugin-tslint": "^6.1.0", 93 | "@typescript-eslint/parser": "^5.55.0", 94 | "eslint": "^8.36.0", 95 | "eslint-config-prettier": "^8.7.0", 96 | "eslint-plugin-import": "^2.27.5", 97 | "eslint-plugin-no-null": "^1.0.2", 98 | "eslint-plugin-prettier": "^4.2.1", 99 | "husky": "^8.0.0", 100 | "lint-staged": "^13.2.0", 101 | "mkdirp": "^1.0.3", 102 | "npm-run-all": "^4.1.5", 103 | "prettier": "^2.8.7", 104 | "rimraf": "^4.4.1", 105 | "stylelint": "^14.9.1", 106 | "stylelint-config-prettier": "^9.0.4", 107 | "stylelint-config-recommended": "^8.0.0", 108 | "stylelint-config-standard": "^26.0.0", 109 | "stylelint-prettier": "^2.0.0", 110 | "typescript": "~5.0.1" 111 | }, 112 | "jupyterlab": { 113 | "extension": true, 114 | "discovery": { 115 | "server": { 116 | "managers": [ 117 | "pip" 118 | ], 119 | "base": { 120 | "name": "jupyterlab_github" 121 | } 122 | } 123 | }, 124 | "schemaDir": "schema", 125 | "outputDir": "jupyterlab_github/labextension" 126 | }, 127 | "sideEffects": [ 128 | "style/*.css", 129 | "style/index.js" 130 | ], 131 | "styleModule": "style/index.js" 132 | } 133 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "jupyterlab_github" 7 | readme = "README.md" 8 | license = { file = "LICENSE" } 9 | requires-python = ">=3.8" 10 | classifiers = [ 11 | "Framework :: Jupyter", 12 | "Framework :: Jupyter :: JupyterLab", 13 | "Framework :: Jupyter :: JupyterLab :: 4", 14 | "Framework :: Jupyter :: JupyterLab :: Extensions", 15 | "Framework :: Jupyter :: JupyterLab :: Extensions :: Prebuilt", 16 | "License :: OSI Approved :: BSD License", 17 | "Programming Language :: Python", 18 | "Programming Language :: Python :: 3", 19 | "Programming Language :: Python :: 3.8", 20 | "Programming Language :: Python :: 3.9", 21 | "Programming Language :: Python :: 3.10", 22 | "Programming Language :: Python :: 3.11", 23 | ] 24 | dependencies = [ 25 | "jupyterlab>=4.0.0,<5", 26 | ] 27 | dynamic = ["version", "description", "authors", "urls", "keywords"] 28 | 29 | [tool.hatch.version] 30 | source = "nodejs" 31 | 32 | [tool.hatch.metadata.hooks.nodejs] 33 | fields = ["description", "authors", "urls"] 34 | 35 | [tool.hatch.build.targets.sdist] 36 | artifacts = ["jupyterlab_github/labextension"] 37 | exclude = [".github", "binder"] 38 | 39 | [tool.hatch.build.targets.wheel.shared-data] 40 | "jupyter-config/jupyter_server_config.d" = "etc/jupyter/jupyter_server_config.d" 41 | "jupyterlab_github/labextension" = "share/jupyter/labextensions/@jupyterlab/github" 42 | "install.json" = "share/jupyter/labextensions/@jupyterlab/github/install.json" 43 | 44 | [tool.hatch.build.hooks.version] 45 | path = "jupyterlab_github/_version.py" 46 | 47 | [tool.hatch.build.hooks.jupyter-builder] 48 | dependencies = ["hatch-jupyter-builder>=0.5"] 49 | build-function = "hatch_jupyter_builder.npm_builder" 50 | ensured-targets = [ 51 | "jupyterlab_github/labextension/static/style.js", 52 | "jupyterlab_github/labextension/package.json", 53 | ] 54 | skip-if-exists = ["jupyterlab_github/labextension/static/style.js"] 55 | optional-editable-build = true 56 | 57 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 58 | build_cmd = "build:prod" 59 | npm = ["jlpm"] 60 | 61 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 62 | build_cmd = "build" 63 | npm = ["jlpm"] 64 | source_dir = "src" 65 | build_dir = "jupyterlab_github/labextension" 66 | 67 | [tool.jupyter-releaser.options] 68 | version_cmd = "hatch version" 69 | 70 | [tool.jupyter-releaser.hooks] 71 | before-build-npm = [ 72 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 73 | "jlpm", 74 | "jlpm build:prod" 75 | ] 76 | before-build-python = ["jlpm clean:all"] 77 | 78 | [tool.check-wheel-contents] 79 | ignore = ["W002"] 80 | -------------------------------------------------------------------------------- /schema/drive.json: -------------------------------------------------------------------------------- 1 | { 2 | "jupyter.lab.setting-icon-class": "jp-GitHub-icon", 3 | "jupyter.lab.setting-icon-label": "GitHub", 4 | "title": "GitHub", 5 | "description": "Settings for the GitHub plugin.", 6 | "properties": { 7 | "baseUrl": { 8 | "type": "string", 9 | "title": "The GitHub Base URL", 10 | "default": "https://github.com" 11 | }, 12 | "accessToken": { 13 | "type": "string", 14 | "title": "A GitHub Personal Access Token", 15 | "description": "WARNING: For security reasons access tokens should be set in the server extension.", 16 | "default": "" 17 | }, 18 | "defaultRepo": { 19 | "type": "string", 20 | "title": "Default Repository", 21 | "default": "" 22 | } 23 | }, 24 | "type": "object" 25 | } 26 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __import__('setuptools').setup() 2 | -------------------------------------------------------------------------------- /src/browser.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { ToolbarButton } from '@jupyterlab/apputils'; 5 | 6 | import { URLExt } from '@jupyterlab/coreutils'; 7 | 8 | import { FileBrowser } from '@jupyterlab/filebrowser'; 9 | 10 | import { refreshIcon } from '@jupyterlab/ui-components'; 11 | 12 | import { find } from '@lumino/algorithm'; 13 | 14 | import { Message } from '@lumino/messaging'; 15 | 16 | import { ISignal, Signal } from '@lumino/signaling'; 17 | 18 | import { PanelLayout, Widget } from '@lumino/widgets'; 19 | 20 | import { GitHubDrive, parsePath } from './contents'; 21 | 22 | /** 23 | * The base url for a mybinder deployment. 24 | */ 25 | const MY_BINDER_BASE_URL = 'https://mybinder.org/v2/gh'; 26 | 27 | /** 28 | * The className for disabling the mybinder button. 29 | */ 30 | const MY_BINDER_DISABLED = 'jp-MyBinderButton-disabled'; 31 | 32 | /** 33 | * Widget for hosting the GitHub filebrowser. 34 | */ 35 | export class GitHubFileBrowser extends Widget { 36 | constructor(browser: FileBrowser, drive: GitHubDrive) { 37 | super(); 38 | this.addClass('jp-GitHubBrowser'); 39 | this.layout = new PanelLayout(); 40 | (this.layout as PanelLayout).addWidget(browser); 41 | this._browser = browser; 42 | this._drive = drive; 43 | 44 | // Create an editable name for the user/org name. 45 | this.userName = new GitHubUserInput(); 46 | this.userName.node.title = 'Click to edit user/organization'; 47 | this._browser.toolbar.addItem('user', this.userName); 48 | this.userName.nameChanged.connect(this._onUserChanged, this); 49 | // Create a button that opens GitHub at the appropriate 50 | // repo+directory. 51 | this._openGitHubButton = new ToolbarButton({ 52 | onClick: () => { 53 | let url = this._drive.baseUrl; 54 | // If there is no valid user, open the GitHub homepage. 55 | if (!this._drive.validUser) { 56 | window.open(url); 57 | return; 58 | } 59 | const localPath = 60 | this._browser.model.manager.services.contents.localPath( 61 | this._browser.model.path 62 | ); 63 | const resource = parsePath(localPath); 64 | url = URLExt.join(url, resource.user); 65 | if (resource.repository) { 66 | url = URLExt.join( 67 | url, 68 | resource.repository, 69 | 'tree', 70 | 'master', 71 | resource.path 72 | ); 73 | } 74 | window.open(url); 75 | }, 76 | iconClass: 'jp-GitHub-icon jp-Icon jp-Icon-16', 77 | tooltip: 'Open this repository on GitHub' 78 | }); 79 | this._openGitHubButton.addClass('jp-GitHub-toolbar-item'); 80 | this._browser.toolbar.addItem('GitHub', this._openGitHubButton); 81 | 82 | // Create a button the opens MyBinder to the appropriate repo. 83 | this._launchBinderButton = new ToolbarButton({ 84 | onClick: () => { 85 | // If binder is not active for this directory, do nothing. 86 | if (!this._binderActive) { 87 | return; 88 | } 89 | const localPath = 90 | this._browser.model.manager.services.contents.localPath( 91 | this._browser.model.path 92 | ); 93 | const resource = parsePath(localPath); 94 | const url = URLExt.join( 95 | MY_BINDER_BASE_URL, 96 | resource.user, 97 | resource.repository, 98 | 'master' 99 | ); 100 | // Attempt to open using the JupyterLab tree handler 101 | const tree = URLExt.join('lab', 'tree', resource.path); 102 | window.open(url + `?urlpath=${tree}`); 103 | }, 104 | tooltip: 'Launch this repository on mybinder.org', 105 | iconClass: 'jp-MyBinderButton jp-Icon jp-Icon-16' 106 | }); 107 | this._launchBinderButton.addClass('jp-GitHub-toolbar-item'); 108 | this._browser.toolbar.addItem('binder', this._launchBinderButton); 109 | 110 | // Add our own refresh button, since the other one is hidden 111 | // via CSS. 112 | const refresher = new ToolbarButton({ 113 | icon: refreshIcon, 114 | onClick: () => { 115 | this._browser.model.refresh(); 116 | }, 117 | tooltip: 'Refresh File List' 118 | }); 119 | refresher.addClass('jp-GitHub-toolbar-item'); 120 | this._browser.toolbar.addItem('gh-refresher', refresher); 121 | 122 | // Set up a listener to check if we can launch mybinder. 123 | this._browser.model.pathChanged.connect(this._onPathChanged, this); 124 | // Trigger an initial pathChanged to check for binder state. 125 | this._onPathChanged(); 126 | 127 | this._drive.rateLimitedState.changed.connect(this._updateErrorPanel, this); 128 | } 129 | 130 | /** 131 | * An editable widget hosting the current user name. 132 | */ 133 | readonly userName: GitHubUserInput; 134 | 135 | /** 136 | * React to a change in user. 137 | */ 138 | private _onUserChanged() { 139 | if (this._changeGuard) { 140 | return; 141 | } 142 | this._changeGuard = true; 143 | this._browser.model.cd(`/${this.userName.name}`).then(() => { 144 | this._changeGuard = false; 145 | this._updateErrorPanel(); 146 | // Once we have the new listing, maybe give the file listing 147 | // focus. Once the input element is removed, the active element 148 | // appears to revert to document.body. If the user has subsequently 149 | // focused another element, don't focus the browser listing. 150 | if (document.activeElement === document.body) { 151 | const listing = (this._browser.layout as PanelLayout).widgets[1]; 152 | listing.node.focus(); 153 | } 154 | }); 155 | } 156 | 157 | /** 158 | * React to the path changing for the browser. 159 | */ 160 | private _onPathChanged(): void { 161 | const localPath = this._browser.model.manager.services.contents.localPath( 162 | this._browser.model.path 163 | ); 164 | const resource = parsePath(localPath); 165 | 166 | // If we are not already changing the user name, set it. 167 | if (!this._changeGuard) { 168 | this._changeGuard = true; 169 | this.userName.name = resource.user; 170 | this._changeGuard = false; 171 | this._updateErrorPanel(); 172 | } 173 | 174 | // Check for a valid user. 175 | if (!this._drive.validUser) { 176 | this._launchBinderButton.addClass(MY_BINDER_DISABLED); 177 | this._binderActive = false; 178 | return; 179 | } 180 | // Check for a valid repo. 181 | if (!resource.repository) { 182 | this._launchBinderButton.addClass(MY_BINDER_DISABLED); 183 | this._binderActive = false; 184 | return; 185 | } 186 | // If we are in the root of the repository, check for one of 187 | // the special files indicating we can launch the repository on mybinder. 188 | // TODO: If the user navigates to a subdirectory without hitting the root 189 | // of the repository, we will not check for whether the repo is binder-able. 190 | // Figure out some way around this. 191 | if (resource.path === '') { 192 | const item = find(this._browser.model.items(), i => { 193 | return ( 194 | i.name === 'requirements.txt' || 195 | i.name === 'environment.yml' || 196 | i.name === 'apt.txt' || 197 | i.name === 'REQUIRE' || 198 | i.name === 'Dockerfile' || 199 | (i.name === 'binder' && i.type === 'directory') 200 | ); 201 | }); 202 | if (item) { 203 | this._launchBinderButton.removeClass(MY_BINDER_DISABLED); 204 | this._binderActive = true; 205 | return; 206 | } else { 207 | this._launchBinderButton.addClass(MY_BINDER_DISABLED); 208 | this._binderActive = false; 209 | return; 210 | } 211 | } 212 | // If we got this far, we are in a subdirectory of a valid 213 | // repository, and should not change the binderActive status. 214 | return; 215 | } 216 | 217 | /** 218 | * React to a change in the validity of the drive. 219 | */ 220 | private _updateErrorPanel(): void { 221 | const localPath = this._browser.model.manager.services.contents.localPath( 222 | this._browser.model.path 223 | ); 224 | const resource = parsePath(localPath); 225 | const rateLimited = this._drive.rateLimitedState.get(); 226 | const validUser = this._drive.validUser; 227 | 228 | // If we currently have an error panel, remove it. 229 | if (this._errorPanel) { 230 | const listing = (this._browser.layout as PanelLayout).widgets[1]; 231 | listing.node.removeChild(this._errorPanel.node); 232 | this._errorPanel.dispose(); 233 | this._errorPanel = null; 234 | } 235 | 236 | // If we are being rate limited, make an error panel. 237 | if (rateLimited) { 238 | this._errorPanel = new GitHubErrorPanel( 239 | 'You have been rate limited by GitHub! ' + 240 | 'You will need to wait about an hour before ' + 241 | 'continuing' 242 | ); 243 | const listing = (this._browser.layout as PanelLayout).widgets[1]; 244 | listing.node.appendChild(this._errorPanel.node); 245 | return; 246 | } 247 | 248 | // If we have an invalid user, make an error panel. 249 | if (!validUser) { 250 | const message = resource.user 251 | ? `"${resource.user}" appears to be an invalid user name!` 252 | : 'Please enter a GitHub user name'; 253 | this._errorPanel = new GitHubErrorPanel(message); 254 | const listing = (this._browser.layout as PanelLayout).widgets[1]; 255 | listing.node.appendChild(this._errorPanel.node); 256 | return; 257 | } 258 | } 259 | 260 | private _browser: FileBrowser; 261 | private _drive: GitHubDrive; 262 | private _errorPanel: GitHubErrorPanel | null = null; 263 | private _openGitHubButton: ToolbarButton; 264 | private _launchBinderButton: ToolbarButton; 265 | private _binderActive = false; 266 | private _changeGuard = false; 267 | } 268 | 269 | /** 270 | * A widget that hosts an editable field, 271 | * used to host the currently active GitHub 272 | * user name. 273 | */ 274 | export class GitHubUserInput extends Widget { 275 | constructor() { 276 | super(); 277 | this.addClass('jp-GitHubUserInput'); 278 | const layout = (this.layout = new PanelLayout()); 279 | const wrapper = new Widget(); 280 | wrapper.addClass('jp-GitHubUserInput-wrapper'); 281 | this._input = document.createElement('input'); 282 | this._input.placeholder = 'GitHub User'; 283 | this._input.className = 'jp-GitHubUserInput-input'; 284 | wrapper.node.appendChild(this._input); 285 | layout.addWidget(wrapper); 286 | } 287 | 288 | /** 289 | * The current name of the field. 290 | */ 291 | get name(): string { 292 | return this._name; 293 | } 294 | set name(value: string) { 295 | if (value === this._name) { 296 | return; 297 | } 298 | const old = this._name; 299 | this._name = value; 300 | this._input.value = value; 301 | this._nameChanged.emit({ 302 | oldValue: old, 303 | newValue: value 304 | }); 305 | } 306 | 307 | /** 308 | * A signal for when the name changes. 309 | */ 310 | get nameChanged(): ISignal { 311 | return this._nameChanged; 312 | } 313 | 314 | /** 315 | * Handle the DOM events for the widget. 316 | * 317 | * @param event - The DOM event sent to the widget. 318 | * 319 | * #### Notes 320 | * This method implements the DOM `EventListener` interface and is 321 | * called in response to events on the main area widget's node. It should 322 | * not be called directly by user code. 323 | */ 324 | handleEvent(event: KeyboardEvent): void { 325 | switch (event.type) { 326 | case 'keydown': 327 | switch (event.keyCode) { 328 | case 13: // Enter 329 | event.stopPropagation(); 330 | event.preventDefault(); 331 | this.name = this._input.value; 332 | this._input.blur(); 333 | break; 334 | default: 335 | break; 336 | } 337 | break; 338 | case 'blur': 339 | event.stopPropagation(); 340 | event.preventDefault(); 341 | this.name = this._input.value; 342 | break; 343 | case 'focus': 344 | event.stopPropagation(); 345 | event.preventDefault(); 346 | this._input.select(); 347 | break; 348 | default: 349 | break; 350 | } 351 | } 352 | 353 | /** 354 | * Handle `after-attach` messages for the widget. 355 | */ 356 | protected onAfterAttach(msg: Message): void { 357 | this._input.addEventListener('keydown', this); 358 | this._input.addEventListener('blur', this); 359 | this._input.addEventListener('focus', this); 360 | } 361 | 362 | /** 363 | * Handle `before-detach` messages for the widget. 364 | */ 365 | protected onBeforeDetach(msg: Message): void { 366 | this._input.removeEventListener('keydown', this); 367 | this._input.removeEventListener('blur', this); 368 | this._input.removeEventListener('focus', this); 369 | } 370 | 371 | private _name = ''; 372 | private _nameChanged = new Signal< 373 | this, 374 | { newValue: string; oldValue: string } 375 | >(this); 376 | private _input: HTMLInputElement; 377 | } 378 | 379 | /** 380 | * A widget hosting an error panel for the browser, 381 | * used if there is an invalid user name or if we 382 | * are being rate-limited. 383 | */ 384 | export class GitHubErrorPanel extends Widget { 385 | constructor(message: string) { 386 | super(); 387 | this.addClass('jp-GitHubErrorPanel'); 388 | const image = document.createElement('div'); 389 | const text = document.createElement('div'); 390 | image.className = 'jp-GitHubErrorImage'; 391 | text.className = 'jp-GitHubErrorText'; 392 | text.textContent = message; 393 | this.node.appendChild(image); 394 | this.node.appendChild(text); 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /src/contents.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { Signal, ISignal } from '@lumino/signaling'; 5 | 6 | import { PathExt, URLExt } from '@jupyterlab/coreutils'; 7 | 8 | import { DocumentRegistry } from '@jupyterlab/docregistry'; 9 | 10 | import { ObservableValue } from '@jupyterlab/observables'; 11 | 12 | import { Contents, ServerConnection } from '@jupyterlab/services'; 13 | 14 | import { 15 | browserApiRequest, 16 | proxiedApiRequest, 17 | GitHubRepo, 18 | GitHubContents, 19 | GitHubBlob, 20 | GitHubFileContents, 21 | GitHubDirectoryListing 22 | } from './github'; 23 | 24 | import * as base64js from 'base64-js'; 25 | 26 | export const DEFAULT_GITHUB_API_URL = 'https://api.github.com'; 27 | export const DEFAULT_GITHUB_BASE_URL = 'https://github.com'; 28 | 29 | /** 30 | * A Contents.IDrive implementation that serves as a read-only 31 | * view onto GitHub repositories. 32 | */ 33 | export class GitHubDrive implements Contents.IDrive { 34 | /** 35 | * Construct a new drive object. 36 | * 37 | * @param options - The options used to initialize the object. 38 | */ 39 | constructor(registry: DocumentRegistry) { 40 | this._serverSettings = ServerConnection.makeSettings(); 41 | this._fileTypeForPath = (path: string) => { 42 | const types = registry.getFileTypesForPath(path); 43 | return types.length === 0 ? registry.getFileType('text')! : types[0]; 44 | }; 45 | 46 | this.baseUrl = DEFAULT_GITHUB_BASE_URL; 47 | 48 | // Test an api request to the notebook server 49 | // to see if the server proxy is installed. 50 | // If so, use that. If not, warn the user and 51 | // use the client-side implementation. 52 | this._useProxy = new Promise(resolve => { 53 | const requestUrl = URLExt.join(this._serverSettings.baseUrl, 'github'); 54 | proxiedApiRequest(requestUrl, this._serverSettings) 55 | .then(() => { 56 | resolve(true); 57 | }) 58 | .catch(() => { 59 | console.warn( 60 | 'The JupyterLab GitHub server extension appears ' + 61 | 'to be missing. If you do not install it with application ' + 62 | 'credentials, you are likely to be rate limited by GitHub ' + 63 | 'very quickly' 64 | ); 65 | resolve(false); 66 | }); 67 | }); 68 | 69 | // Initialize the rate-limited observable. 70 | this.rateLimitedState = new ObservableValue(false); 71 | } 72 | 73 | /** 74 | * The name of the drive. 75 | */ 76 | get name(): 'GitHub' { 77 | return 'GitHub'; 78 | } 79 | 80 | /** 81 | * State for whether the user is valid. 82 | */ 83 | get validUser(): boolean { 84 | return this._validUser; 85 | } 86 | 87 | /** 88 | * Settings for the notebook server. 89 | */ 90 | get serverSettings(): ServerConnection.ISettings { 91 | return this._serverSettings; 92 | } 93 | 94 | /** 95 | * State for whether the drive is being rate limited by GitHub. 96 | */ 97 | readonly rateLimitedState: ObservableValue; 98 | 99 | /** 100 | * A signal emitted when a file operation takes place. 101 | */ 102 | get fileChanged(): ISignal { 103 | return this._fileChanged; 104 | } 105 | 106 | /** 107 | * Test whether the manager has been disposed. 108 | */ 109 | get isDisposed(): boolean { 110 | return this._isDisposed; 111 | } 112 | 113 | /** 114 | * Dispose of the resources held by the manager. 115 | */ 116 | dispose(): void { 117 | if (this.isDisposed) { 118 | return; 119 | } 120 | this._isDisposed = true; 121 | Signal.clearData(this); 122 | } 123 | 124 | /** 125 | * The GitHub base URL 126 | */ 127 | get baseUrl(): string { 128 | return this._baseUrl; 129 | } 130 | 131 | /** 132 | * The GitHub base URL is set by the settingsRegistry change hook 133 | */ 134 | set baseUrl(url: string) { 135 | this._baseUrl = url; 136 | } 137 | 138 | /** 139 | * The GitHub access token 140 | */ 141 | get accessToken(): string | null | undefined { 142 | return this._accessToken; 143 | } 144 | 145 | /** 146 | * The GitHub access token is set by the settingsRegistry change hook 147 | */ 148 | set accessToken(token: string | null | undefined) { 149 | this._accessToken = token; 150 | } 151 | 152 | /** 153 | * Get a file or directory. 154 | * 155 | * @param path: The path to the file. 156 | * 157 | * @param options: The options used to fetch the file. 158 | * 159 | * @returns A promise which resolves with the file content. 160 | */ 161 | get( 162 | path: string, 163 | options?: Contents.IFetchOptions 164 | ): Promise { 165 | const resource = parsePath(path); 166 | // If the org has not been set, return an empty directory 167 | // placeholder. 168 | if (resource.user === '') { 169 | this._validUser = false; 170 | return Promise.resolve(Private.dummyDirectory); 171 | } 172 | 173 | // If the org has been set and the path is empty, list 174 | // the repositories for the org. 175 | if (resource.user && !resource.repository) { 176 | return this._listRepos(resource.user); 177 | } 178 | 179 | // Otherwise identify the repository and get the contents of the 180 | // appropriate resource. 181 | const apiPath = URLExt.encodeParts( 182 | URLExt.join( 183 | 'repos', 184 | resource.user, 185 | resource.repository, 186 | 'contents', 187 | resource.path 188 | ) 189 | ); 190 | return this._apiRequest(apiPath) 191 | .then(contents => { 192 | // Set the states 193 | this._validUser = true; 194 | if (this.rateLimitedState.get() !== false) { 195 | this.rateLimitedState.set(false); 196 | } 197 | 198 | return Private.gitHubContentsToJupyterContents( 199 | path, 200 | contents, 201 | this._fileTypeForPath 202 | ); 203 | }) 204 | .catch((err: ServerConnection.ResponseError) => { 205 | if (err.response.status === 404) { 206 | console.warn( 207 | 'GitHub: cannot find org/repo. ' + 208 | 'Perhaps you misspelled something?' 209 | ); 210 | this._validUser = false; 211 | return Private.dummyDirectory; 212 | } else if ( 213 | err.response.status === 403 && 214 | err.message.indexOf('rate limit') !== -1 215 | ) { 216 | if (this.rateLimitedState.get() !== true) { 217 | this.rateLimitedState.set(true); 218 | } 219 | console.error(err.message); 220 | return Promise.reject(err); 221 | } else if ( 222 | err.response.status === 403 && 223 | err.message.indexOf('blob') !== -1 224 | ) { 225 | // Set the states 226 | this._validUser = true; 227 | if (this.rateLimitedState.get() !== false) { 228 | this.rateLimitedState.set(false); 229 | } 230 | return this._getBlob(path); 231 | } else { 232 | console.error(err.message); 233 | return Promise.reject(err); 234 | } 235 | }); 236 | } 237 | 238 | /** 239 | * Get an encoded download url given a file path. 240 | * 241 | * @param path - An absolute POSIX file path on the server. 242 | * 243 | * #### Notes 244 | * It is expected that the path contains no relative paths, 245 | * use [[ContentsManager.getAbsolutePath]] to get an absolute 246 | * path if necessary. 247 | */ 248 | getDownloadUrl(path: string): Promise { 249 | // Parse the path into user/repo/path 250 | const resource = parsePath(path); 251 | // Error if the user has not been set 252 | if (!resource.user) { 253 | return Promise.reject('GitHub: no active organization'); 254 | } 255 | 256 | // Error if there is no path. 257 | if (!resource.path) { 258 | return Promise.reject('GitHub: No file selected'); 259 | } 260 | 261 | // Otherwise identify the repository and get the url of the 262 | // appropriate resource. 263 | const dirname = PathExt.dirname(resource.path); 264 | const dirApiPath = URLExt.encodeParts( 265 | URLExt.join( 266 | 'repos', 267 | resource.user, 268 | resource.repository, 269 | 'contents', 270 | dirname 271 | ) 272 | ); 273 | return this._apiRequest(dirApiPath).then( 274 | dirContents => { 275 | for (const item of dirContents) { 276 | if (item.path === resource.path) { 277 | return item.download_url; 278 | } 279 | } 280 | throw Private.makeError(404, `Cannot find file at ${resource.path}`); 281 | } 282 | ); 283 | } 284 | 285 | /** 286 | * Create a new untitled file or directory in the specified directory path. 287 | * 288 | * @param options: The options used to create the file. 289 | * 290 | * @returns A promise which resolves with the created file content when the 291 | * file is created. 292 | */ 293 | newUntitled(options: Contents.ICreateOptions = {}): Promise { 294 | return Promise.reject('Repository is read only'); 295 | } 296 | 297 | /** 298 | * Delete a file. 299 | * 300 | * @param path - The path to the file. 301 | * 302 | * @returns A promise which resolves when the file is deleted. 303 | */ 304 | delete(path: string): Promise { 305 | return Promise.reject('Repository is read only'); 306 | } 307 | 308 | /** 309 | * Rename a file or directory. 310 | * 311 | * @param path - The original file path. 312 | * 313 | * @param newPath - The new file path. 314 | * 315 | * @returns A promise which resolves with the new file contents model when 316 | * the file is renamed. 317 | */ 318 | rename(path: string, newPath: string): Promise { 319 | return Promise.reject('Repository is read only'); 320 | } 321 | 322 | /** 323 | * Save a file. 324 | * 325 | * @param path - The desired file path. 326 | * 327 | * @param options - Optional overrides to the model. 328 | * 329 | * @returns A promise which resolves with the file content model when the 330 | * file is saved. 331 | */ 332 | save( 333 | path: string, 334 | options: Partial 335 | ): Promise { 336 | return Promise.reject('Repository is read only'); 337 | } 338 | 339 | /** 340 | * Copy a file into a given directory. 341 | * 342 | * @param path - The original file path. 343 | * 344 | * @param toDir - The destination directory path. 345 | * 346 | * @returns A promise which resolves with the new contents model when the 347 | * file is copied. 348 | */ 349 | copy(fromFile: string, toDir: string): Promise { 350 | return Promise.reject('Repository is read only'); 351 | } 352 | 353 | /** 354 | * Create a checkpoint for a file. 355 | * 356 | * @param path - The path of the file. 357 | * 358 | * @returns A promise which resolves with the new checkpoint model when the 359 | * checkpoint is created. 360 | */ 361 | createCheckpoint(path: string): Promise { 362 | return Promise.reject('Repository is read only'); 363 | } 364 | 365 | /** 366 | * List available checkpoints for a file. 367 | * 368 | * @param path - The path of the file. 369 | * 370 | * @returns A promise which resolves with a list of checkpoint models for 371 | * the file. 372 | */ 373 | listCheckpoints(path: string): Promise { 374 | return Promise.resolve([]); 375 | } 376 | 377 | /** 378 | * Restore a file to a known checkpoint state. 379 | * 380 | * @param path - The path of the file. 381 | * 382 | * @param checkpointID - The id of the checkpoint to restore. 383 | * 384 | * @returns A promise which resolves when the checkpoint is restored. 385 | */ 386 | restoreCheckpoint(path: string, checkpointID: string): Promise { 387 | return Promise.reject('Repository is read only'); 388 | } 389 | 390 | /** 391 | * Delete a checkpoint for a file. 392 | * 393 | * @param path - The path of the file. 394 | * 395 | * @param checkpointID - The id of the checkpoint to delete. 396 | * 397 | * @returns A promise which resolves when the checkpoint is deleted. 398 | */ 399 | deleteCheckpoint(path: string, checkpointID: string): Promise { 400 | return Promise.reject('Read only'); 401 | } 402 | 403 | /** 404 | * If a file is too large (> 1Mb), we need to access it over the 405 | * GitHub Git Data API. 406 | */ 407 | private _getBlob(path: string): Promise { 408 | let blobData: GitHubFileContents; 409 | // Get the contents of the parent directory so that we can 410 | // get the sha of the blob. 411 | const resource = parsePath(path); 412 | const dirname = PathExt.dirname(resource.path); 413 | const dirApiPath = URLExt.encodeParts( 414 | URLExt.join( 415 | 'repos', 416 | resource.user, 417 | resource.repository, 418 | 'contents', 419 | dirname 420 | ) 421 | ); 422 | return this._apiRequest(dirApiPath) 423 | .then(dirContents => { 424 | for (const item of dirContents) { 425 | if (item.path === resource.path) { 426 | blobData = item as GitHubFileContents; 427 | return item.sha; 428 | } 429 | } 430 | throw Error('Cannot find sha for blob'); 431 | }) 432 | .then(sha => { 433 | // Once we have the sha, form the api url and make the request. 434 | const blobApiPath = URLExt.encodeParts( 435 | URLExt.join( 436 | 'repos', 437 | resource.user, 438 | resource.repository, 439 | 'git', 440 | 'blobs', 441 | sha 442 | ) 443 | ); 444 | return this._apiRequest(blobApiPath); 445 | }) 446 | .then(blob => { 447 | // Convert the data to a Contents.IModel. 448 | blobData.content = blob.content; 449 | return Private.gitHubContentsToJupyterContents( 450 | path, 451 | blobData, 452 | this._fileTypeForPath 453 | ); 454 | }); 455 | } 456 | 457 | /** 458 | * List the repositories for the currently active user. 459 | */ 460 | private _listRepos(user: string): Promise { 461 | // First, check if the `user` string is actually an org. 462 | // If will return with an error if not, and we can try 463 | // the user path. 464 | const apiPath = URLExt.encodeParts(URLExt.join('orgs', user, 'repos')); 465 | return this._apiRequest(apiPath) 466 | .catch(err => { 467 | // If we can't find the org, it may be a user. 468 | if (err.response.status === 404) { 469 | // Check if it is the authenticated user. 470 | return this._apiRequest('user') 471 | .then(currentUser => { 472 | let reposPath: string; 473 | // If we are looking at the currently authenticated user, 474 | // get all the repositories they own, which includes private ones. 475 | if (currentUser.login === user) { 476 | reposPath = 'user/repos?type=owner'; 477 | } else { 478 | reposPath = URLExt.encodeParts( 479 | URLExt.join('users', user, 'repos') 480 | ); 481 | } 482 | return this._apiRequest(reposPath); 483 | }) 484 | .catch(err => { 485 | // If there is no authenticated user, return the public 486 | // users api path. 487 | if (err.response.status === 401) { 488 | const reposPath = URLExt.encodeParts( 489 | URLExt.join('users', user, 'repos') 490 | ); 491 | return this._apiRequest(reposPath); 492 | } 493 | throw err; 494 | }); 495 | } 496 | throw err; 497 | }) 498 | .then(repos => { 499 | // Set the states 500 | this._validUser = true; 501 | if (this.rateLimitedState.get() !== false) { 502 | this.rateLimitedState.set(false); 503 | } 504 | return Private.reposToDirectory(repos); 505 | }) 506 | .catch(err => { 507 | if ( 508 | err.response.status === 403 && 509 | err.message.indexOf('rate limit') !== -1 510 | ) { 511 | if (this.rateLimitedState.get() !== true) { 512 | this.rateLimitedState.set(true); 513 | } 514 | } else { 515 | console.error(err.message); 516 | console.warn( 517 | 'GitHub: cannot find user. ' + 'Perhaps you misspelled something?' 518 | ); 519 | this._validUser = false; 520 | } 521 | return Private.dummyDirectory; 522 | }); 523 | } 524 | 525 | /** 526 | * Determine whether to make the call via the 527 | * notebook server proxy or not. 528 | */ 529 | private _apiRequest(apiPath: string): Promise { 530 | return this._useProxy.then(result => { 531 | const parts = apiPath.split('?'); 532 | const path = parts[0]; 533 | const query = (parts[1] || '').split('&'); 534 | const params: { [key: string]: string } = {}; 535 | for (const param of query) { 536 | if (param) { 537 | const [key, value] = param.split('='); 538 | params[key] = value; 539 | } 540 | } 541 | let requestUrl: string; 542 | if (result === true) { 543 | requestUrl = URLExt.join(this._serverSettings.baseUrl, 'github'); 544 | // add the access token if defined 545 | if (this.accessToken) { 546 | params['access_token'] = this.accessToken; 547 | } 548 | } else { 549 | requestUrl = DEFAULT_GITHUB_API_URL; 550 | } 551 | if (path) { 552 | requestUrl = URLExt.join(requestUrl, path); 553 | } 554 | const newQuery = Object.keys(params) 555 | .map(key => `${key}=${params[key]}`) 556 | .join('&'); 557 | requestUrl += '?' + newQuery; 558 | if (result === true) { 559 | return proxiedApiRequest(requestUrl, this._serverSettings); 560 | } else { 561 | return browserApiRequest(requestUrl); 562 | } 563 | }); 564 | } 565 | 566 | private _baseUrl: string = 'github'; 567 | private _accessToken: string | null | undefined; 568 | private _validUser = false; 569 | private _serverSettings: ServerConnection.ISettings; 570 | private _useProxy: Promise; 571 | private _fileTypeForPath: (path: string) => DocumentRegistry.IFileType; 572 | private _isDisposed = false; 573 | private _fileChanged = new Signal(this); 574 | } 575 | 576 | /** 577 | * Specification for a file in a repository. 578 | */ 579 | export interface IGitHubResource { 580 | /** 581 | * The user or organization for the resource. 582 | */ 583 | readonly user: string; 584 | 585 | /** 586 | * The repository in the organization/user. 587 | */ 588 | readonly repository: string; 589 | 590 | /** 591 | * The path in the repository to the resource. 592 | */ 593 | readonly path: string; 594 | } 595 | 596 | /** 597 | * Parse a path into a IGitHubResource. 598 | */ 599 | export function parsePath(path: string): IGitHubResource { 600 | const parts = path.split('/'); 601 | const user = parts.length > 0 ? parts[0] : ''; 602 | const repository = parts.length > 1 ? parts[1] : ''; 603 | const repoPath = parts.length > 2 ? URLExt.join(...parts.slice(2)) : ''; 604 | return { user, repository, path: repoPath }; 605 | } 606 | 607 | /** 608 | * Private namespace for utility functions. 609 | */ 610 | namespace Private { 611 | /** 612 | * A dummy contents model indicating an invalid or 613 | * nonexistent repository. 614 | */ 615 | export const dummyDirectory: Contents.IModel = { 616 | type: 'directory', 617 | path: '', 618 | name: '', 619 | format: 'json', 620 | content: [], 621 | created: '', 622 | writable: false, 623 | last_modified: '', 624 | mimetype: '' 625 | }; 626 | 627 | /** 628 | * Given a JSON GitHubContents object returned by the GitHub API v3, 629 | * convert it to the Jupyter Contents.IModel. 630 | * 631 | * @param path - the path to the contents model in the repository. 632 | * 633 | * @param contents - the GitHubContents object. 634 | * 635 | * @param fileTypeForPath - a function that, given a path, returns 636 | * a DocumentRegistry.IFileType, used by JupyterLab to identify different 637 | * openers, icons, etc. 638 | * 639 | * @returns a Contents.IModel object. 640 | */ 641 | export function gitHubContentsToJupyterContents( 642 | path: string, 643 | contents: GitHubContents | GitHubContents[], 644 | fileTypeForPath: (path: string) => DocumentRegistry.IFileType 645 | ): Contents.IModel { 646 | if (Array.isArray(contents)) { 647 | // If we have an array, it is a directory of GitHubContents. 648 | // Iterate over that and convert all of the items in the array/ 649 | return { 650 | name: PathExt.basename(path), 651 | path: path, 652 | format: 'json', 653 | type: 'directory', 654 | writable: false, 655 | created: '', 656 | last_modified: '', 657 | mimetype: '', 658 | content: contents.map(c => { 659 | return gitHubContentsToJupyterContents( 660 | PathExt.join(path, c.name), 661 | c, 662 | fileTypeForPath 663 | ); 664 | }) 665 | } as Contents.IModel; 666 | } else if (contents.type === 'file' || contents.type === 'symlink') { 667 | // If it is a file or blob, convert to a file 668 | const fileType = fileTypeForPath(path); 669 | const fileContents = (contents as GitHubFileContents).content; 670 | let content: any; 671 | switch (fileType.fileFormat) { 672 | case 'text': 673 | content = 674 | fileContents !== undefined 675 | ? Private.b64DecodeUTF8(fileContents) 676 | : null; 677 | break; 678 | case 'base64': 679 | content = fileContents !== undefined ? fileContents : null; 680 | break; 681 | case 'json': 682 | content = 683 | fileContents !== undefined 684 | ? JSON.parse(Private.b64DecodeUTF8(fileContents)) 685 | : null; 686 | break; 687 | default: 688 | throw new Error(`Unexpected file format: ${fileType.fileFormat}`); 689 | } 690 | return { 691 | name: PathExt.basename(path), 692 | path: path, 693 | format: fileType.fileFormat, 694 | type: 'file', 695 | created: '', 696 | writable: false, 697 | last_modified: '', 698 | mimetype: fileType.mimeTypes[0], 699 | content 700 | }; 701 | } else if (contents.type === 'dir') { 702 | // If it is a directory, convert to that. 703 | return { 704 | name: PathExt.basename(path), 705 | path: path, 706 | format: 'json', 707 | type: 'directory', 708 | created: '', 709 | writable: false, 710 | last_modified: '', 711 | mimetype: '', 712 | content: null 713 | }; 714 | } else if (contents.type === 'submodule') { 715 | // If it is a submodule, throw an error, since we cannot 716 | // GET submodules at the moment. NOTE: due to a bug in the GithHub 717 | // API, the `type` for submodules in a directory listing is incorrectly 718 | // reported as `file`: https://github.com/github/developer.github.com/commit/1b329b04cece9f3087faa7b1e0382317a9b93490 719 | // This means submodules will show up in the listing, but we still should not 720 | // open them. 721 | throw makeError( 722 | 400, 723 | `Cannot open "${contents.name}" because it is a submodule` 724 | ); 725 | } else { 726 | throw makeError( 727 | 500, 728 | `"${contents.name}" has and unexpected type: ${contents.type}` 729 | ); 730 | } 731 | } 732 | 733 | /** 734 | * Given an array of JSON GitHubRepo objects returned by the GitHub API v3, 735 | * convert it to the Jupyter Contents.IModel conforming to a directory of 736 | * those repositories. 737 | * 738 | * @param repo - the GitHubRepo object. 739 | * 740 | * @returns a Contents.IModel object. 741 | */ 742 | export function reposToDirectory(repos: GitHubRepo[]): Contents.IModel { 743 | // If it is a directory, convert to that. 744 | const content: Contents.IModel[] = repos.map(repo => { 745 | return { 746 | name: repo.name, 747 | path: repo.full_name, 748 | format: 'json', 749 | type: 'directory', 750 | created: '', 751 | writable: false, 752 | last_modified: '', 753 | mimetype: '', 754 | content: null 755 | } as Contents.IModel; 756 | }); 757 | 758 | return { 759 | name: '', 760 | path: '', 761 | format: 'json', 762 | type: 'directory', 763 | created: '', 764 | last_modified: '', 765 | writable: false, 766 | mimetype: '', 767 | content 768 | }; 769 | } 770 | 771 | /** 772 | * Wrap an API error in a hacked-together error object 773 | * masquerading as an `ServerConnection.ResponseError`. 774 | */ 775 | export function makeError( 776 | code: number, 777 | message: string 778 | ): ServerConnection.ResponseError { 779 | const response = new Response(message, { 780 | status: code, 781 | statusText: message 782 | }); 783 | return new ServerConnection.ResponseError(response, message); 784 | } 785 | 786 | /** 787 | * Decoder from bytes to UTF-8. 788 | */ 789 | const decoder = new TextDecoder('utf8'); 790 | 791 | /** 792 | * Decode a base-64 encoded string into unicode. 793 | * 794 | * See https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_2_%E2%80%93_rewrite_the_DOMs_atob()_and_btoa()_using_JavaScript's_TypedArrays_and_UTF-8 795 | */ 796 | export function b64DecodeUTF8(str: string): string { 797 | const bytes = base64js.toByteArray(str.replace(/\n/g, '')); 798 | return decoder.decode(bytes); 799 | } 800 | } 801 | -------------------------------------------------------------------------------- /src/github.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { ServerConnection } from '@jupyterlab/services'; 5 | 6 | /** 7 | * Make a client-side request to the GitHub API. 8 | * 9 | * @param url - the api path for the GitHub API v3 10 | * (not including the base url). 11 | * 12 | * @returns a Promise resolved with the JSON response. 13 | */ 14 | export function browserApiRequest(url: string): Promise { 15 | return window.fetch(url).then(response => { 16 | if (response.status !== 200) { 17 | return response.json().then(data => { 18 | throw new ServerConnection.ResponseError(response, data.message); 19 | }); 20 | } 21 | return response.json(); 22 | }); 23 | } 24 | 25 | /** 26 | * Make a request to the notebook server proxy for the 27 | * GitHub API. 28 | * 29 | * @param url - the api path for the GitHub API v3 30 | * (not including the base url) 31 | * 32 | * @param settings - the settings for the current notebook server. 33 | * 34 | * @returns a Promise resolved with the JSON response. 35 | */ 36 | export function proxiedApiRequest( 37 | url: string, 38 | settings: ServerConnection.ISettings 39 | ): Promise { 40 | return ServerConnection.makeRequest(url, {}, settings).then(response => { 41 | if (response.status !== 200) { 42 | return response.json().then(data => { 43 | throw new ServerConnection.ResponseError(response, data.message); 44 | }); 45 | } 46 | return response.json(); 47 | }); 48 | } 49 | 50 | /** 51 | * Typings representing contents from the GitHub API v3. 52 | * Cf: https://developer.github.com/v3/repos/contents/ 53 | */ 54 | export interface GitHubContents { 55 | /** 56 | * The type of the file. 57 | */ 58 | type: 'file' | 'dir' | 'submodule' | 'symlink'; 59 | 60 | /** 61 | * The size of the file (in bytes). 62 | */ 63 | size: number; 64 | 65 | /** 66 | * The name of the file. 67 | */ 68 | name: string; 69 | 70 | /** 71 | * The path of the file in the repository. 72 | */ 73 | path: string; 74 | 75 | /** 76 | * A unique sha identifier for the file. 77 | */ 78 | sha: string; 79 | 80 | /** 81 | * The URL for the file in the GitHub API. 82 | */ 83 | url: string; 84 | 85 | /** 86 | * The URL for git access to the file. 87 | */ 88 | // tslint:disable-next-line 89 | git_url: string; 90 | 91 | /** 92 | * The URL for the file in the GitHub UI. 93 | */ 94 | // tslint:disable-next-line 95 | html_url: string; 96 | 97 | /** 98 | * The raw download URL for the file. 99 | */ 100 | // tslint:disable-next-line 101 | download_url: string; 102 | 103 | /** 104 | * Unsure the purpose of these. 105 | */ 106 | _links: { 107 | git: string; 108 | 109 | self: string; 110 | 111 | html: string; 112 | }; 113 | } 114 | 115 | /** 116 | * Typings representing file contents from the GitHub API v3. 117 | * Cf: https://developer.github.com/v3/repos/contents/#response-if-content-is-a-file 118 | */ 119 | export interface GitHubFileContents extends GitHubContents { 120 | /** 121 | * The type of the contents. 122 | */ 123 | type: 'file'; 124 | 125 | /** 126 | * Encoding of the content. All files are base64 encoded. 127 | */ 128 | encoding: 'base64'; 129 | 130 | /** 131 | * The actual base64 encoded contents. 132 | */ 133 | content?: string; 134 | } 135 | 136 | /** 137 | * Typings representing a directory from the GitHub API v3. 138 | */ 139 | export interface GitHubDirectoryContents extends GitHubContents { 140 | /** 141 | * The type of the contents. 142 | */ 143 | type: 'dir'; 144 | } 145 | 146 | /** 147 | * Typings representing a blob from the GitHub API v3. 148 | * Cf: https://developer.github.com/v3/git/blobs/#response 149 | */ 150 | export interface GitHubBlob { 151 | /** 152 | * The base64-encoded contents of the file. 153 | */ 154 | content: string; 155 | 156 | /** 157 | * The encoding of the contents. Always base64. 158 | */ 159 | encoding: 'base64'; 160 | 161 | /** 162 | * The URL for the blob. 163 | */ 164 | url: string; 165 | 166 | /** 167 | * The unique sha for the blob. 168 | */ 169 | sha: string; 170 | 171 | /** 172 | * The size of the blob, in bytes. 173 | */ 174 | size: number; 175 | } 176 | 177 | /** 178 | * Typings representing symlink contents from the GitHub API v3. 179 | * Cf: https://developer.github.com/v3/repos/contents/#response-if-content-is-a-symlink 180 | */ 181 | export interface GitHubSymlinkContents extends GitHubContents { 182 | /** 183 | * The type of the contents. 184 | */ 185 | type: 'symlink'; 186 | } 187 | 188 | /** 189 | * Typings representing submodule contents from the GitHub API v3. 190 | * Cf: https://developer.github.com/v3/repos/contents/#response-if-content-is-a-submodule 191 | */ 192 | export interface GitHubSubmoduleContents extends GitHubContents { 193 | /** 194 | * The type of the contents. 195 | */ 196 | type: 'submodule'; 197 | } 198 | 199 | /** 200 | * Typings representing directory contents from the GitHub API v3. 201 | * Cf: https://developer.github.com/v3/repos/contents/#response-if-content-is-a-directory 202 | */ 203 | export type GitHubDirectoryListing = GitHubContents[]; 204 | 205 | /** 206 | * Typings representing repositories from the GitHub API v3. 207 | * Cf: https://developer.github.com/v3/repos/#list-organization-repositories 208 | * 209 | * #### Notes 210 | * This is incomplete. 211 | */ 212 | export interface GitHubRepo { 213 | /** 214 | * ID for the repository. 215 | */ 216 | id: number; 217 | 218 | /** 219 | * The owner of the repository. 220 | */ 221 | owner: any; 222 | 223 | /** 224 | * The name of the repository. 225 | */ 226 | name: string; 227 | 228 | /** 229 | * The full name of the repository, including the owner name. 230 | */ 231 | // tslint:disable-next-line 232 | full_name: string; 233 | 234 | /** 235 | * A description of the repository. 236 | */ 237 | description: string; 238 | 239 | /** 240 | * Whether the repository is private. 241 | */ 242 | private: boolean; 243 | 244 | /** 245 | * Whether the repository is a fork. 246 | */ 247 | fork: boolean; 248 | 249 | /** 250 | * The URL for the repository in the GitHub API. 251 | */ 252 | url: string; 253 | 254 | /** 255 | * The URL for the repository in the GitHub UI. 256 | */ 257 | // tslint:disable-next-line 258 | html_url: string; 259 | } 260 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (c) Jupyter Development Team. 2 | // Distributed under the terms of the Modified BSD License. 3 | 4 | import { 5 | ILayoutRestorer, 6 | JupyterFrontEnd, 7 | JupyterFrontEndPlugin 8 | } from '@jupyterlab/application'; 9 | 10 | import { Dialog, showDialog } from '@jupyterlab/apputils'; 11 | 12 | import { LabIcon } from '@jupyterlab/ui-components'; 13 | 14 | import { IDocumentManager } from '@jupyterlab/docmanager'; 15 | 16 | import { IFileBrowserFactory } from '@jupyterlab/filebrowser'; 17 | 18 | import { ISettingRegistry } from '@jupyterlab/settingregistry'; 19 | 20 | import { GitHubDrive, DEFAULT_GITHUB_BASE_URL } from './contents'; 21 | 22 | import { GitHubFileBrowser } from './browser'; 23 | 24 | import GitHubSvgStr from '../style/octocat-light.svg'; 25 | 26 | /** 27 | * GitHub filebrowser plugin state namespace. 28 | */ 29 | const NAMESPACE = 'github-filebrowser'; 30 | 31 | /** 32 | * The ID for the plugin. 33 | */ 34 | const PLUGIN_ID = '@jupyterlab/github:drive'; 35 | 36 | /** 37 | * GitHub Icon class. 38 | */ 39 | export const gitHubIcon = new LabIcon({ 40 | name: `${NAMESPACE}:icon`, 41 | svgstr: GitHubSvgStr 42 | }); 43 | 44 | /** 45 | * The JupyterLab plugin for the GitHub Filebrowser. 46 | */ 47 | const fileBrowserPlugin: JupyterFrontEndPlugin = { 48 | id: PLUGIN_ID, 49 | requires: [IDocumentManager, IFileBrowserFactory, ISettingRegistry], 50 | optional: [ILayoutRestorer], 51 | activate: activateFileBrowser, 52 | autoStart: true 53 | }; 54 | 55 | /** 56 | * Activate the file browser. 57 | */ 58 | function activateFileBrowser( 59 | app: JupyterFrontEnd, 60 | manager: IDocumentManager, 61 | factory: IFileBrowserFactory, 62 | settingRegistry: ISettingRegistry, 63 | restorer: ILayoutRestorer | null 64 | ): void { 65 | // Add the GitHub backend to the contents manager. 66 | const drive = new GitHubDrive(app.docRegistry); 67 | manager.services.contents.addDrive(drive); 68 | 69 | // Create the embedded filebrowser. GitHub repos likely 70 | // don't need as often of a refresh interval as normal ones, 71 | // and rate-limiting can be an issue, so we give a 5 minute 72 | // refresh interval. 73 | const browser = factory.createFileBrowser(NAMESPACE, { 74 | driveName: drive.name, 75 | refreshInterval: 300000 76 | }); 77 | 78 | const gitHubBrowser = new GitHubFileBrowser(browser, drive); 79 | 80 | gitHubBrowser.title.icon = gitHubIcon; 81 | gitHubBrowser.title.iconClass = 'jp-SideBar-tabIcon'; 82 | gitHubBrowser.title.caption = 'Browse GitHub'; 83 | 84 | gitHubBrowser.id = 'github-file-browser'; 85 | 86 | // Add the file browser widget to the application restorer. 87 | if (restorer) { 88 | restorer.add(gitHubBrowser, NAMESPACE); 89 | } 90 | app.shell.add(gitHubBrowser, 'left', { rank: 102 }); 91 | 92 | let shouldWarn = false; 93 | const onSettingsUpdated = async (settings: ISettingRegistry.ISettings) => { 94 | const baseUrl = settings.get('baseUrl').composite as 95 | | string 96 | | null 97 | | undefined; 98 | const accessToken = settings.get('accessToken').composite as 99 | | string 100 | | null 101 | | undefined; 102 | drive.baseUrl = baseUrl || DEFAULT_GITHUB_BASE_URL; 103 | if (accessToken) { 104 | let proceed = true; 105 | if (shouldWarn) { 106 | proceed = await Private.showWarning(); 107 | } 108 | if (!proceed) { 109 | settings.remove('accessToken'); 110 | } else { 111 | drive.accessToken = accessToken; 112 | } 113 | } else { 114 | drive.accessToken = null; 115 | } 116 | }; 117 | 118 | // Fetch the initial state of the settings. 119 | Promise.all([settingRegistry.load(PLUGIN_ID), app.restored]) 120 | .then(([settings]) => { 121 | settings.changed.connect(onSettingsUpdated); 122 | onSettingsUpdated(settings); 123 | // Don't warn about access token on initial page load, but do for every setting thereafter. 124 | shouldWarn = true; 125 | const defaultRepo = settings.get('defaultRepo').composite as 126 | | string 127 | | null; 128 | if (defaultRepo) { 129 | browser.model.restored.then(() => { 130 | browser.model.cd(`/${defaultRepo}`); 131 | }); 132 | } 133 | }) 134 | .catch((reason: Error) => { 135 | console.error(reason.message); 136 | }); 137 | 138 | return; 139 | } 140 | 141 | export default fileBrowserPlugin; 142 | 143 | /** 144 | * A namespace for module-private functions. 145 | */ 146 | namespace Private { 147 | /** 148 | * Show a warning dialog about security. 149 | * 150 | * @returns whether the user accepted the dialog. 151 | */ 152 | export async function showWarning(): Promise { 153 | return showDialog({ 154 | title: 'Security Alert!', 155 | body: 156 | 'Adding a client side access token can pose a security risk! ' + 157 | 'Please consider using the server extension instead.' + 158 | 'Do you want to continue?', 159 | buttons: [ 160 | Dialog.cancelButton({ label: 'CANCEL' }), 161 | Dialog.warnButton({ label: 'PROCEED' }) 162 | ] 163 | }).then(result => { 164 | if (result.button.accept) { 165 | return true; 166 | } else { 167 | return false; 168 | } 169 | }); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/svg.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const image: string; 3 | export default image; 4 | } 5 | -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | | Copyright (c) Jupyter Development Team. 3 | | Distributed under the terms of the Modified BSD License. 4 | |---------------------------------------------------------------------------- */ 5 | 6 | [data-jp-theme-light='true'] .jp-GitHub-icon { 7 | background-image: 'url(octocat-light.svg)'; 8 | } 9 | 10 | [data-jp-theme-light='false'] .jp-GitHub-icon { 11 | background-image: 'url(octocat-dark.svg)'; 12 | } 13 | 14 | .jp-GitHubBrowser { 15 | background-color: var(--jp-layout-color1); 16 | height: 100%; 17 | } 18 | 19 | .jp-GitHubBrowser .jp-FileBrowser { 20 | flex-grow: 1; 21 | height: 100%; 22 | } 23 | 24 | .jp-GitHubUserInput { 25 | overflow: hidden; 26 | white-space: nowrap; 27 | text-align: center; 28 | font-size: large; 29 | padding: 0; 30 | background-color: var(--jp-layout-color1); 31 | } 32 | 33 | .jp-FileBrowser-toolbar.jp-Toolbar .jp-Toolbar-item.jp-GitHubUserInput { 34 | flex: 8 8; 35 | } 36 | 37 | .jp-GitHubUserInput-wrapper { 38 | background-color: var(--jp-input-active-background); 39 | border: var(--jp-border-width) solid var(--jp-border-color2); 40 | height: 30px; 41 | padding: 0 0 0 12px; 42 | margin: 0 4px 0 0; 43 | } 44 | 45 | .jp-GitHubUserInput-wrapper:focus-within { 46 | border: var(--jp-border-width) solid var(--md-blue-500); 47 | box-shadow: inset 0 0 4px var(--md-blue-300); 48 | } 49 | 50 | .jp-GitHubUserInput-wrapper input { 51 | background: transparent; 52 | float: left; 53 | border: none; 54 | outline: none; 55 | font-size: var(--jp-ui-font-size3); 56 | color: var(--jp-ui-font-color0); 57 | width: calc(100% - 18px); 58 | line-height: 28px; 59 | } 60 | 61 | .jp-GitHubUserInput-wrapper input::placeholder { 62 | color: var(--jp-ui-font-color3); 63 | font-size: var(--jp-ui-font-size1); 64 | text-transform: uppercase; 65 | } 66 | 67 | .jp-GitHubBrowser .jp-ToolbarButton.jp-Toolbar-item.jp-GitHub-toolbar-item { 68 | display: block; 69 | } 70 | 71 | .jp-GitHubBrowser .jp-ToolbarButton.jp-Toolbar-item { 72 | display: none; 73 | } 74 | 75 | .jp-GitHubBrowser .jp-DirListing-headerItem.jp-id-modified { 76 | display: none; 77 | } 78 | 79 | .jp-GitHubBrowser .jp-DirListing-itemModified { 80 | display: none; 81 | } 82 | 83 | .jp-GitHubErrorPanel { 84 | position: absolute; 85 | display: flex; 86 | flex-direction: column; 87 | justify-content: center; 88 | align-items: center; 89 | z-index: 10; 90 | left: 0; 91 | top: 0; 92 | width: 100%; 93 | height: 100%; 94 | background: var(--jp-layout-color2); 95 | } 96 | 97 | .jp-GitHubErrorImage { 98 | background-size: 100%; 99 | width: 200px; 100 | height: 165px; 101 | background-image: 'url(octocat_error.png)'; 102 | } 103 | 104 | .jp-GitHubErrorText { 105 | font-size: var(--jp-ui-font-size3); 106 | color: var(--jp-ui-font-color1); 107 | text-align: center; 108 | padding: 12px; 109 | } 110 | 111 | .jp-GitHubBrowser .jp-MyBinderButton { 112 | background-image: 'url(binder.svg)'; 113 | } 114 | 115 | .jp-GitHubBrowser .jp-MyBinderButton-disabled { 116 | opacity: 0.3; 117 | } 118 | 119 | #setting-editor .jp-PluginList-icon.jp-GitHub-icon { 120 | background-size: 85%; 121 | background-repeat: no-repeat; 122 | background-position: center; 123 | } 124 | -------------------------------------------------------------------------------- /style/binder.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | @import 'base.css'; 2 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /style/octocat-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 58 | -------------------------------------------------------------------------------- /style/octocat-light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /style/octocat_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jupyterlab/jupyterlab-github/6ab46858aa5dc0b93510b8fdb52e9066178847ea/style/octocat_error.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "noImplicitAny": true, 5 | "strictNullChecks": true, 6 | "noEmitOnError": true, 7 | "noUnusedLocals": true, 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "target": "es2018", 11 | "outDir": "lib", 12 | "allowSyntheticDefaultImports": true, 13 | "composite": true, 14 | "esModuleInterop": true, 15 | "incremental": true, 16 | "preserveWatchOutput": true, 17 | "resolveJsonModule": true, 18 | "rootDir": "src", 19 | "strict": true, 20 | "types": [], 21 | "jsx": "react" 22 | }, 23 | "include": ["src/*"] 24 | } 25 | --------------------------------------------------------------------------------