diff --git a/.copier-answers.yml b/.copier-answers.yml index 15b2ae6..650fd85 100644 --- a/.copier-answers.yml +++ b/.copier-answers.yml @@ -11,4 +11,3 @@ project_short_description: Upload Jupyter notebooks to GitHub Gist. python_name: jupyterlab_gist_it repository: https://github.com/adrn/jupyterlab-gist-it test: true - diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 093b50c..8e03ed7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,76 +11,76 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - - name: Install dependencies - run: python -m pip install -U "jupyterlab>=4.0.0,<5" - - - name: Lint the extension - run: | - set -eux - jlpm - jlpm run lint:check - - - name: Test the extension - run: | - set -eux - jlpm run test - - - name: Build the extension - run: | - set -eux - python -m pip install .[test] - - jupyter labextension list - jupyter labextension list 2>&1 | grep -ie "jupyterlab_gist_it.*OK" - python -m jupyterlab.browser_check - - - name: Package the extension - run: | - set -eux - - pip install build - python -m build - pip uninstall -y "jupyterlab_gist_it" jupyterlab - - - name: Upload extension packages - uses: actions/upload-artifact@v3 - with: - name: extension-artifacts - path: dist/jupyterlab_gist_it* - if-no-files-found: error + - name: Checkout + uses: actions/checkout@v3 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Install dependencies + run: python -m pip install -U "jupyterlab>=4.0.0,<5" + + - name: Lint the extension + run: | + set -eux + jlpm + jlpm run lint:check + + - name: Test the extension + run: | + set -eux + jlpm run test + + - name: Build the extension + run: | + set -eux + python -m pip install .[test] + + jupyter labextension list + jupyter labextension list 2>&1 | grep -ie "jupyterlab_gist_it.*OK" + python -m jupyterlab.browser_check + + - name: Package the extension + run: | + set -eux + + pip install build + python -m build + pip uninstall -y "jupyterlab_gist_it" jupyterlab + + - name: Upload extension packages + uses: actions/upload-artifact@v3 + with: + name: extension-artifacts + path: dist/jupyterlab_gist_it* + if-no-files-found: error test_isolated: needs: build runs-on: ubuntu-latest steps: - - name: Install Python - uses: actions/setup-python@v4 - with: - python-version: '3.9' - architecture: 'x64' - - uses: actions/download-artifact@v3 - with: - name: extension-artifacts - - name: Install and Test - run: | - set -eux - # Remove NodeJS, twice to take care of system and locally installed node versions. - sudo rm -rf $(which node) - sudo rm -rf $(which node) - - pip install "jupyterlab>=4.0.0,<5" jupyterlab_gist_it*.whl - - - jupyter labextension list - jupyter labextension list 2>&1 | grep -ie "jupyterlab_gist_it.*OK" - python -m jupyterlab.browser_check --no-browser-test + - name: Install Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + architecture: 'x64' + - uses: actions/download-artifact@v3 + with: + name: extension-artifacts + - name: Install and Test + run: | + set -eux + # Remove NodeJS, twice to take care of system and locally installed node versions. + sudo rm -rf $(which node) + sudo rm -rf $(which node) + + pip install "jupyterlab>=4.0.0,<5" jupyterlab_gist_it*.whl + + + jupyter labextension list + jupyter labextension list 2>&1 | grep -ie "jupyterlab_gist_it.*OK" + python -m jupyterlab.browser_check --no-browser-test integration-tests: name: Integration tests @@ -91,53 +91,53 @@ jobs: PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/pw-browsers steps: - - name: Checkout - uses: actions/checkout@v3 - - - name: Base Setup - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - - name: Download extension package - uses: actions/download-artifact@v3 - with: - name: extension-artifacts - - - name: Install the extension - run: | - set -eux - python -m pip install "jupyterlab>=4.0.0,<5" jupyterlab_gist_it*.whl - - - name: Install dependencies - working-directory: ui-tests - env: - YARN_ENABLE_IMMUTABLE_INSTALLS: 0 - PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 - run: jlpm install - - - name: Set up browser cache - uses: actions/cache@v3 - with: - path: | - ${{ github.workspace }}/pw-browsers - key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} - - - name: Install browser - run: jlpm playwright install chromium - working-directory: ui-tests - - - name: Execute integration tests - working-directory: ui-tests - run: | - jlpm playwright test - - - name: Upload Playwright Test report - if: always() - uses: actions/upload-artifact@v3 - with: - name: jupyterlab_gist_it-playwright-tests - path: | - ui-tests/test-results - ui-tests/playwright-report + - name: Checkout + uses: actions/checkout@v3 + + - name: Base Setup + uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 + + - name: Download extension package + uses: actions/download-artifact@v3 + with: + name: extension-artifacts + + - name: Install the extension + run: | + set -eux + python -m pip install "jupyterlab>=4.0.0,<5" jupyterlab_gist_it*.whl + + - name: Install dependencies + working-directory: ui-tests + env: + YARN_ENABLE_IMMUTABLE_INSTALLS: 0 + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + run: jlpm install + + - name: Set up browser cache + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/pw-browsers + key: ${{ runner.os }}-${{ hashFiles('ui-tests/yarn.lock') }} + + - name: Install browser + run: jlpm playwright install chromium + working-directory: ui-tests + + - name: Execute integration tests + working-directory: ui-tests + run: | + jlpm playwright test + + - name: Upload Playwright Test report + if: always() + uses: actions/upload-artifact@v3 + with: + name: jupyterlab_gist_it-playwright-tests + path: | + ui-tests/test-results + ui-tests/playwright-report check_links: name: Check Links diff --git a/.github/workflows/check-release.yml b/.github/workflows/check-release.yml index 2a184ed..e8d5ed0 100644 --- a/.github/workflows/check-release.yml +++ b/.github/workflows/check-release.yml @@ -1,9 +1,9 @@ name: Check Release on: push: - branches: ["main"] + branches: ['main'] pull_request: - branches: ["*"] + branches: ['*'] jobs: check_release: @@ -16,7 +16,6 @@ jobs: - name: Check Release uses: jupyter-server/jupyter_releaser/.github/actions/check-release@v2 with: - token: ${{ secrets.GITHUB_TOKEN }} - name: Upload Distributions diff --git a/.github/workflows/prep-release.yml b/.github/workflows/prep-release.yml index 6f09281..48a0630 100644 --- a/.github/workflows/prep-release.yml +++ b/.github/workflows/prep-release.yml @@ -1,22 +1,22 @@ -name: "Step 1: Prep Release" +name: 'Step 1: Prep Release' on: workflow_dispatch: inputs: version_spec: - description: "New Version Specifier" - default: "next" + description: 'New Version Specifier' + default: 'next' required: false branch: - description: "The branch to target" + description: 'The branch to target' required: false post_version_spec: - description: "Post Version Specifier" + description: 'Post Version Specifier' required: false since: - description: "Use PRs with activity since this date or git reference" + description: 'Use PRs with activity since this date or git reference' required: false since_last_stable: - description: "Use PRs with activity since the last stable git tag" + description: 'Use PRs with activity since the last stable git tag' required: false type: boolean jobs: @@ -36,6 +36,6 @@ jobs: since: ${{ github.event.inputs.since }} since_last_stable: ${{ github.event.inputs.since_last_stable }} - - name: "** Next Step **" + - name: '** Next Step **' run: | echo "Optional): Review Draft Release: ${{ steps.prep-release.outputs.release_url }}" diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml index 1ddf2e2..aeb9281 100644 --- a/.github/workflows/publish-release.yml +++ b/.github/workflows/publish-release.yml @@ -1,15 +1,15 @@ -name: "Step 2: Publish Release" +name: 'Step 2: Publish Release' on: workflow_dispatch: inputs: branch: - description: "The target branch" + description: 'The target branch' required: false release_url: - description: "The URL of the draft GitHub release" + description: 'The URL of the draft GitHub release' required: false steps_to_skip: - description: "Comma separated list of steps to skip" + description: 'Comma separated list of steps to skip' required: false jobs: @@ -44,13 +44,13 @@ jobs: token: ${{ secrets.ADMIN_GITHUB_TOKEN }} release_url: ${{ steps.populate-release.outputs.release_url }} - - name: "** Next Step **" + - name: '** Next Step **' if: ${{ success() }} run: | echo "Verify the final release" echo ${{ steps.finalize-release.outputs.release_url }} - - name: "** Failure Message **" + - name: '** Failure Message **' if: ${{ failure() }} run: | echo "Failed to Publish the Draft Release Url:" diff --git a/schema/settings.json b/schema/settings.json index 4a0db26..b047d66 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -11,4 +11,4 @@ "default": "" } } -} \ No newline at end of file +} diff --git a/src/GistHelper.ts b/src/GistHelper.ts index d5001e9..7cdd3da 100644 --- a/src/GistHelper.ts +++ b/src/GistHelper.ts @@ -1,13 +1,13 @@ import { Widget } from '@lumino/widgets'; import { NotebookPanel, INotebookModel } from '@jupyterlab/notebook'; -import { Octokit } from "@octokit/core"; +import { Octokit } from '@octokit/core'; import { showDialog } from '@jupyterlab/apputils'; import { Notification } from '@jupyterlab/apputils'; -import { PartialJSONObject} from '@lumino/coreutils'; +import { PartialJSONObject } from '@lumino/coreutils'; const GITHUB_HEADER = { 'X-GitHub-Api-Version': '2022-11-28' -} +}; export interface IGistInfo extends PartialJSONObject { gist_url?: string; @@ -16,9 +16,10 @@ export interface IGistInfo extends PartialJSONObject { } export class GistItDialog extends Widget { - - constructor(defaultDescription: string = "", headerText: string = "") { - super({ node: GistItDialog.createFormNode(headerText, defaultDescription) }); + constructor(defaultDescription: string = '', headerText: string = '') { + super({ + node: GistItDialog.createFormNode(headerText, defaultDescription) + }); } getValue(): string { @@ -26,9 +27,10 @@ export class GistItDialog extends Widget { return this.node.querySelector('input').value.trim(); } - public static createFormNode(headerText: string, defaultDescription: string): - HTMLElement { - + public static createFormNode( + headerText: string, + defaultDescription: string + ): HTMLElement { const hdrNode = document.createElement('div'); const hdr = document.createElement('span'); @@ -57,9 +59,13 @@ export class GistHelper { public panel: NotebookPanel; public model: INotebookModel; - constructor(accessToken: string, panel: NotebookPanel, model: INotebookModel) { - if (accessToken == null || accessToken == "") { - throw new Error("A Personal Access Token is required"); + constructor( + accessToken: string, + panel: NotebookPanel, + model: INotebookModel + ) { + if (accessToken == null || accessToken == '') { + throw new Error('A Personal Access Token is required'); } this.octokit = new Octokit({ auth: accessToken }); this.panel = panel; @@ -69,7 +75,7 @@ export class GistHelper { public getFiles(): any { const title = this.panel.title['_label'] as string; const content = this.model.toString(); - let files = { [title]: {"content": content } }; + let files = { [title]: { content: content } }; return files; } @@ -80,7 +86,7 @@ export class GistHelper { checkbox: { label: 'Private?', caption: 'private', - checked: false, + checked: false } }); // console.log(result); @@ -90,20 +96,20 @@ export class GistHelper { return {} as IGistInfo; } else { _public = !result.isChecked; - description = result.value ? result.value : ""; + description = result.value ? result.value : ''; } console.log(_public, description); let files = this.getFiles(); - console.log(files) + console.log(files); let gist_info = {} as IGistInfo; try { const resp = await this.octokit.request('POST /gists', { - 'description': description, - 'public': _public, - 'files': files, - 'headers': GITHUB_HEADER + description: description, + public: _public, + files: files, + headers: GITHUB_HEADER }); gist_info.gist_url = resp.data.html_url; @@ -113,12 +119,17 @@ export class GistHelper { Notification.success(`Created a new gist ${gist_info.gist_id}`, { actions: [ // @ts-ignore:next-line - { label: 'Copy URL', callback: () => navigator.clipboard.writeText(resp.data.html_url) }, - { label: 'Open Gist', callback: () => window.open(gist_info.gist_url, '_blank') } + { + label: 'Copy URL', + callback: () => navigator.clipboard.writeText(resp.data.html_url) + }, + { + label: 'Open Gist', + callback: () => window.open(gist_info.gist_url, '_blank') + } ], autoClose: 10000 }); - } catch (error: any) { Notification.error(`Failed to create Gist`, { autoClose: 5000 @@ -128,28 +139,32 @@ export class GistHelper { // @ts-ignore:next-line this.model.setMetadata('gist_info', gist_info); - console.log("Saved metadata", gist_info); + console.log('Saved metadata', gist_info); return gist_info; } public async updateGist(oldGistInfo: IGistInfo): Promise { - // first, try to load existing info from Gist let initResp; try { - initResp = await this.octokit.request(`GET /gists/${oldGistInfo.gist_id}`, { - gist_id: oldGistInfo.gist_id, - headers: GITHUB_HEADER - }); - + initResp = await this.octokit.request( + `GET /gists/${oldGistInfo.gist_id}`, + { + gist_id: oldGistInfo.gist_id, + headers: GITHUB_HEADER + } + ); } catch (error: any) { if (error.status == 404) { // Gist doesn't exist on GitHub, but it does in the notebook metadata - console.log("Gist doesn't exist, but notebook metadata contains a Gist ID"); + console.log( + "Gist doesn't exist, but notebook metadata contains a Gist ID" + ); const result = await showDialog({ - title: "Gist does not exist on GitHub but a Gist ID is stored in the notebook metadata. Create a new Gist instead?" + title: + 'Gist does not exist on GitHub but a Gist ID is stored in the notebook metadata. Create a new Gist instead?' }); console.log(result); @@ -158,11 +173,10 @@ export class GistHelper { } else if (result.button.label == 'Ok') { return this.createGist(); } else { - throw new Error("Unexpected error"); + throw new Error('Unexpected error'); } - } else { - Notification.error("Unexpected error with Gist It", { + Notification.error('Unexpected error with Gist It', { autoClose: 5000 }); throw error; @@ -176,23 +190,26 @@ export class GistHelper { body: new GistItDialog( initResp.data.description, `Existing gist: ${oldGistInfo.gist_id}` - ), + ) }); let description; if (result.button.label == 'Cancel') { return {} as IGistInfo; } else { - description = result.value ? result.value : ""; + description = result.value ? result.value : ''; } let gist_info = {} as IGistInfo; try { - const resp = await this.octokit.request(`PATCH /gists/${oldGistInfo.gist_id}`, { - 'description': description, - 'files': this.getFiles(), - 'headers': GITHUB_HEADER - }); + const resp = await this.octokit.request( + `PATCH /gists/${oldGistInfo.gist_id}`, + { + description: description, + files: this.getFiles(), + headers: GITHUB_HEADER + } + ); gist_info.gist_url = resp.data.html_url; gist_info.gist_id = resp.data.id; gist_info.create_date = resp.data.created_at; @@ -200,12 +217,17 @@ export class GistHelper { Notification.success(`Updated gist ${gist_info.gist_id}`, { actions: [ // @ts-ignore:next-line - { label: 'Copy URL', callback: () => navigator.clipboard.writeText(resp.data.html_url) }, - { label: 'Open Gist', callback: () => window.open(gist_info.gist_url, '_blank') } + { + label: 'Copy URL', + callback: () => navigator.clipboard.writeText(resp.data.html_url) + }, + { + label: 'Open Gist', + callback: () => window.open(gist_info.gist_url, '_blank') + } ], autoClose: 10000 }); - } catch (error: any) { Notification.error(`Failed to update gist ${gist_info.gist_id}`, { autoClose: 5000 @@ -214,8 +236,8 @@ export class GistHelper { // @ts-ignore:next-line this.model.setMetadata('gist_info', gist_info); - console.log("Saved metadata", gist_info); + console.log('Saved metadata', gist_info); return gist_info; } -} \ No newline at end of file +} diff --git a/src/gistitwidget.ts b/src/gistitwidget.ts index c5a0fcc..a5c6be8 100644 --- a/src/gistitwidget.ts +++ b/src/gistitwidget.ts @@ -2,17 +2,17 @@ import { Widget } from '@lumino/widgets'; import { NotebookPanel } from '@jupyterlab/notebook'; import { ISettingRegistry } from '@jupyterlab/settingregistry'; import { ToolbarButton } from '@jupyterlab/apputils'; -import { GistHelper } from "./GistHelper"; +import { GistHelper } from './GistHelper'; export const PLUGIN_NAME = 'jupyterlab_gist_it'; export interface IGistItSettings { personalAccessToken: string; - } +} // TODO: move elsewhere export class GistItDialog extends Widget { - constructor() { + constructor() { super({ node: GistItDialog.createFormNode() }); } @@ -38,78 +38,75 @@ export class GistItDialog extends Widget { } export default class GistItWidget extends Widget { - constructor( - panel: NotebookPanel, - settingRegistry: ISettingRegistry - ) { - super(); - this._panel = panel; - this._settingRegistry = settingRegistry; - - this._settingRegistry.load(`${PLUGIN_NAME}:settings`).then( - (settings: ISettingRegistry.ISettings) => { - this._updateSettings(settings); - settings.changed.connect(this._updateSettings.bind(this)); - console.log("yo dog"); - }, - (err: Error) => { - console.error( - `Could not load settings for ${PLUGIN_NAME}: ${err}` - ); - } - ); - - // Set up notebook metadata: - let model = this._panel.model; - - const buttons: Array<[string, ToolbarButton]> = []; - buttons.push([ - 'gistItSend', - new ToolbarButton({ - className: 'gistItSend', - iconClass: 'fa fa-github ', - onClick: async () => { - if (model == null) { - // TODO: notification error - return; - } - let gist_info = model.getMetadata('gist_info'); - console.log(gist_info); - - console.log(`Sup. ${this._settings.personalAccessToken} stuff`); - let gh = new GistHelper(this._settings.personalAccessToken, this._panel, model); - - if (gist_info == null || gist_info.gist_id == null) { - console.log('Creating new gist'); - gist_info = await gh.createGist(); - - } else { - console.log(`gist already exists: ${gist_info.gist_id}`); - gist_info = await gh.updateGist(gist_info); + constructor(panel: NotebookPanel, settingRegistry: ISettingRegistry) { + super(); + this._panel = panel; + this._settingRegistry = settingRegistry; + + this._settingRegistry.load(`${PLUGIN_NAME}:settings`).then( + (settings: ISettingRegistry.ISettings) => { + this._updateSettings(settings); + settings.changed.connect(this._updateSettings.bind(this)); + console.log('yo dog'); + }, + (err: Error) => { + console.error(`Could not load settings for ${PLUGIN_NAME}: ${err}`); } - - }, - tooltip: 'Gist It', - // label: 'GistIt', - }) - ]); - - this._panel.content; - - buttons.reverse(); - buttons.forEach((item) => { - panel.toolbar.insertItem(9, item[0], item[1]); - }); + ); + + // Set up notebook metadata: + let model = this._panel.model; + + const buttons: Array<[string, ToolbarButton]> = []; + buttons.push([ + 'gistItSend', + new ToolbarButton({ + className: 'gistItSend', + iconClass: 'fa fa-github ', + onClick: async () => { + if (model == null) { + // TODO: notification error + return; + } + let gist_info = model.getMetadata('gist_info'); + console.log(gist_info); + + console.log(`Sup. ${this._settings.personalAccessToken} stuff`); + let gh = new GistHelper( + this._settings.personalAccessToken, + this._panel, + model + ); + + if (gist_info == null || gist_info.gist_id == null) { + console.log('Creating new gist'); + gist_info = await gh.createGist(); + } else { + console.log(`gist already exists: ${gist_info.gist_id}`); + gist_info = await gh.updateGist(gist_info); + } + }, + tooltip: 'Gist It' + // label: 'GistIt', + }) + ]); + + this._panel.content; + + buttons.reverse(); + buttons.forEach(item => { + panel.toolbar.insertItem(9, item[0], item[1]); + }); } _updateSettings(settings: ISettingRegistry.ISettings) { - this._settings.personalAccessToken = settings.get('personalAccessToken') - .composite as string; + this._settings.personalAccessToken = settings.get('personalAccessToken') + .composite as string; } private _panel: NotebookPanel; private _settingRegistry: ISettingRegistry; private _settings: IGistItSettings = { - personalAccessToken: '' + personalAccessToken: '' }; -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index 53967f8..5dc3b83 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,10 +10,9 @@ import { ISettingRegistry } from '@jupyterlab/settingregistry'; import GistItWidget, { PLUGIN_NAME } from './gistitwidget'; - export class GistItWidgetExtension - implements DocumentRegistry.IWidgetExtension { - + implements DocumentRegistry.IWidgetExtension +{ constructor(settingRegistry: ISettingRegistry) { this._settingRegistry = settingRegistry; } @@ -33,17 +32,12 @@ const plugin: JupyterFrontEndPlugin = { description: 'Upload Jupyter notebooks to GitHub Gist.', autoStart: true, optional: [ISettingRegistry], - activate: ( - app: JupyterFrontEnd, - settingsRegistry: ISettingRegistry - ) => { - + activate: (app: JupyterFrontEnd, settingsRegistry: ISettingRegistry) => { app.docRegistry.addWidgetExtension( 'Notebook', new GistItWidgetExtension(settingsRegistry) ); console.log('JupyterLab extension jupyterlab_gist_it is activated!'); - } }; diff --git a/ui-tests/tests/jupyterlab_gist_it.spec.ts b/ui-tests/tests/jupyterlab_gist_it.spec.ts index c8627f6..f67c9bb 100644 --- a/ui-tests/tests/jupyterlab_gist_it.spec.ts +++ b/ui-tests/tests/jupyterlab_gist_it.spec.ts @@ -16,6 +16,8 @@ test('should emit an activation console message', async ({ page }) => { await page.goto(); expect( - logs.filter(s => s === 'JupyterLab extension jupyterlab_gist_it is activated!') + logs.filter( + s => s === 'JupyterLab extension jupyterlab_gist_it is activated!' + ) ).toHaveLength(1); });