├── .yarnrc.yml ├── style ├── index.js ├── index.css └── base.css ├── setup.py ├── src ├── lc_notebook_diff_components ├── theme.ts ├── __tests__ │ └── lc_notebook_diff.spec.ts ├── widgets │ ├── icons.tsx │ ├── files-initializer.ts │ └── main.tsx ├── index.ts └── components │ ├── view.tsx │ └── select-files.tsx ├── ui-tests ├── e2e-notebook │ ├── notebooks │ │ └── scripts │ │ │ ├── __init__.py │ │ │ ├── jupyterlab.py │ │ │ └── playwright.py │ ├── requirements.txt │ └── run_notebooks.py ├── playwright.config.js ├── jupyter_server_test_config.py ├── package.json ├── tests │ └── lc_notebook_diff.spec.ts └── README.md ├── MANIFEST.in ├── babel.config.js ├── CHANGELOG.md ├── .prettierignore ├── tsconfig.test.json ├── install.json ├── nbextension ├── typings.json ├── src │ └── index.ts ├── package.json ├── tsconfig.json └── package-lock.json ├── lc_notebook_diff ├── nbextension │ ├── notebook_diff.yaml │ ├── main.css │ ├── jupyter-notebook-diff.css │ └── main.js └── __init__.py ├── components ├── src │ ├── index.ts │ ├── Notebook.ts │ ├── Relation.ts │ ├── Cell.ts │ └── DiffView.ts ├── package.json ├── package-lock.json └── tsconfig.json ├── postBuild ├── .dockerignore ├── .copier-answers.yml ├── tsconfig.json ├── jest.config.js ├── hatch_build.py ├── .github └── workflows │ ├── binder-on-pr.yml │ ├── build.yml │ ├── e2e-tests.yml │ └── release.yml ├── Dockerfile ├── LICENSE.txt ├── .gitignore ├── pyproject.toml ├── README.md ├── RELEASE.md ├── package.json └── html ├── 02.ipynb └── 03.ipynb /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /style/index.js: -------------------------------------------------------------------------------- 1 | import './base.css'; 2 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | __import__("setuptools").setup() 2 | -------------------------------------------------------------------------------- /src/lc_notebook_diff_components: -------------------------------------------------------------------------------- 1 | ../components/src -------------------------------------------------------------------------------- /style/index.css: -------------------------------------------------------------------------------- 1 | @import url('base.css'); 2 | -------------------------------------------------------------------------------- /ui-tests/e2e-notebook/notebooks/scripts/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include lc_notebook_diff/nbextension/* 2 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = require('@jupyterlab/testutils/lib/babel.config'); 2 | -------------------------------------------------------------------------------- /ui-tests/e2e-notebook/requirements.txt: -------------------------------------------------------------------------------- 1 | papermill>=2.5.0 2 | playwright>=1.45.0 3 | jupyterlab>=4.4.0 4 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/theme.ts: -------------------------------------------------------------------------------- 1 | import { createTheme } from '@mui/material/styles'; 2 | 3 | export const theme = createTheme({}); 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | **/node_modules 3 | **/lib 4 | **/package.json 5 | !/package.json 6 | lc_notebook_diff 7 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "types": ["jest"] 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /install.json: -------------------------------------------------------------------------------- 1 | { 2 | "packageManager": "python", 3 | "packageName": "lc_notebook_diff", 4 | "uninstallInstructions": "Use your Python package manager (pip, conda, etc.) to uninstall the package lc_notebook_diff" 5 | } 6 | -------------------------------------------------------------------------------- /nbextension/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "codemirror": "registry:dt/codemirror#0.0.0+20170209230452" 4 | }, 5 | "globalDependencies": { 6 | "jquery": "registry:dt/jquery#1.10.0+20170310222111" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/__tests__/lc_notebook_diff.spec.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests 3 | */ 4 | 5 | describe('lc_notebook_diff', () => { 6 | it('should be tested', () => { 7 | expect(1 + 1).toEqual(2); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /lc_notebook_diff/nbextension/notebook_diff.yaml: -------------------------------------------------------------------------------- 1 | Type: Jupyter Notebook Extension 2 | Name: LC notebook diff 3 | Section: tree 4 | Description: "Diff Extension for Notebooks" 5 | Main: main.js 6 | # 1.x means nbclassic - leave 6.x in place just in case. 7 | Compatibility: 1.x 5.x 6.x 8 | -------------------------------------------------------------------------------- /components/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | DiffView, 3 | MergeViewProvider, 4 | MergeViewOptions, 5 | MergeViewResult 6 | } from './DiffView'; 7 | export { Notebook } from './Notebook'; 8 | export { Cell } from './Cell'; 9 | export { Relation, RelationMatchType } from './Relation'; 10 | -------------------------------------------------------------------------------- /postBuild: -------------------------------------------------------------------------------- 1 | pip --no-cache-dir install jupyter_nbextensions_configurator 2 | jupyter nbextensions_configurator enable --sys-prefix 3 | 4 | jupyter nbextension install --py lc_notebook_diff --sys-prefix 5 | jupyter nbextension enable --py lc_notebook_diff --sys-prefix 6 | 7 | pip install https://github.com/NII-cloud-operation/Jupyter-LC_nblineage/tarball/master 8 | 9 | jupyter nblineage quick-setup --sys-prefix 10 | -------------------------------------------------------------------------------- /ui-tests/playwright.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Configuration for Playwright using default from @jupyterlab/galata 3 | */ 4 | const baseConfig = require('@jupyterlab/galata/lib/playwright-config'); 5 | 6 | module.exports = { 7 | ...baseConfig, 8 | webServer: { 9 | command: 'jlpm start', 10 | url: 'http://localhost:8888/lab', 11 | timeout: 120 * 1000, 12 | reuseExistingServer: !process.env.CI 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /lc_notebook_diff/nbextension/main.css: -------------------------------------------------------------------------------- 1 | #diff-content-container { 2 | margin: 1em; 3 | } 4 | 5 | .diff-content-panel { 6 | margin: 1em; 7 | } 8 | 9 | .diff-content-panel .list-group { 10 | margin: 1em; 11 | } 12 | 13 | #diff-content .title { 14 | font-weight: bold; 15 | } 16 | 17 | #notebook_diff_error .alert { 18 | margin: 1em; 19 | } 20 | 21 | span.nbdiff-order { 22 | margin-left: 1em; 23 | } 24 | 25 | button.nbdiff-order { 26 | padding: 0; 27 | } 28 | -------------------------------------------------------------------------------- /ui-tests/jupyter_server_test_config.py: -------------------------------------------------------------------------------- 1 | """Server configuration for integration tests. 2 | 3 | !! Never use this configuration in production because it 4 | opens the server to the world and provide access to JupyterLab 5 | JavaScript objects through the global window variable. 6 | """ 7 | from jupyterlab.galata import configure_jupyter_server 8 | 9 | configure_jupyter_server(c) 10 | 11 | # Uncomment to set server log level to debug level 12 | # c.ServerApp.log_level = "DEBUG" 13 | -------------------------------------------------------------------------------- /components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc_notebook_diff_components", 3 | "version": "0.1.0", 4 | "description": "Components for lc_notebook_diff", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "scripts": { 8 | "build": "tsc", 9 | "test": "echo \"Error: no test specified\" && exit 1" 10 | }, 11 | "author": "", 12 | "license": "BSD-3-Clause", 13 | "devDependencies": { 14 | "@types/jquery": "^3.5.29", 15 | "typescript": "^5.3.3" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ui-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc_notebook_diff-ui-tests", 3 | "version": "1.0.0", 4 | "description": "JupyterLab lc_notebook_diff Integration Tests", 5 | "private": true, 6 | "scripts": { 7 | "start": "jupyter lab --config jupyter_server_test_config.py", 8 | "test": "jlpm playwright test", 9 | "test:update": "jlpm playwright test --update-snapshots" 10 | }, 11 | "devDependencies": { 12 | "@jupyterlab/galata": "^5.0.5", 13 | "@playwright/test": "^1.37.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | lib/ 2 | node_modules/ 3 | components/lib/ 4 | components/node_modules/ 5 | nbextension/lib/ 6 | nbextension/node_modules/ 7 | nbextension/diff_match_patch.js 8 | lc_notebook_diff/labextension/ 9 | lc_notebook_diff/nbextension/diff_match_patch.js 10 | lc_notebook_diff/nbextension/jupyter-notebook-diff.js 11 | lc_notebook_diff/nbextension/jupyter-notebook-diff.js.map 12 | *.tsbuildinfo 13 | ui-tests/test-results/ 14 | ui-tests/playwright-report/ 15 | ui-tests/e2e-notebook/artifacts/ 16 | .git/ 17 | .github/ 18 | .eslintcache 19 | .stylelintcache 20 | __pycache__/ 21 | .DS_Store 22 | -------------------------------------------------------------------------------- /.copier-answers.yml: -------------------------------------------------------------------------------- 1 | # Changes here will be overwritten by Copier; NEVER EDIT MANUALLY 2 | _commit: v4.2.4 3 | _src_path: https://github.com/jupyterlab/extension-template 4 | author_email: yazawa@yzwlab.net 5 | author_name: nii-cloud-operation 6 | data_format: string 7 | file_extension: '' 8 | has_binder: false 9 | has_settings: false 10 | kind: frontend 11 | labextension_name: lc_notebook_diff 12 | mimetype: '' 13 | mimetype_name: '' 14 | project_short_description: diff extension 15 | python_name: lc_notebook_diff 16 | repository: https://github.com/NII-cloud-operation/Jupyter-LC_notebook_diff 17 | test: true 18 | viewer_name: '' 19 | 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "composite": true, 5 | "declaration": true, 6 | "esModuleInterop": true, 7 | "incremental": true, 8 | "jsx": "react", 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "noEmitOnError": true, 12 | "noImplicitAny": true, 13 | "noUnusedLocals": true, 14 | "preserveWatchOutput": true, 15 | "resolveJsonModule": true, 16 | "outDir": "lib", 17 | "rootDir": "src", 18 | "strict": true, 19 | "strictNullChecks": true, 20 | "target": "ES2018" 21 | }, 22 | "include": ["src/*", "src/**/*"], 23 | } 24 | -------------------------------------------------------------------------------- /src/widgets/icons.tsx: -------------------------------------------------------------------------------- 1 | import { LabIcon } from '@jupyterlab/ui-components'; 2 | 3 | import { faCodeCompare } from '@fortawesome/free-solid-svg-icons'; 4 | 5 | type Icon = { 6 | icon: any[]; 7 | }; 8 | 9 | function extractSvgString(icon: Icon): string { 10 | const path = icon.icon[4]; 11 | return ` 12 | 13 | 14 | 15 | `; 16 | } 17 | 18 | export const diffIcon = new LabIcon({ 19 | name: 'notebook_diff::notebookdiff', 20 | svgstr: extractSvgString(faCodeCompare) 21 | }); 22 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const jestJupyterLab = require('@jupyterlab/testutils/lib/jest-config'); 2 | 3 | const esModules = [ 4 | '@codemirror', 5 | '@jupyter/ydoc', 6 | '@jupyterlab/', 7 | 'lib0', 8 | 'nanoid', 9 | 'vscode-ws-jsonrpc', 10 | 'y-protocols', 11 | 'y-websocket', 12 | 'yjs' 13 | ].join('|'); 14 | 15 | const baseConfig = jestJupyterLab(__dirname); 16 | 17 | module.exports = { 18 | ...baseConfig, 19 | automock: false, 20 | collectCoverageFrom: [ 21 | 'src/**/*.{ts,tsx}', 22 | '!src/**/*.d.ts', 23 | '!src/**/.ipynb_checkpoints/*' 24 | ], 25 | coverageReporters: ['lcov', 'text'], 26 | testRegex: 'src/.*/.*.spec.ts[x]?$', 27 | transformIgnorePatterns: [`/node_modules/(?!${esModules}).+`] 28 | }; 29 | -------------------------------------------------------------------------------- /nbextension/src/index.ts: -------------------------------------------------------------------------------- 1 | declare var define:any; 2 | define(JupyterNotebook); 3 | 4 | import { DiffView as DiffViewBase, RelationMatchType } from 'lc_notebook_diff_components'; 5 | 6 | namespace JupyterNotebook { 7 | export class DiffView extends DiffViewBase { 8 | constructor( 9 | rootSelector: string, codeMirror: any, filenames: string[], 10 | filecontents: string[], 11 | errorCallback?: (url: string, jqXHR: any, textStatus: string, errorThrown: any) => void, 12 | {matchType = RelationMatchType.Fuzzy}: { matchType?: RelationMatchType } = {} 13 | ) { 14 | super($, rootSelector, codeMirror, filenames, filecontents, errorCallback, { matchType: matchType }); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /nbextension/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc_notebook_diff_nbextension", 3 | "version": "0.1.0", 4 | "description": "nbextension for lc_notebook_diff", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "tsc && esbuild src/index.ts --bundle --outfile=../lc_notebook_diff/nbextension/jupyter-notebook-diff.js --sourcemap --target=es2016 --platform=browser --format=cjs && curl -L https://github.com/google/diff-match-patch/raw/master/javascript/diff_match_patch.js > ../lc_notebook_diff/nbextension/diff_match_patch.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "author": "", 11 | "license": "BSD-3-Clause", 12 | "dependencies": { 13 | "lc_notebook_diff_components": "file:../components" 14 | }, 15 | "devDependencies": { 16 | "esbuild": "^0.19.11", 17 | "typescript": "^5.3.3" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /hatch_build.py: -------------------------------------------------------------------------------- 1 | """Custom build script for hatch backend""" 2 | import os 3 | from pathlib import Path 4 | import subprocess 5 | import sys 6 | try: 7 | from urllib.request import urlopen 8 | except ImportError: 9 | from urllib import urlopen 10 | 11 | HERE = Path(__file__).parent.resolve() 12 | LIB = os.path.join(HERE, 'lc_notebook_diff', 'nbextension') 13 | from hatchling.builders.hooks.plugin.interface import BuildHookInterface 14 | 15 | def build_nbextension(): 16 | # Execute the node build script 17 | subprocess.check_call(['npm', 'run', 'build'], cwd=LIB) 18 | 19 | class CustomHook(BuildHookInterface): 20 | """A custom build hook.""" 21 | PLUGIN_NAME = "custom" 22 | 23 | def initialize(self, version, build_data): 24 | """Initialize the hook.""" 25 | if self.target_name not in ["sdist"]: 26 | return 27 | build_nbextension() -------------------------------------------------------------------------------- /lc_notebook_diff/__init__.py: -------------------------------------------------------------------------------- 1 | try: 2 | from ._version import __version__ 3 | except ImportError: 4 | # Fallback when using the package in dev mode without installing 5 | # in editable mode with pip. It is highly recommended to install 6 | # the package from a stable release or in editable mode: https://pip.pypa.io/en/stable/topics/local-project-installs/#editable-installs 7 | import warnings 8 | warnings.warn("Importing 'lc_notebook_diff' outside a proper installation.") 9 | __version__ = "dev" 10 | 11 | 12 | def _jupyter_labextension_paths(): 13 | return [{ 14 | "src": "labextension", 15 | "dest": "lc_notebook_diff" 16 | }] 17 | 18 | # nbextension 19 | def _jupyter_nbextension_paths(): 20 | return [dict( 21 | section="tree", 22 | src="nbextension", 23 | dest="notebook_diff", 24 | require="notebook_diff/main")] 25 | -------------------------------------------------------------------------------- /src/widgets/files-initializer.ts: -------------------------------------------------------------------------------- 1 | import { JupyterFrontEnd } from '@jupyterlab/application'; 2 | import { Widget, TabPanel } from '@lumino/widgets'; 3 | 4 | type WidgetFactory = (forNotebook7: boolean) => Widget; 5 | 6 | export class FilesInitializer { 7 | widgetFactory: WidgetFactory; 8 | 9 | constructor(widgetFactory: WidgetFactory) { 10 | this.widgetFactory = widgetFactory; 11 | } 12 | 13 | start(app: JupyterFrontEnd) { 14 | this.addToMain(app); 15 | } 16 | 17 | addToMain(app: JupyterFrontEnd) { 18 | const widgets = Array.from(app.shell.widgets('main')); 19 | if (widgets.length === 0) { 20 | setTimeout(() => { 21 | this.addToMain(app); 22 | }, 10); 23 | return; 24 | } 25 | const tab = widgets[0] as TabPanel; 26 | if (!tab.addWidget) { 27 | console.log('lc_notebook_diff: running on JupyterLab'); 28 | app.shell.add(this.widgetFactory(false), 'left', { rank: 2000 }); 29 | return; 30 | } 31 | console.log('lc_notebook_diff: running on Jupyter Notebook 7'); 32 | tab.addWidget(this.widgetFactory(true)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.github/workflows/binder-on-pr.yml: -------------------------------------------------------------------------------- 1 | # Reference https://mybinder.readthedocs.io/en/latest/howto/gh-actions-badges.html 2 | name: Binder Badge 3 | on: 4 | pull_request_target: 5 | types: [opened] 6 | 7 | permissions: 8 | pull-requests: write 9 | 10 | 11 | jobs: 12 | binder: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: comment on PR with Binder link 16 | uses: actions/github-script@v3 17 | with: 18 | github-token: ${{secrets.GITHUB_TOKEN}} 19 | script: | 20 | var PR_HEAD_USERREPO = process.env.PR_HEAD_USERREPO; 21 | var PR_HEAD_REF = process.env.PR_HEAD_REF; 22 | github.issues.createComment({ 23 | issue_number: context.issue.number, 24 | owner: context.repo.owner, 25 | repo: context.repo.repo, 26 | 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 on branch _${PR_HEAD_USERREPO}/${PR_HEAD_REF}_` 27 | }) 28 | env: 29 | PR_HEAD_REF: ${{ github.event.pull_request.head.ref }} 30 | PR_HEAD_USERREPO: ${{ github.event.pull_request.head.repo.full_name }} 31 | 32 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | JupyterFrontEnd, 3 | JupyterFrontEndPlugin 4 | } from '@jupyterlab/application'; 5 | import { IDocumentManager } from '@jupyterlab/docmanager'; 6 | import diff from 'diff-match-patch'; 7 | 8 | import { FilesInitializer } from './widgets/files-initializer'; 9 | import { buildWidget } from './widgets/main'; 10 | import { DiffView } from './lc_notebook_diff_components'; 11 | 12 | Object.keys(diff).forEach(key => { 13 | (window as any)[key] = (diff as any)[key]; 14 | }); 15 | 16 | function initWidgets(app: JupyterFrontEnd, documents: IDocumentManager) { 17 | const initializer = new FilesInitializer((withLabel: boolean) => 18 | buildWidget(documents, withLabel) 19 | ); 20 | initializer.start(app); 21 | } 22 | 23 | /** 24 | * Initialization data for the lc_notebook_diff extension. 25 | */ 26 | const plugin: JupyterFrontEndPlugin = { 27 | id: 'lc_notebook_diff:plugin', 28 | description: 'diff extension', 29 | autoStart: true, 30 | requires: [IDocumentManager], 31 | activate: (app: JupyterFrontEnd, documents: IDocumentManager) => { 32 | console.log('JupyterLab extension lc_notebook_diff is activated!'); 33 | console.log('DiffView', DiffView); 34 | initWidgets(app, documents); 35 | } 36 | }; 37 | 38 | export default plugin; 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM quay.io/jupyter/scipy-notebook:notebook-7.5.0 2 | 3 | USER root 4 | 5 | # Install Node.js 20.x (required for Etherpad build) 6 | RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ 7 | apt-get install -y nodejs && \ 8 | apt-get clean && \ 9 | mkdir -p /.npm && \ 10 | chown jovyan:users -R /.npm && \ 11 | rm -rf /var/lib/apt/lists/* 12 | ENV NPM_CONFIG_PREFIX=/.npm 13 | ENV PATH=/.npm/bin/:${PATH} 14 | 15 | RUN pip install --no-cache jupyter_nbextensions_configurator 16 | 17 | COPY . /tmp/notebook_diff 18 | RUN cd /tmp/notebook_diff/components && npm install && npm run build && \ 19 | cd /tmp/notebook_diff/nbextension && npm install && npm run build && \ 20 | pip install --no-cache /tmp/notebook_diff 21 | 22 | RUN jupyter labextension enable lc_notebook_diff 23 | 24 | RUN jupyter nbclassic-extension install --py jupyter_nbextensions_configurator --sys-prefix && \ 25 | jupyter nbclassic-extension enable --py jupyter_nbextensions_configurator --sys-prefix && \ 26 | jupyter nbclassic-serverextension enable --py jupyter_nbextensions_configurator --sys-prefix && \ 27 | jupyter nbclassic-extension install --py lc_notebook_diff --sys-prefix && \ 28 | jupyter nbclassic-extension enable --py lc_notebook_diff --sys-prefix && \ 29 | fix-permissions /home/$NB_USER 30 | 31 | USER $NB_USER 32 | 33 | RUN cp -fr /tmp/notebook_diff/html /home/$NB_USER/ 34 | -------------------------------------------------------------------------------- /ui-tests/e2e-notebook/notebooks/scripts/jupyterlab.py: -------------------------------------------------------------------------------- 1 | """Helper functions for LC Wrapper E2E tests.""" 2 | 3 | 4 | async def ensure_launcher_tab_opened(page): 5 | """Ensure launcher tab is opened and active.""" 6 | # Check if Launcher tab exists, if not create one 7 | launcher_tab = page.locator('//*[contains(@class, "lm-TabBar-tabLabel") and text() = "Launcher"]') 8 | if not await launcher_tab.is_visible(): 9 | # Click the "+" button to open a new launcher 10 | await page.locator('//*[@data-command="launcher:create"]').click() 11 | 12 | # Click on "Launcher" tab to make sure it's active 13 | await page.locator('//*[contains(@class, "lm-TabBar-tabLabel") and text() = "Launcher"]').click() 14 | 15 | 16 | async def get_notebook_panel_ids(page): 17 | """Get set of all notebook panel IDs.""" 18 | notebook_panels = page.locator('.jp-NotebookPanel') 19 | count = await notebook_panels.count() 20 | ids = [] 21 | for i in range(count): 22 | panel = notebook_panels.nth(i) 23 | panel_id = await panel.get_attribute('id') 24 | ids.append(panel_id) 25 | return set(ids) 26 | 27 | 28 | def get_file_browser_item_locator(page, filename): 29 | return page.locator(f'//*[contains(@class, "jp-DirListing-item") and contains(@title, "Name: {filename}")]') 30 | 31 | 32 | def get_current_tab_closer_locator(page): 33 | """Get locator for current tab's close button.""" 34 | return page.locator('.jp-mod-current .lm-TabBar-tabCloseIcon') 35 | -------------------------------------------------------------------------------- /components/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc_notebook_diff_components", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "lc_notebook_diff_components", 9 | "version": "0.1.0", 10 | "license": "BSD-3-Clause", 11 | "devDependencies": { 12 | "@types/jquery": "^3.5.29", 13 | "typescript": "^5.3.3" 14 | } 15 | }, 16 | "node_modules/@types/jquery": { 17 | "version": "3.5.29", 18 | "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.29.tgz", 19 | "integrity": "sha512-oXQQC9X9MOPRrMhPHHOsXqeQDnWeCDT3PelUIg/Oy8FAbzSZtFHRjc7IpbfFVmpLtJ+UOoywpRsuO5Jxjybyeg==", 20 | "dev": true, 21 | "dependencies": { 22 | "@types/sizzle": "*" 23 | } 24 | }, 25 | "node_modules/@types/sizzle": { 26 | "version": "2.3.8", 27 | "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.8.tgz", 28 | "integrity": "sha512-0vWLNK2D5MT9dg0iOo8GlKguPAU02QjmZitPEsXRuJXU/OGIOt9vT9Fc26wtYuavLxtO45v9PGleoL9Z0k1LHg==", 29 | "dev": true 30 | }, 31 | "node_modules/typescript": { 32 | "version": "5.3.3", 33 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 34 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 35 | "dev": true, 36 | "bin": { 37 | "tsc": "bin/tsc", 38 | "tsserver": "bin/tsserver" 39 | }, 40 | "engines": { 41 | "node": ">=14.17" 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018, National Institute of Informatics. 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 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * 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 | * Neither the name of National Institute of Informatics 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 | -------------------------------------------------------------------------------- /src/components/view.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useMemo } from 'react'; 2 | import { INotebookModel } from '@jupyterlab/notebook'; 3 | import jquery from 'jquery'; 4 | import { MergeView } from 'codemirror'; 5 | import 'codemirror/lib/codemirror.css'; 6 | import 'codemirror/addon/merge/merge'; 7 | import 'codemirror/addon/merge/merge.css'; 8 | import 'codemirror/addon/search/searchcursor'; 9 | import 'codemirror/addon/search/matchesonscrollbar'; 10 | import 'codemirror/addon/scroll/annotatescrollbar'; 11 | 12 | import { 13 | DiffView, 14 | MergeViewOptions, 15 | MergeViewProvider, 16 | MergeViewResult 17 | } from '../lc_notebook_diff_components'; 18 | import { NotebookPath } from './select-files'; 19 | 20 | export type Notebook = { 21 | path: NotebookPath; 22 | content: INotebookModel; 23 | }; 24 | 25 | export type Props = { 26 | notebooks: Notebook[]; 27 | }; 28 | 29 | class MergeViewManager implements MergeViewProvider { 30 | MergeView(element: HTMLElement, options: MergeViewOptions): MergeViewResult { 31 | const mv = new MergeView(element, options); 32 | return { 33 | edit: mv.editor() 34 | }; 35 | } 36 | } 37 | 38 | export function NotebooksView({ notebooks }: Props) { 39 | const viewId = useMemo(() => { 40 | const notebookIds = notebooks.map(notebook => 41 | encodeURIComponent(notebook.path).replace(/[%.]/g, '-') 42 | ); 43 | return `diff-view-${notebookIds.join('-')}`; 44 | }, [notebooks]); 45 | 46 | useEffect(() => { 47 | setTimeout(() => { 48 | new DiffView( 49 | jquery, 50 | `#${viewId}`, 51 | new MergeViewManager(), 52 | notebooks.map(notebook => notebook.path), 53 | notebooks.map(notebook => notebook.content) 54 | ); 55 | }, 100); 56 | }, [notebooks, viewId]); 57 | 58 | return
; 59 | } 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.bundle.* 2 | lib/ 3 | node_modules/ 4 | *.log 5 | .eslintcache 6 | .stylelintcache 7 | *.egg-info/ 8 | .ipynb_checkpoints 9 | *.tsbuildinfo 10 | lc_notebook_diff/labextension 11 | # Version file is handled by hatchling 12 | lc_notebook_diff/_version.py 13 | 14 | # Integration tests 15 | ui-tests/test-results/ 16 | ui-tests/playwright-report/ 17 | ui-tests/e2e-notebook/artifacts/ 18 | 19 | # Created by https://www.gitignore.io/api/python 20 | # Edit at https://www.gitignore.io/?templates=python 21 | 22 | ### Python ### 23 | # Byte-compiled / optimized / DLL files 24 | __pycache__/ 25 | *.py[cod] 26 | *$py.class 27 | 28 | # C extensions 29 | *.so 30 | 31 | # Distribution / packaging 32 | .Python 33 | build/ 34 | develop-eggs/ 35 | dist/ 36 | downloads/ 37 | eggs/ 38 | .eggs/ 39 | lib/ 40 | lib64/ 41 | parts/ 42 | sdist/ 43 | var/ 44 | wheels/ 45 | pip-wheel-metadata/ 46 | share/python-wheels/ 47 | .installed.cfg 48 | *.egg 49 | MANIFEST 50 | 51 | # PyInstaller 52 | # Usually these files are written by a python script from a template 53 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 54 | *.manifest 55 | *.spec 56 | 57 | # Installer logs 58 | pip-log.txt 59 | pip-delete-this-directory.txt 60 | 61 | # Unit test / coverage reports 62 | htmlcov/ 63 | .tox/ 64 | .nox/ 65 | .coverage 66 | .coverage.* 67 | .cache 68 | nosetests.xml 69 | coverage/ 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 | # Yarn cache 126 | .yarn/ 127 | 128 | # Working files 129 | work/ 130 | lc_notebook_diff/nbextension/diff_match_patch.js 131 | lc_notebook_diff/nbextension/jupyter-notebook-diff.js 132 | lc_notebook_diff/nbextension/jupyter-notebook-diff.js.map 133 | nbextension/diff_match_patch.js 134 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: main 6 | pull_request: 7 | branches: '*' 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v2 15 | 16 | - name: Base Setup 17 | uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 18 | 19 | - name: Install dependencies 20 | run: python -m pip install -U 'jupyterlab>=4.0.0,<5' && python -m pip install -U 'notebook>=7,<8' 21 | 22 | - name: Build the extension 23 | run: | 24 | set -eux 25 | jlpm 26 | jlpm run eslint:check 27 | cd ./components 28 | npm install 29 | npm run build 30 | cd .. 31 | cd ./nbextension 32 | npm install 33 | npm run build 34 | cd .. 35 | python -m pip install . 36 | 37 | jupyter labextension list 2>&1 | grep -ie "lc_notebook_diff.*OK" 38 | python -m jupyterlab.browser_check 39 | 40 | jlpm install 41 | cd ./ui-tests 42 | jlpm install 43 | jlpm playwright install 44 | jlpm playwright test 45 | cd .. 46 | 47 | pip install build 48 | python -m build --sdist 49 | cp dist/*.tar.gz myextension.tar.gz 50 | pip uninstall -y "lc_notebook_diff" jupyterlab 51 | rm -rf myextension 52 | 53 | npm pack 54 | mv lc_notebook_diff-*.tgz myextension-nodejs.tgz 55 | 56 | - uses: actions/upload-artifact@v4 57 | if: ${{ !cancelled() }} 58 | with: 59 | name: playwright-report 60 | path: ui-tests/playwright-report/ 61 | retention-days: 30 62 | 63 | - uses: actions/upload-artifact@v4 64 | with: 65 | name: myextension-sdist 66 | path: myextension.tar.gz 67 | 68 | - uses: actions/upload-artifact@v4 69 | with: 70 | name: myextension-nodejs 71 | path: myextension-nodejs.tgz 72 | 73 | test_isolated: 74 | needs: build 75 | runs-on: ubuntu-latest 76 | 77 | steps: 78 | - name: Checkout 79 | uses: actions/checkout@v2 80 | - name: Install Python 81 | uses: actions/setup-python@v2 82 | with: 83 | python-version: '3.9' 84 | architecture: 'x64' 85 | - uses: actions/download-artifact@v4 86 | with: 87 | name: myextension-sdist 88 | - name: Install and Test 89 | run: | 90 | set -eux 91 | # Remove NodeJS, twice to take care of system and locally installed node versions. 92 | sudo rm -rf $(which node) 93 | sudo rm -rf $(which node) 94 | pip install myextension.tar.gz 95 | pip install jupyterlab 96 | jupyter labextension list 2>&1 | grep -ie "lc_notebook_diff.*OK" 97 | python -m jupyterlab.browser_check --no-chrome-test 98 | -------------------------------------------------------------------------------- /src/components/select-files.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback } from 'react'; 2 | import { Box, Grid } from '@mui/material'; 3 | import { ArrowDownward, ArrowUpward } from '@mui/icons-material'; 4 | 5 | export type NotebookPath = string; 6 | 7 | export type Props = { 8 | numberOfFiles: number; 9 | onFilesSelected?: (files: NotebookPath[]) => void; 10 | }; 11 | 12 | export function SelectFiles({ numberOfFiles, onFilesSelected }: Props) { 13 | const [filenames, setFilenames] = useState([]); 14 | 15 | const notifyFilesSelected = useCallback( 16 | (changedFilenames: string[]) => { 17 | if (!onFilesSelected) { 18 | return; 19 | } 20 | onFilesSelected(changedFilenames); 21 | }, 22 | [onFilesSelected] 23 | ); 24 | 25 | return ( 26 | 27 | {Array.from({ length: numberOfFiles }).map((_, index) => ( 28 | 34 | 35 | File #{index + 1} 36 | 37 | 38 | { 41 | const newFilenames = [...filenames]; 42 | newFilenames[index] = event.target.value; 43 | setFilenames(newFilenames); 44 | notifyFilesSelected(newFilenames); 45 | }} 46 | /> 47 | 48 | 49 | {index > 0 && ( 50 | 63 | )} 64 | {index < numberOfFiles - 1 && ( 65 | 78 | )} 79 | 80 | 81 | ))} 82 | 83 | ); 84 | } 85 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["hatchling>=1.5.0", "jupyterlab>=4.0.0,<5", "hatch-nodejs-version>=0.3.2"] 3 | build-backend = "hatchling.build" 4 | 5 | [project] 6 | name = "lc_notebook_diff" 7 | readme = "README.md" 8 | license = { file = "LICENSE.txt" } 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 | "Programming Language :: Python :: 3.12", 24 | ] 25 | dependencies = [ 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 | # Used to call hatch_build.py 36 | [tool.hatch.build.hooks.custom] 37 | 38 | [tool.hatch.build.targets.sdist.force-include] 39 | "lc_notebook_diff/nbextension/diff_match_patch.js" = "lc_notebook_diff/nbextension/diff_match_patch.js" 40 | "lc_notebook_diff/nbextension/jupyter-notebook-diff.js" = "lc_notebook_diff/nbextension/jupyter-notebook-diff.js" 41 | "components/src" = "src/lc_notebook_diff_components" 42 | 43 | [tool.hatch.build.targets.sdist] 44 | artifacts = ["lc_notebook_diff/labextension"] 45 | exclude = [".github", "binder"] 46 | 47 | [tool.hatch.build.targets.wheel.shared-data] 48 | "lc_notebook_diff/labextension" = "share/jupyter/labextensions/lc_notebook_diff" 49 | "install.json" = "share/jupyter/labextensions/lc_notebook_diff/install.json" 50 | "lc_notebook_diff/nbextension" = "share/jupyter/nbextensions/notebook_diff" 51 | 52 | [tool.hatch.build.hooks.version] 53 | path = "lc_notebook_diff/_version.py" 54 | 55 | [tool.hatch.build.hooks.jupyter-builder] 56 | dependencies = ["hatch-jupyter-builder>=0.5"] 57 | build-function = "hatch_jupyter_builder.npm_builder" 58 | ensured-targets = [ 59 | "lc_notebook_diff/labextension/static/style.js", 60 | "lc_notebook_diff/labextension/package.json", 61 | ] 62 | skip-if-exists = ["lc_notebook_diff/labextension/static/style.js"] 63 | 64 | [tool.hatch.build.hooks.jupyter-builder.build-kwargs] 65 | build_cmd = "build:prod" 66 | npm = ["jlpm"] 67 | 68 | [tool.hatch.build.hooks.jupyter-builder.editable-build-kwargs] 69 | build_cmd = "install:extension" 70 | npm = ["jlpm"] 71 | source_dir = "src" 72 | build_dir = "lc_notebook_diff/labextension" 73 | 74 | [tool.jupyter-releaser.options] 75 | version_cmd = "hatch version" 76 | 77 | [tool.jupyter-releaser.hooks] 78 | before-build-npm = [ 79 | "python -m pip install 'jupyterlab>=4.0.0,<5'", 80 | "jlpm", 81 | "jlpm build:prod" 82 | ] 83 | before-build-python = ["jlpm clean:all"] 84 | 85 | [tool.check-wheel-contents] 86 | ignore = ["W002"] 87 | -------------------------------------------------------------------------------- /src/widgets/main.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useState } from 'react'; 2 | import { Box, Button, ThemeProvider } from '@mui/material'; 3 | 4 | import { ReactWidget } from '@jupyterlab/apputils'; 5 | import { IDocumentManager } from '@jupyterlab/docmanager'; 6 | import { INotebookModel } from '@jupyterlab/notebook'; 7 | import { Notification } from '@jupyterlab/apputils'; 8 | 9 | import { theme } from '../theme'; 10 | import { NotebookPath, SelectFiles } from '../components/select-files'; 11 | import { NotebooksView } from '../components/view'; 12 | import { diffIcon } from './icons'; 13 | 14 | type MainWidgetProps = { 15 | documents: IDocumentManager; 16 | }; 17 | 18 | type NotebookModelCache = { 19 | path: NotebookPath; 20 | content: INotebookModel; 21 | }; 22 | 23 | async function loadNotebooks( 24 | documents: IDocumentManager, 25 | notebooks: NotebookPath[] 26 | ): Promise { 27 | const manager = documents.services.contents; 28 | const promises = notebooks.map(notebook => manager.get(notebook)); 29 | const results = await Promise.all(promises); 30 | return results.map(result => { 31 | return result.content as INotebookModel; 32 | }); 33 | } 34 | 35 | function MainWidget({ documents }: MainWidgetProps) { 36 | const [files, setFiles] = useState([]); 37 | const [notebooks, setNotebooks] = useState([]); 38 | 39 | const filesChanged = useCallback( 40 | (files: NotebookPath[]) => { 41 | setFiles(files); 42 | }, 43 | [documents] 44 | ); 45 | const showError = useCallback((error: any) => { 46 | const errorMessage = error.message || error; 47 | console.error(errorMessage, error); 48 | Notification.error(`Error on notebook diff: ${errorMessage}`, { 49 | autoClose: false 50 | }); 51 | }, []); 52 | const showDiff = useCallback(() => { 53 | loadNotebooks(documents, files) 54 | .then(models => { 55 | console.log('Notebooks loaded', models); 56 | setNotebooks( 57 | models.map((model, index) => ({ path: files[index], content: model })) 58 | ); 59 | }) 60 | .catch(reason => { 61 | showError(reason); 62 | }); 63 | }, [documents, files]); 64 | 65 | return ( 66 | 67 | 68 | 69 | 77 | {notebooks && notebooks.length > 0 && ( 78 | 79 | )} 80 | 81 | 82 | ); 83 | } 84 | 85 | export function buildWidget( 86 | documents: IDocumentManager, 87 | forNotebook7: boolean 88 | ): ReactWidget { 89 | const widget = ReactWidget.create(); 90 | widget.id = 'lc_notebook_diff::main'; 91 | widget.title.icon = diffIcon; 92 | widget.title.caption = 'Diff'; 93 | if (forNotebook7) { 94 | widget.title.label = 'Diff'; 95 | } 96 | widget.addClass('jupyter-notebook-diff-main'); 97 | return widget; 98 | } 99 | -------------------------------------------------------------------------------- /components/src/Notebook.ts: -------------------------------------------------------------------------------- 1 | import { Cell } from './Cell'; 2 | 3 | export class Notebook { 4 | /** ファイル名 */ 5 | filename: string; 6 | 7 | /** meme */ 8 | meme: string; 9 | 10 | /** セル */ 11 | cellList: Cell[]; 12 | 13 | /** memeからセルへの連想配列 */ 14 | cellMap: { [key: string]: Cell[] }; 15 | 16 | /** JQueryノード */ 17 | $view: JQuery; 18 | 19 | /** 初期化する */ 20 | constructor($: JQueryStatic, filename: string, data: any) { 21 | // ファイル名 22 | this.filename = filename; 23 | 24 | // Cellの配列を初期化 25 | this.cellList = []; 26 | for (let i = 0; i < data['cells'].length; i++) { 27 | this.cellList.push(new Cell($, this, data['cells'][i])); 28 | } 29 | 30 | // memeからセルへの連想配列を初期化 31 | this.cellMap = {}; 32 | for (let i = 0; i < this.cellList.length; i++) { 33 | const meme = this.cellList[i].meme; 34 | if (this.cellMap[meme] === undefined) { 35 | this.cellMap[meme] = []; 36 | } 37 | this.cellMap[meme].push(this.cellList[i]); 38 | } 39 | 40 | // 現在のmeme 41 | this.meme = data['metadata']['lc_notebook_meme']['current']; 42 | 43 | // 描画 44 | this.$view = $(this.html()); 45 | for (let i = 0; i < this.cellList.length; i++) { 46 | this.$view.append(this.cellList[i].$view); 47 | } 48 | } 49 | 50 | /** HTMLを生成する */ 51 | private html(): string { 52 | let html = ''; 53 | html += 54 | '
'; 57 | html += '
' + this.filename + '
'; 58 | html += '
'; 59 | return html; 60 | } 61 | 62 | /** memeを指定してセルを取得する */ 63 | getCellsByMeme(meme: string): Cell[] { 64 | const cells = this.cellMap[meme]; 65 | return cells === undefined ? [] : cells; 66 | } 67 | 68 | /** memeを指定してセルを取得する */ 69 | getCellByMeme(meme: string): Cell | null { 70 | return this.getCellsByMeme(meme)[0]; 71 | } 72 | 73 | /** セルを取得する */ 74 | getCellAt(index: number): Cell { 75 | return this.cellList[index]; 76 | } 77 | 78 | /** セルを非選択にする */ 79 | unselectAll(): void { 80 | for (const cell of this.cellList) { 81 | cell.select(false); 82 | } 83 | } 84 | 85 | /** 選択中のセルのmemeを取得する */ 86 | selectedMeme(): string | null { 87 | const cell = this.selectedCell(); 88 | return cell === null ? null : cell.meme; 89 | } 90 | 91 | /** memeを指定して最初のcellを選択する */ 92 | selectByMeme(meme: string): void { 93 | const cell = this.getCellByMeme(meme); 94 | if (cell !== null) { 95 | cell.select(true); 96 | } 97 | } 98 | 99 | /** 選択中のセルを取得する */ 100 | selectedCell(): Cell | null { 101 | for (const cell of this.cellList) { 102 | if (cell.selected) { 103 | return cell; 104 | } 105 | } 106 | return null; 107 | } 108 | 109 | /** memeを指定してマークする */ 110 | markByMeme(meme: string): void { 111 | for (const cell of this.cellList) { 112 | cell.mark(cell.meme === meme); 113 | } 114 | } 115 | 116 | /** すべてのセルをアンマークする */ 117 | unmarkAll(): void { 118 | for (const cell of this.cellList) { 119 | cell.mark(false); 120 | } 121 | } 122 | 123 | /** セルの数を取得する */ 124 | get count(): number { 125 | return this.cellList.length; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /ui-tests/tests/lc_notebook_diff.spec.ts: -------------------------------------------------------------------------------- 1 | import { IJupyterLabPage, expect, test } from '@jupyterlab/galata'; 2 | import { Page, test as treeTest } from '@playwright/test'; 3 | 4 | // https://github.com/jupyterlab/jupyterlab/blob/9844a6fdb680aeae28a4d6238433f751ce5a6204/galata/src/fixtures.ts#L319-L336 5 | async function waitForLabApplication({ baseURL }, use) { 6 | const waitIsReady = async ( 7 | page: Page, 8 | helpers: IJupyterLabPage 9 | ): Promise => { 10 | await page.waitForSelector('#jupyterlab-splash', { 11 | state: 'detached' 12 | }); 13 | //! not wait for launcher tab. 14 | }; 15 | await use(waitIsReady); 16 | } 17 | 18 | async function waitForTreeApplication(page: Page) { 19 | await page.waitForSelector('.jp-FileBrowser-Panel', { 20 | state: 'visible' 21 | }); 22 | } 23 | 24 | /** 25 | * Don't load JupyterLab webpage before running the tests. 26 | * This is required to ensure we capture all log messages. 27 | */ 28 | test.use({ 29 | autoGoto: false, 30 | waitForApplication: waitForLabApplication 31 | }); 32 | test('should emit an activation console message on JupyterLab', async ({ 33 | page 34 | }) => { 35 | const logs: string[] = []; 36 | 37 | page.on('console', message => { 38 | logs.push(message.text()); 39 | }); 40 | 41 | await page.goto(); 42 | 43 | expect( 44 | logs.filter( 45 | s => s === 'JupyterLab extension lc_notebook_diff is activated!' 46 | ) 47 | ).toHaveLength(1); 48 | }); 49 | 50 | treeTest( 51 | 'should emit an activation console message on Jupyter Notebook 7', 52 | async ({ page }) => { 53 | const logs: string[] = []; 54 | 55 | page.on('console', message => { 56 | logs.push(message.text()); 57 | }); 58 | 59 | await page.goto('http://localhost:8888/tree'); 60 | await waitForTreeApplication(page); 61 | 62 | expect( 63 | logs.filter( 64 | s => s === 'JupyterLab extension lc_notebook_diff is activated!' 65 | ) 66 | ).toHaveLength(1); 67 | } 68 | ); 69 | 70 | test.use({ 71 | autoGoto: true, 72 | waitForApplication: waitForLabApplication 73 | }); 74 | test('should show the diff button on JupyterLab', async ({ page }) => { 75 | await page.waitForSelector('[data-id="lc_notebook_diff::main"]'); 76 | const button = await page.$('[data-id="lc_notebook_diff::main"]'); 77 | expect(button).toBeTruthy(); 78 | await button?.click(); 79 | 80 | const showDiffButton = await page.waitForSelector( 81 | 'button.jupyter-notebook-diff-show-diff' 82 | ); 83 | await showDiffButton?.waitForElementState('visible'); 84 | expect(showDiffButton).toBeTruthy(); 85 | }); 86 | 87 | treeTest( 88 | 'should show the diff button on Jupyter Notebook 7', 89 | async ({ page }) => { 90 | await page.goto('http://localhost:8888/tree'); 91 | await waitForTreeApplication(page); 92 | 93 | await page.waitForSelector('[data-icon="notebook_diff::notebookdiff"]'); 94 | const button = await page.$('[data-icon="notebook_diff::notebookdiff"]'); 95 | expect(button).toBeTruthy(); 96 | await button?.click(); 97 | 98 | const showDiffButton = await page.waitForSelector( 99 | 'button.jupyter-notebook-diff-show-diff' 100 | ); 101 | await showDiffButton?.waitForElementState('visible'); 102 | expect(showDiffButton).toBeTruthy(); 103 | } 104 | ); 105 | -------------------------------------------------------------------------------- /.github/workflows/e2e-tests.yml: -------------------------------------------------------------------------------- 1 | name: E2E Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | pull_request: 8 | branches: 9 | - '**' 10 | workflow_dispatch: 11 | 12 | jobs: 13 | notebook-e2e: 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.11' 24 | 25 | - name: Cache pip dependencies 26 | uses: actions/cache@v4 27 | with: 28 | path: ~/.cache/pip 29 | key: ${{ runner.os }}-pip-${{ hashFiles('ui-tests/e2e-notebook/requirements.txt') }} 30 | restore-keys: | 31 | ${{ runner.os }}-pip- 32 | 33 | - name: Install Python dependencies 34 | run: | 35 | set -euxo pipefail 36 | pip install --upgrade pip 37 | pip install -r ui-tests/e2e-notebook/requirements.txt 38 | 39 | - name: Install Playwright browsers 40 | run: | 41 | set -euxo pipefail 42 | python -m playwright install --with-deps 43 | 44 | - name: Build Docker image 45 | run: | 46 | set -euxo pipefail 47 | docker build -t lc_notebook_diff:test . 48 | 49 | - name: Prepare test workspace 50 | run: | 51 | set -euxo pipefail 52 | mkdir -p ui-tests/e2e-notebook/artifacts/jupyter-work 53 | cp -r html/* ui-tests/e2e-notebook/artifacts/jupyter-work/ 54 | chmod -R a+w ui-tests/e2e-notebook/artifacts/jupyter-work 55 | 56 | - name: Start lc_notebook_diff container 57 | run: | 58 | set -euxo pipefail 59 | docker run -d \ 60 | --name lc-notebook-diff-test \ 61 | -p 8888:8888 \ 62 | -v $(pwd)/ui-tests/e2e-notebook/artifacts/jupyter-work:/home/jovyan/work \ 63 | lc_notebook_diff:test \ 64 | start-notebook.sh --ServerApp.token='test-token' --ServerApp.allow_origin='*' --ServerApp.root_dir='/home/jovyan/work' 65 | sleep 30 66 | # Health check 67 | curl --retry 10 --retry-delay 5 --retry-connrefused --fail http://localhost:8888/lab?token=test-token || (docker logs lc-notebook-diff-test && exit 1) 68 | 69 | - name: Run notebook E2E tests 70 | env: 71 | E2E_TRANSITION_TIMEOUT: '60000' 72 | E2E_DEFAULT_DELAY: '200' 73 | JUPYTERLAB_URL: 'http://localhost:8888/lab?token=test-token' 74 | NOTEBOOK7_URL: 'http://localhost:8888/tree?token=test-token' 75 | NBCLASSIC_URL: 'http://localhost:8888/nbclassic/tree?token=test-token' 76 | JUPYTER_WORK_DIR: ${{ github.workspace }}/ui-tests/e2e-notebook/artifacts/jupyter-work 77 | run: | 78 | set -euxo pipefail 79 | python ui-tests/e2e-notebook/run_notebooks.py --skip-failed-test 80 | 81 | - name: Gather Docker logs 82 | if: always() 83 | run: | 84 | set -euxo pipefail 85 | mkdir -p ui-tests/e2e-notebook/artifacts 86 | docker logs lc-notebook-diff-test > ui-tests/e2e-notebook/artifacts/lc-notebook-diff-container.log 2>&1 || true 87 | 88 | - name: Upload artifacts 89 | if: always() 90 | uses: actions/upload-artifact@v4 91 | with: 92 | name: e2e-results 93 | path: | 94 | ui-tests/e2e-notebook/artifacts 95 | 96 | - name: Shutdown container 97 | if: always() 98 | run: | 99 | set -euxo pipefail 100 | docker stop lc-notebook-diff-test || true 101 | docker rm lc-notebook-diff-test || true 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jupyter-LC\_notebook\_diff [![Github Actions Status](https://github.com/NII-cloud-operation/Jupyter-LC_notebook_diff/workflows/Build/badge.svg)](https://github.com/NII-cloud-operation/Jupyter-LC_notebook_diff/actions/workflows/build.yml) [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/NII-cloud-operation/Jupyter-LC_notebook_diff/master) 2 | 3 | Jupyter-LC\_notebook\_diff is an extension that compares two or three Jupyter notebooks. 4 | 5 | 6 | ## Prerequisite 7 | 8 | - Jupyter Notebook 6.5.x (nbclassic) or 7.x 9 | 10 | ## Install 11 | 12 | To install the extension, download the tar.gz file from the [latest release](https://github.com/NII-cloud-operation/Jupyter-LC_notebook_diff/releases) and execute: 13 | 14 | ```bash 15 | $ pip install lc_notebook_diff-*.tar.gz 16 | ``` 17 | 18 | For nbclassic users, you will also need to install and enable the extension: 19 | 20 | ```bash 21 | $ jupyter nbclassic-extension install --py lc_notebook_diff --sys-prefix 22 | $ jupyter nbclassic-extension enable --py lc_notebook_diff --sys-prefix 23 | ``` 24 | 25 | then restart Jupyter notebook. 26 | 27 | ## Uninstall 28 | 29 | To remove the extension, execute: 30 | 31 | ```bash 32 | pip uninstall lc_notebook_diff 33 | ``` 34 | 35 | ## Contributing 36 | 37 | ### Development install 38 | 39 | Note: You will need NodeJS to build the extension package. 40 | 41 | The `jlpm` command is JupyterLab's pinned version of 42 | [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use 43 | `yarn` or `npm` in lieu of `jlpm` below. 44 | 45 | ```bash 46 | jlpm prepare 47 | # Clone the repo to your local environment 48 | # Change directory to the lc_notebook_diff directory 49 | # Install package in development mode 50 | pip install -e "." 51 | # Link your development version of the extension with JupyterLab 52 | jupyter labextension develop . --overwrite 53 | # Rebuild extension Typescript source after making changes 54 | jlpm build 55 | ``` 56 | 57 | You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension. 58 | 59 | ```bash 60 | # Watch the source directory in one terminal, automatically rebuilding when needed 61 | jlpm watch 62 | # Run JupyterLab in another terminal 63 | jupyter lab 64 | ``` 65 | 66 | With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt). 67 | 68 | By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command: 69 | 70 | ```bash 71 | jupyter lab build --minimize=False 72 | ``` 73 | 74 | ### Development uninstall 75 | 76 | ```bash 77 | pip uninstall lc_notebook_diff 78 | ``` 79 | 80 | In development mode, you will also need to remove the symlink created by `jupyter labextension develop` 81 | command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions` 82 | folder is located. Then you can remove the symlink named `lc_notebook_diff` within that folder. 83 | 84 | ### Testing the extension 85 | 86 | #### Frontend tests 87 | 88 | This extension is using [Jest](https://jestjs.io/) for JavaScript code testing. 89 | 90 | To execute them, execute: 91 | 92 | ```sh 93 | jlpm 94 | jlpm test 95 | ``` 96 | 97 | #### Integration tests 98 | 99 | This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests). 100 | More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab. 101 | 102 | More information are provided within the [ui-tests](./ui-tests/README.md) README. 103 | 104 | ### Packaging the extension 105 | 106 | See [RELEASE](RELEASE.md) 107 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Making a new release of lc_notebook_diff 2 | 3 | The extension can be published to `PyPI` and `npm` manually or using the [Jupyter Releaser](https://github.com/jupyter-server/jupyter_releaser). 4 | 5 | ## Manual release 6 | 7 | ### Python package 8 | 9 | This extension can be distributed as Python packages. All of the Python 10 | packaging instructions are in the `pyproject.toml` file to wrap your extension in a 11 | Python package. Before generating a package, you first need to install some tools: 12 | 13 | ```bash 14 | pip install build twine hatch 15 | ``` 16 | 17 | Bump the version using `hatch`. By default this will create a tag. 18 | See the docs on [hatch-nodejs-version](https://github.com/agoose77/hatch-nodejs-version#semver) for details. 19 | 20 | ```bash 21 | hatch version 22 | ``` 23 | 24 | Make sure to clean up all the development files before building the package: 25 | 26 | ```bash 27 | jlpm clean:all 28 | ``` 29 | 30 | You could also clean up the local git repository: 31 | 32 | ```bash 33 | git clean -dfX 34 | ``` 35 | 36 | To create a Python source package (`.tar.gz`) and the binary package (`.whl`) in the `dist/` directory, do: 37 | 38 | ```bash 39 | python -m build 40 | ``` 41 | 42 | > `python setup.py sdist bdist_wheel` is deprecated and will not work for this package. 43 | 44 | Then to upload the package to PyPI, do: 45 | 46 | ```bash 47 | twine upload dist/* 48 | ``` 49 | 50 | ### NPM package 51 | 52 | To publish the frontend part of the extension as a NPM package, do: 53 | 54 | ```bash 55 | npm login 56 | npm publish --access public 57 | ``` 58 | 59 | ## Automated releases with the Jupyter Releaser 60 | 61 | The extension repository should already be compatible with the Jupyter Releaser. 62 | 63 | Check out the [workflow documentation](https://jupyter-releaser.readthedocs.io/en/latest/get_started/making_release_from_repo.html) for more information. 64 | 65 | Here is a summary of the steps to cut a new release: 66 | 67 | - Add tokens to the [Github Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) in the repository: 68 | - `ADMIN_GITHUB_TOKEN` (with "public_repo" and "repo:status" permissions); see the [documentation](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token) 69 | - `NPM_TOKEN` (with "automation" permission); see the [documentation](https://docs.npmjs.com/creating-and-viewing-access-tokens) 70 | - Set up PyPI 71 | 72 |
Using PyPI trusted publisher (modern way) 73 | 74 | - Set up your PyPI project by [adding a trusted publisher](https://docs.pypi.org/trusted-publishers/adding-a-publisher/) 75 | - The _workflow name_ is `publish-release.yml` and the _environment_ should be left blank. 76 | - Ensure the publish release job as `permissions`: `id-token : write` (see the [documentation](https://docs.pypi.org/trusted-publishers/using-a-publisher/)) 77 | 78 |
79 | 80 |
Using PyPI token (legacy way) 81 | 82 | - If the repo generates PyPI release(s), create a scoped PyPI [token](https://packaging.python.org/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/#saving-credentials-on-github). We recommend using a scoped token for security reasons. 83 | 84 | - You can store the token as `PYPI_TOKEN` in your fork's `Secrets`. 85 | 86 | - Advanced usage: if you are releasing multiple repos, you can create a secret named `PYPI_TOKEN_MAP` instead of `PYPI_TOKEN` that is formatted as follows: 87 | 88 | ```text 89 | owner1/repo1,token1 90 | owner2/repo2,token2 91 | ``` 92 | 93 | If you have multiple Python packages in the same repository, you can point to them as follows: 94 | 95 | ```text 96 | owner1/repo1/path/to/package1,token1 97 | owner1/repo1/path/to/package2,token2 98 | ``` 99 | 100 |
101 | 102 | - Go to the Actions panel 103 | - Run the "Step 1: Prep Release" workflow 104 | - Check the draft changelog 105 | - Run the "Step 2: Publish Release" workflow 106 | 107 | ## Publishing to `conda-forge` 108 | 109 | If the package is not on conda forge yet, check the documentation to learn how to add it: https://conda-forge.org/docs/maintainer/adding_pkgs.html 110 | 111 | Otherwise a bot should pick up the new version publish to PyPI, and open a new PR on the feedstock repository automatically. 112 | -------------------------------------------------------------------------------- /lc_notebook_diff/nbextension/jupyter-notebook-diff.css: -------------------------------------------------------------------------------- 1 | .jupyter-notebook-diff > .wrapper { 2 | display: table; 3 | table-layout: fixed; 4 | width: 100%; 5 | box-sizing: border-box; 6 | border-collapse: collapse; 7 | font-family: Consolas, 'Courier New', Courier, Monaco, monospace; 8 | font-size: 11px; 9 | } 10 | .jupyter-notebook-diff > .wrapper > .notebook { 11 | display: table-cell; 12 | vertical-align: top; 13 | position: relative; 14 | } 15 | .jupyter-notebook-diff > .wrapper > .relation { 16 | display: table-cell; 17 | vertical-align: top; 18 | width: 50px; 19 | } 20 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(1) > .title, 21 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(1) > .cell > .meme { 22 | background-color: #ebebff; 23 | } 24 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(3) > .title, 25 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(3) > .cell > .meme { 26 | background-color: #d8ffd8; 27 | } 28 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(5) > .title, 29 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(5) > .cell > .meme { 30 | background-color: #ffffdc; 31 | } 32 | .jupyter-notebook-diff > .wrapper > .notebook > .title { 33 | margin: 0 0px 2px 0px; 34 | padding: 5px; 35 | } 36 | .jupyter-notebook-diff > .wrapper > .notebook > .cell { 37 | } 38 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme { 39 | margin: 5px 0px 0 0px; 40 | padding: 5px; 41 | border: 1px solid #ddd; 42 | border-radius: 3px 3px 0 0; 43 | } 44 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .source { 45 | margin: -1px 0px 5px 0px; 46 | padding: 5px; 47 | border: 1px solid #ddd; 48 | border-radius: 0 0 3px 3px; 49 | overflow-x: hidden; 50 | overflow-y: hidden; 51 | } 52 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .open-button, 53 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .close-button, 54 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button { 55 | display: none; 56 | background-color: #fff; 57 | border: 1px solid #ccc; 58 | color: #ccc; 59 | text-align: center; 60 | width: 12px; 61 | margin-right: 5px; 62 | cursor: pointer; 63 | } 64 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button { 65 | display: inline-block; 66 | float: right; 67 | color: red; 68 | } 69 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button.marked { 70 | border: 2px solid #f00; 71 | } 72 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button > span { 73 | display: none; 74 | } 75 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button.selected > span { 76 | display: inline; 77 | } 78 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .close-button { 79 | display: inline-block; 80 | } 81 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.closed > .meme > .open-button { 82 | display: inline-block; 83 | } 84 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.closed > .meme > .close-button { 85 | display: none; 86 | } 87 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.closed > .source { 88 | max-height: 100px; 89 | } 90 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.highlight > .source { 91 | background-color: #ffffee; 92 | } 93 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.changed1 > .source { 94 | background-color: #ffd699; 95 | } 96 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.changed1.highlight > .source { 97 | background-color: #ff9900; 98 | } 99 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.changed2 > .source { 100 | background-color: #ffffcc; 101 | } 102 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.changed2.highlight > .source { 103 | background-color: #ffff33; 104 | } 105 | .jupyter-notebook-diff > .dark { 106 | display: none; 107 | position: fixed; 108 | top: 0; 109 | left: 0; 110 | width: 100%; 111 | height: 100%; 112 | background-color: #000; 113 | opacity: 0.5; 114 | } 115 | .jupyter-notebook-diff > .merge-view { 116 | display: none; 117 | position: fixed; 118 | top: 10%; 119 | left: 10%; 120 | width: 80%; 121 | } 122 | 123 | .CodeMirror-merge, .CodeMirror-merge .CodeMirror { 124 | height: 600px; 125 | } 126 | 127 | .jupyter-notebook-diff .merge-view .CodeMirror-merge-pane { 128 | background-color: white; 129 | } 130 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: ['*'] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Install node 15 | uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | - name: Install Python 19 | uses: actions/setup-python@v2 20 | with: 21 | python-version: '3.9' 22 | architecture: 'x64' 23 | 24 | 25 | - name: Setup pip cache 26 | uses: actions/cache@v2 27 | with: 28 | path: ~/.cache/pip 29 | key: pip-3.9-${{ hashFiles('package.json') }} 30 | restore-keys: | 31 | pip-3.9- 32 | pip- 33 | 34 | - name: Get yarn cache directory path 35 | id: yarn-cache-dir-path 36 | run: echo "::set-output name=dir::$(yarn cache dir)" 37 | - name: Setup yarn cache 38 | uses: actions/cache@v2 39 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 40 | with: 41 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 42 | key: yarn-${{ hashFiles('**/yarn.lock') }} 43 | restore-keys: | 44 | yarn- 45 | 46 | - name: Install dependencies 47 | run: python -m pip install -U 'jupyterlab>=4.0.0,<5' && python -m pip install -U 'notebook>=7,<8' 48 | - name: Build the extension 49 | run: | 50 | set -eux 51 | jlpm 52 | jlpm run eslint:check 53 | cd ./components 54 | npm install 55 | npm run build 56 | cd .. 57 | cd ./nbextension 58 | npm install 59 | npm run build 60 | cd .. 61 | python -m pip install . 62 | 63 | jupyter labextension list 2>&1 | grep -ie "lc_notebook_diff.*OK" 64 | python -m jupyterlab.browser_check 65 | 66 | jlpm install 67 | cd ./ui-tests 68 | jlpm install 69 | jlpm playwright install 70 | jlpm playwright test 71 | cd .. 72 | 73 | pip install build 74 | python -m build --sdist 75 | cp dist/*.tar.gz myextension.tar.gz 76 | pip uninstall -y myextension jupyterlab 77 | rm -rf myextension 78 | 79 | npm pack 80 | mv lc_notebook_diff-*.tgz myextension-nodejs.tgz 81 | 82 | - uses: actions/upload-artifact@v4 83 | if: ${{ !cancelled() }} 84 | with: 85 | name: playwright-report 86 | path: ui-tests/playwright-report/ 87 | retention-days: 30 88 | 89 | - uses: actions/upload-artifact@v4 90 | with: 91 | name: myextension-sdist 92 | path: myextension.tar.gz 93 | 94 | - uses: actions/upload-artifact@v4 95 | with: 96 | name: myextension-nodejs 97 | path: myextension-nodejs.tgz 98 | 99 | draft_release: 100 | needs: build 101 | runs-on: ubuntu-latest 102 | 103 | steps: 104 | - name: Checkout 105 | uses: actions/checkout@v2 106 | - uses: actions/download-artifact@v4 107 | with: 108 | name: myextension-sdist 109 | - uses: actions/download-artifact@v4 110 | with: 111 | name: myextension-nodejs 112 | - name: release 113 | uses: actions/create-release@v1 114 | id: create_release 115 | with: 116 | draft: true 117 | prerelease: false 118 | release_name: ${{ github.ref }} 119 | tag_name: ${{ github.ref }} 120 | body_path: CHANGELOG.md 121 | env: 122 | GITHUB_TOKEN: ${{ github.token }} 123 | - name: upload pip package 124 | uses: actions/upload-release-asset@v1 125 | env: 126 | GITHUB_TOKEN: ${{ github.token }} 127 | with: 128 | upload_url: ${{ steps.create_release.outputs.upload_url }} 129 | asset_path: myextension.tar.gz 130 | asset_name: lc_notebook_diff-${{ github.ref_name }}.tar.gz 131 | asset_content_type: application/gzip 132 | - name: upload nodejs package 133 | uses: actions/upload-release-asset@v1 134 | env: 135 | GITHUB_TOKEN: ${{ github.token }} 136 | with: 137 | upload_url: ${{ steps.create_release.outputs.upload_url }} 138 | asset_path: myextension-nodejs.tgz 139 | asset_name: lc_notebook_diff-${{ github.ref_name }}.tgz 140 | asset_content_type: application/gzip 141 | -------------------------------------------------------------------------------- /ui-tests/README.md: -------------------------------------------------------------------------------- 1 | # Integration Testing 2 | 3 | This folder contains the integration tests of the extension. 4 | 5 | They are defined using [Playwright](https://playwright.dev/docs/intro) test runner 6 | and [Galata](https://github.com/jupyterlab/jupyterlab/tree/main/galata) helper. 7 | 8 | The Playwright configuration is defined in [playwright.config.js](./playwright.config.js). 9 | 10 | The JupyterLab server configuration to use for the integration test is defined 11 | in [jupyter_server_test_config.py](./jupyter_server_test_config.py). 12 | 13 | The default configuration will produce video for failing tests and an HTML report. 14 | 15 | > There is a new experimental UI mode that you may fall in love with; see [that video](https://www.youtube.com/watch?v=jF0yA-JLQW0). 16 | 17 | ## Run the tests 18 | 19 | > All commands are assumed to be executed from the root directory 20 | 21 | To run the tests, you need to: 22 | 23 | 1. Compile the extension: 24 | 25 | ```sh 26 | jlpm install 27 | jlpm build:prod 28 | ``` 29 | 30 | > Check the extension is installed in JupyterLab. 31 | 32 | 2. Install test dependencies (needed only once): 33 | 34 | ```sh 35 | cd ./ui-tests 36 | jlpm install 37 | jlpm playwright install 38 | cd .. 39 | ``` 40 | 41 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) tests: 42 | 43 | ```sh 44 | cd ./ui-tests 45 | jlpm playwright test 46 | ``` 47 | 48 | Test results will be shown in the terminal. In case of any test failures, the test report 49 | will be opened in your browser at the end of the tests execution; see 50 | [Playwright documentation](https://playwright.dev/docs/test-reporters#html-reporter) 51 | for configuring that behavior. 52 | 53 | ## Update the tests snapshots 54 | 55 | > All commands are assumed to be executed from the root directory 56 | 57 | If you are comparing snapshots to validate your tests, you may need to update 58 | the reference snapshots stored in the repository. To do that, you need to: 59 | 60 | 1. Compile the extension: 61 | 62 | ```sh 63 | jlpm install 64 | jlpm build:prod 65 | ``` 66 | 67 | > Check the extension is installed in JupyterLab. 68 | 69 | 2. Install test dependencies (needed only once): 70 | 71 | ```sh 72 | cd ./ui-tests 73 | jlpm install 74 | jlpm playwright install 75 | cd .. 76 | ``` 77 | 78 | 3. Execute the [Playwright](https://playwright.dev/docs/intro) command: 79 | 80 | ```sh 81 | cd ./ui-tests 82 | jlpm playwright test -u 83 | ``` 84 | 85 | > Some discrepancy may occurs between the snapshots generated on your computer and 86 | > the one generated on the CI. To ease updating the snapshots on a PR, you can 87 | > type `please update playwright snapshots` to trigger the update by a bot on the CI. 88 | > Once the bot has computed new snapshots, it will commit them to the PR branch. 89 | 90 | ## Create tests 91 | 92 | > All commands are assumed to be executed from the root directory 93 | 94 | To create tests, the easiest way is to use the code generator tool of playwright: 95 | 96 | 1. Compile the extension: 97 | 98 | ```sh 99 | jlpm install 100 | jlpm build:prod 101 | ``` 102 | 103 | > Check the extension is installed in JupyterLab. 104 | 105 | 2. Install test dependencies (needed only once): 106 | 107 | ```sh 108 | cd ./ui-tests 109 | jlpm install 110 | jlpm playwright install 111 | cd .. 112 | ``` 113 | 114 | 3. Start the server: 115 | 116 | ```sh 117 | cd ./ui-tests 118 | jlpm start 119 | ``` 120 | 121 | 4. Execute the [Playwright code generator](https://playwright.dev/docs/codegen) in **another terminal**: 122 | 123 | ```sh 124 | cd ./ui-tests 125 | jlpm playwright codegen localhost:8888 126 | ``` 127 | 128 | ## Debug tests 129 | 130 | > All commands are assumed to be executed from the root directory 131 | 132 | To debug tests, a good way is to use the inspector tool of playwright: 133 | 134 | 1. Compile the extension: 135 | 136 | ```sh 137 | jlpm install 138 | jlpm build:prod 139 | ``` 140 | 141 | > Check the extension is installed in JupyterLab. 142 | 143 | 2. Install test dependencies (needed only once): 144 | 145 | ```sh 146 | cd ./ui-tests 147 | jlpm install 148 | jlpm playwright install 149 | cd .. 150 | ``` 151 | 152 | 3. Execute the Playwright tests in [debug mode](https://playwright.dev/docs/debug): 153 | 154 | ```sh 155 | cd ./ui-tests 156 | jlpm playwright test --debug 157 | ``` 158 | 159 | ## Upgrade Playwright and the browsers 160 | 161 | To update the web browser versions, you must update the package `@playwright/test`: 162 | 163 | ```sh 164 | cd ./ui-tests 165 | jlpm up "@playwright/test" 166 | jlpm playwright install 167 | ``` 168 | -------------------------------------------------------------------------------- /ui-tests/e2e-notebook/run_notebooks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | """Execute notebook-based E2E scenarios with Papermill.""" 3 | 4 | from __future__ import annotations 5 | 6 | import sys 7 | from pathlib import Path 8 | import argparse 9 | import os 10 | 11 | import papermill as pm 12 | from papermill.exceptions import PapermillExecutionError 13 | 14 | NOTEBOOK_ROOT = Path(__file__).resolve().parent / "notebooks" 15 | ARTIFACT_ROOT = Path(__file__).resolve().parent / "artifacts" 16 | RESULT_ROOT = ARTIFACT_ROOT / "notebooks" 17 | 18 | 19 | def parse_args() -> argparse.Namespace: 20 | parser = argparse.ArgumentParser(description="Execute notebook E2E tests.") 21 | parser.add_argument( 22 | "--skip-failed-test", 23 | dest="skip_failed_test", 24 | action="store_true", 25 | help="Continue executing remaining notebooks even if a notebook fails.", 26 | ) 27 | return parser.parse_args() 28 | 29 | 30 | def main() -> int: 31 | args = parse_args() 32 | if not NOTEBOOK_ROOT.exists(): 33 | raise FileNotFoundError(f"Notebook directory not found: {NOTEBOOK_ROOT}") 34 | 35 | notebooks = sorted(NOTEBOOK_ROOT.glob("*.ipynb")) 36 | 37 | transition_timeout_env = os.getenv("E2E_TRANSITION_TIMEOUT") 38 | if transition_timeout_env: 39 | try: 40 | transition_timeout = int(transition_timeout_env) 41 | except ValueError as exc: 42 | raise ValueError("E2E_TRANSITION_TIMEOUT must be an integer") from exc 43 | else: 44 | transition_timeout = None 45 | 46 | default_delay_env = os.getenv("E2E_DEFAULT_DELAY") 47 | if default_delay_env: 48 | try: 49 | default_delay = int(default_delay_env) 50 | except ValueError as exc: 51 | raise ValueError("E2E_DEFAULT_DELAY must be an integer") from exc 52 | else: 53 | default_delay = None 54 | 55 | ARTIFACT_ROOT.mkdir(parents=True, exist_ok=True) 56 | RESULT_ROOT.mkdir(parents=True, exist_ok=True) 57 | 58 | if not notebooks: 59 | print("No notebooks to execute. Add notebooks under ui-tests/e2e-notebook/notebooks.") 60 | return 0 61 | 62 | failures: list[tuple[Path, str]] = [] 63 | browser_types = ["chromium", "firefox"] 64 | 65 | for notebook in notebooks: 66 | notebook_dir = notebook.parent 67 | 68 | for browser_type in browser_types: 69 | result_notebook = RESULT_ROOT / f"{notebook.stem}-{browser_type}-result.ipynb" 70 | notebook_artifact_dir = RESULT_ROOT / f"{notebook.stem}_{browser_type}" 71 | notebook_artifact_dir.mkdir(parents=True, exist_ok=True) 72 | 73 | print(f"Running notebook: {notebook} ({browser_type}) -> {result_notebook}") 74 | parameters = { 75 | "default_result_path": str(notebook_artifact_dir), 76 | "browser_type": browser_type, 77 | } 78 | if transition_timeout is not None: 79 | parameters["transition_timeout"] = transition_timeout 80 | if default_delay is not None: 81 | parameters["default_delay"] = default_delay 82 | 83 | jupyterlab_url = os.getenv("JUPYTERLAB_URL") 84 | if jupyterlab_url: 85 | parameters["jupyterlab_url"] = jupyterlab_url 86 | 87 | notebook7_url = os.getenv("NOTEBOOK7_URL") 88 | if notebook7_url: 89 | parameters["notebook7_url"] = notebook7_url 90 | 91 | nbclassic_url = os.getenv("NBCLASSIC_URL") 92 | if nbclassic_url: 93 | parameters["nbclassic_url"] = nbclassic_url 94 | 95 | jupyter_work_dir = os.getenv("JUPYTER_WORK_DIR") 96 | if jupyter_work_dir: 97 | parameters["jupyter_work_dir"] = jupyter_work_dir 98 | 99 | try: 100 | pm.execute_notebook( 101 | str(notebook), 102 | str(result_notebook), 103 | parameters=parameters, 104 | cwd=str(notebook_dir), 105 | ) 106 | except PapermillExecutionError as err: 107 | failures.append((notebook, browser_type)) 108 | if not args.skip_failed_test: 109 | raise 110 | print(f"Notebook failed but continuing: {notebook} ({browser_type}) (reason: {err})") 111 | 112 | if failures: 113 | print("Failed notebooks:") 114 | for notebook, browser_type in failures: 115 | print(f" - {notebook} ({browser_type})") 116 | return 1 117 | 118 | return 0 119 | 120 | 121 | if __name__ == "__main__": 122 | sys.exit(main()) 123 | -------------------------------------------------------------------------------- /style/base.css: -------------------------------------------------------------------------------- 1 | .jupyter-notebook-diff-main { 2 | overflow: auto !important; 3 | padding: 1em; 4 | } 5 | 6 | .jupyter-notebook-diff-main button { 7 | font-size: 13px; 8 | } 9 | 10 | .jupyter-notebook-diff-files svg { 11 | width: 20px; 12 | height: 20px; 13 | } 14 | 15 | .jupyter-notebook-diff-file { 16 | margin: 0.5em; 17 | } 18 | 19 | .jupyter-notebook-diff-file button { 20 | width: 24px; 21 | } 22 | 23 | .jupyter-notebook-diff-file input { 24 | width: 100%; 25 | } 26 | 27 | .jupyter-notebook-diff { 28 | margin: 1em; 29 | } 30 | 31 | .jupyter-notebook-diff > .wrapper { 32 | display: table; 33 | table-layout: fixed; 34 | width: 100%; 35 | box-sizing: border-box; 36 | border-collapse: collapse; 37 | font-family: Consolas, 'Courier New', Courier, Monaco, monospace; 38 | font-size: 11px; 39 | } 40 | .jupyter-notebook-diff > .wrapper > .notebook { 41 | display: table-cell; 42 | vertical-align: top; 43 | position: relative; 44 | } 45 | .jupyter-notebook-diff > .wrapper > .relation { 46 | display: table-cell; 47 | vertical-align: top; 48 | width: 50px; 49 | } 50 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(1) > .title, 51 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(1) > .cell > .meme { 52 | background-color: #ebebff; 53 | } 54 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(3) > .title, 55 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(3) > .cell > .meme { 56 | background-color: #d8ffd8; 57 | } 58 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(5) > .title, 59 | .jupyter-notebook-diff > .wrapper > .notebook:nth-child(5) > .cell > .meme { 60 | background-color: #ffffdc; 61 | } 62 | .jupyter-notebook-diff > .wrapper > .notebook > .title { 63 | margin: 0 0px 2px 0px; 64 | padding: 5px; 65 | } 66 | .jupyter-notebook-diff > .wrapper > .notebook > .cell { 67 | } 68 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme { 69 | margin: 5px 0px 0 0px; 70 | padding: 5px; 71 | border: 1px solid #ddd; 72 | border-radius: 3px 3px 0 0; 73 | } 74 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .source { 75 | margin: -1px 0px 5px 0px; 76 | padding: 5px; 77 | border: 1px solid #ddd; 78 | border-radius: 0 0 3px 3px; 79 | overflow-x: hidden; 80 | overflow-y: hidden; 81 | } 82 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .open-button, 83 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .close-button, 84 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button { 85 | display: none; 86 | background-color: #fff; 87 | border: 1px solid #ccc; 88 | color: #ccc; 89 | text-align: center; 90 | width: 12px; 91 | margin-right: 5px; 92 | cursor: pointer; 93 | } 94 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button { 95 | display: inline-block; 96 | float: right; 97 | color: red; 98 | } 99 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button.marked { 100 | border: 2px solid #f00; 101 | } 102 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button > span { 103 | display: none; 104 | } 105 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .select-button.selected > span { 106 | display: inline; 107 | } 108 | .jupyter-notebook-diff > .wrapper > .notebook > .cell > .meme > .close-button { 109 | display: inline-block; 110 | } 111 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.closed > .meme > .open-button { 112 | display: inline-block; 113 | } 114 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.closed > .meme > .close-button { 115 | display: none; 116 | } 117 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.closed > .source { 118 | max-height: 100px; 119 | } 120 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.highlight > .source { 121 | background-color: #ffffee; 122 | } 123 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.changed1 > .source { 124 | background-color: #ffd699; 125 | } 126 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.changed1.highlight > .source { 127 | background-color: #ff9900; 128 | } 129 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.changed2 > .source { 130 | background-color: #ffffcc; 131 | } 132 | .jupyter-notebook-diff > .wrapper > .notebook > .cell.changed2.highlight > .source { 133 | background-color: #ffff33; 134 | } 135 | .jupyter-notebook-diff > .dark { 136 | display: none; 137 | position: fixed; 138 | top: 0; 139 | left: 0; 140 | width: 100%; 141 | height: 100%; 142 | background-color: #000; 143 | opacity: 0.5; 144 | } 145 | .jupyter-notebook-diff > .merge-view { 146 | display: none; 147 | position: fixed; 148 | top: 10%; 149 | left: 10%; 150 | width: 80%; 151 | } 152 | 153 | .CodeMirror-merge, .CodeMirror-merge .CodeMirror { 154 | height: 600px; 155 | } 156 | 157 | .jupyter-notebook-diff .merge-view .CodeMirror-merge-pane { 158 | background-color: white; 159 | } 160 | -------------------------------------------------------------------------------- /nbextension/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ 5 | "module": "amd", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 6 | // "lib": [], /* Specify library files to be included in the compilation. */ 7 | // "allowJs": true, /* Allow javascript files to be compiled. */ 8 | // "checkJs": true, /* Report errors in .js files. */ 9 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 10 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 11 | "sourceMap": true, /* Generates corresponding '.map' file. */ 12 | // "outFile": "../html/jupyter-notebook-diff.js", /* Concatenate and emit output to single file. */ 13 | "outDir": "./lib", /* Redirect output structure to the directory. */ 14 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 15 | "removeComments": true, /* Do not emit comments to output. */ 16 | // "noEmit": true, /* Do not emit outputs. */ 17 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 18 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 19 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 20 | /* Strict Type-Checking Options */ 21 | "strict": true, /* Enable all strict type-checking options. */ 22 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 23 | // "strictNullChecks": true, /* Enable strict null checks. */ 24 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 25 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 26 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 27 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 28 | /* Additional Checks */ 29 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 30 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 31 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 32 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 33 | /* Module Resolution Options */ 34 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 35 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 36 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 37 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 38 | // "typeRoots": [], /* List of folders to include type definitions from. */ 39 | // "types": [], /* Type declaration files to be included in compilation. */ 40 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 41 | "esModuleInterop": false, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 42 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 43 | /* Source Map Options */ 44 | "sourceRoot": "./src/", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 45 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 46 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 47 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 48 | /* Experimental Options */ 49 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 50 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 51 | } 52 | } -------------------------------------------------------------------------------- /components/src/Relation.ts: -------------------------------------------------------------------------------- 1 | import { Notebook } from './Notebook'; 2 | import { Cell } from './Cell'; 3 | 4 | export enum RelationMatchType { 5 | Exact = 'exact', 6 | Fuzzy = 'fuzzy' 7 | } 8 | 9 | export class Relation { 10 | /** ノートブック(左) */ 11 | notebookLeft: Notebook; 12 | 13 | /** ノートブック(右) */ 14 | notebookRight: Notebook; 15 | 16 | /** JQueryノード */ 17 | $view: JQuery; 18 | 19 | /** Cell IDから左側の関連するCellリストへの連想配列 */ 20 | relatedLeftCells: { [key: string]: Cell[] }; 21 | 22 | /** Cell IDから右側の関連するCellリストへの連想配列 */ 23 | relatedRightCells: { [key: string]: Cell[] }; 24 | 25 | /** Cell IDから隣の関連するCellリストを取得する */ 26 | getRelatedCells(cellId: string): Cell[] { 27 | return (this.relatedLeftCells[cellId] || []).concat( 28 | this.relatedRightCells[cellId] || [] 29 | ); 30 | } 31 | 32 | /** マッチタイプ */ 33 | matchType: RelationMatchType; 34 | 35 | /** 初期化 */ 36 | constructor( 37 | $: JQueryStatic, 38 | notebookLeft: Notebook, 39 | notebookRight: Notebook, 40 | { 41 | matchType = RelationMatchType.Fuzzy 42 | }: { matchType?: RelationMatchType } = {} 43 | ) { 44 | this.notebookLeft = notebookLeft; 45 | this.notebookRight = notebookRight; 46 | this.$view = $('
'); 47 | this.relatedLeftCells = {}; 48 | this.relatedRightCells = {}; 49 | this.matchType = matchType; 50 | } 51 | 52 | /** リレーション構造を更新する */ 53 | updateRelation(): void { 54 | this.relatedLeftCells = {}; 55 | this.relatedRightCells = {}; 56 | 57 | for (const cellLeft of this.notebookLeft.cellList) { 58 | this.relatedRightCells[cellLeft.id] = []; 59 | } 60 | for (const cellRight of this.notebookRight.cellList) { 61 | this.relatedLeftCells[cellRight.id] = []; 62 | } 63 | 64 | if (this.matchType === RelationMatchType.Fuzzy) { 65 | const usedRightCells: { [key: string]: boolean } = {}; 66 | for (const cellLeft of this.notebookLeft.cellList) { 67 | const cellRightList = this.notebookRight 68 | .getCellsByMeme(cellLeft.meme) 69 | .filter(cell => !usedRightCells[cell.id]); 70 | if (cellRightList.length) { 71 | const cellRight = cellRightList[0]; 72 | this.relatedRightCells[cellLeft.id].push(cellRight); 73 | this.relatedLeftCells[cellRight.id].push(cellLeft); 74 | usedRightCells[cellRight.id] = true; 75 | } 76 | } 77 | 78 | for (const cellLeft of this.notebookLeft.cellList.filter( 79 | cell => !this.relatedRightCells[cell.id].length 80 | )) { 81 | const cellRightList = this.notebookRight.cellList 82 | .filter(cell => !usedRightCells[cell.id]) 83 | .filter(cell => cellLeft.memeUuid === cell.memeUuid) 84 | .filter(cell => cellLeft.memeBranchNumber < cell.memeBranchNumber); 85 | if (cellRightList.length) { 86 | const cellRight = cellRightList[0]; 87 | this.relatedRightCells[cellLeft.id].push(cellRight); 88 | this.relatedLeftCells[cellRight.id].push(cellLeft); 89 | usedRightCells[cellRight.id] = true; 90 | } 91 | } 92 | 93 | for (const cellLeft of this.notebookLeft.cellList.filter( 94 | cell => !this.relatedRightCells[cell.id].length 95 | )) { 96 | const cellRightList = this.notebookRight.cellList 97 | .filter(cell => !usedRightCells[cell.id]) 98 | .filter(cell => cellLeft.memeUuid === cell.memeUuid); 99 | if (cellRightList.length) { 100 | const cellRight = cellRightList[0]; 101 | this.relatedRightCells[cellLeft.id].push(cellRight); 102 | this.relatedLeftCells[cellRight.id].push(cellLeft); 103 | usedRightCells[cellRight.id] = true; 104 | } 105 | } 106 | } else if (this.matchType === RelationMatchType.Exact) { 107 | for (const cellLeft of this.notebookLeft.cellList) { 108 | const cellRightList = this.notebookRight.getCellsByMeme(cellLeft.meme); 109 | for (const cellRight of cellRightList) { 110 | this.relatedRightCells[cellLeft.id].push(cellRight); 111 | this.relatedLeftCells[cellRight.id].push(cellLeft); 112 | } 113 | } 114 | } else { 115 | throw new Error(`Invalid match type: ${this.matchType}`); 116 | } 117 | } 118 | 119 | /** 描画を更新する */ 120 | updateView(): void { 121 | this.$view.html(this.html()); 122 | } 123 | 124 | /** HTMLを生成する */ 125 | private html(): string { 126 | let html = ''; 127 | const offsetY = -this.$view.offset()!.top + 14; 128 | const height = Math.max( 129 | this.notebookLeft.$view.height() || 0, 130 | this.notebookRight.$view.height() || 0 131 | ); 132 | html += '
'; 133 | html += ''; 134 | for (const cellLeft of this.notebookLeft.cellList) { 135 | for (const cellRight of this.relatedRightCells[cellLeft.id]) { 136 | const y0 = cellLeft.y + offsetY; 137 | const y1 = cellRight.y + offsetY; 138 | html += 139 | ''; 141 | } 142 | } 143 | html += ''; 144 | html += '
'; 145 | return html; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /components/src/Cell.ts: -------------------------------------------------------------------------------- 1 | import { Notebook } from './Notebook'; 2 | 3 | function splitLinesWithNewLine(source: string): string[] { 4 | return source.match(/(.+[\r\n]+|.+$)/gm) || []; 5 | } 6 | 7 | export class Cell { 8 | /** セルの種類 */ 9 | cellType: string; 10 | 11 | /** メタデータ */ 12 | metaData: any; 13 | 14 | /** ソースコード */ 15 | source: string[]; 16 | 17 | /** ノートブック */ 18 | notebook: Notebook; 19 | 20 | /** 描画されたJQueryノード */ 21 | $view: JQuery; 22 | 23 | /** ID */ 24 | id: string; 25 | 26 | /** 選択中か? */ 27 | selected: boolean; 28 | 29 | /** マーク済みか */ 30 | marked: boolean; 31 | 32 | /** IDのカウンタ */ 33 | static idCounter: number = 0; 34 | 35 | /** 36 | * 初期化 37 | */ 38 | constructor($: JQueryStatic, notebook: Notebook, data: any) { 39 | this.notebook = notebook; 40 | this.cellType = data['cell_type']; 41 | this.metaData = data['metadata']; 42 | const source: any = data['source']; 43 | if (typeof source === 'string') { 44 | this.source = splitLinesWithNewLine(source); 45 | } else { 46 | this.source = source; 47 | } 48 | this.id = (Cell.idCounter++).toString(); 49 | this.selected = false; 50 | this.marked = false; 51 | this.$view = $(this.html()); 52 | } 53 | 54 | /** 55 | * HTMLを生成する 56 | */ 57 | private html(): string { 58 | let html = ''; 59 | html += '
'; 61 | html += '
'; 62 | html += 63 | '+-'; 64 | html += ' '; 65 | if (this.hasMeme) { 66 | const memeTokens = this.meme.split('-'); 67 | html += '' + memeTokens.shift() + '-' + memeTokens.join('-'); 68 | } 69 | html += '
'; 70 | html += '
' + this.sourceEscaped + '
'; 71 | html += '
'; 72 | return html; 73 | } 74 | 75 | /** 76 | * スタイルを更新する 77 | * 関連するCellと異なるソースであれば色を変える 78 | */ 79 | updateStyle(leftCellsList: Cell[][]): void { 80 | if (!this.hasMeme || !leftCellsList.length) { 81 | this.$view.removeClass('changed1'); 82 | this.$view.removeClass('changed2'); 83 | } else if (leftCellsList.length === 1) { 84 | if (this.checkChanged([this], leftCellsList[0])) { 85 | this.$view.addClass('changed1'); 86 | this.$view.removeClass('changed2'); 87 | } else { 88 | this.$view.removeClass('changed1'); 89 | this.$view.removeClass('changed2'); 90 | } 91 | } else { 92 | const ch0 = this.checkChanged([this], leftCellsList[0]); 93 | const ch1 = this.checkChanged([this], leftCellsList[1]); 94 | const ch01 = this.checkChanged(leftCellsList[0], leftCellsList[1]); 95 | if (ch01) { 96 | if (!ch0) { 97 | this.$view.removeClass('changed1'); 98 | this.$view.removeClass('changed2'); 99 | } else if (!ch1) { 100 | this.$view.addClass('changed1'); 101 | this.$view.removeClass('changed2'); 102 | } else { 103 | this.$view.removeClass('changed1'); 104 | this.$view.addClass('changed2'); 105 | } 106 | } else { 107 | if (ch0 || ch1) { 108 | this.$view.addClass('changed1'); 109 | this.$view.removeClass('changed2'); 110 | } else { 111 | this.$view.removeClass('changed1'); 112 | this.$view.removeClass('changed2'); 113 | } 114 | } 115 | } 116 | } 117 | 118 | get hasMeme(): boolean { 119 | if (this.metaData['lc_cell_meme'] === undefined) { 120 | return false; 121 | } 122 | return this.metaData['lc_cell_meme']['current'] !== undefined; 123 | } 124 | 125 | /** このmemeを取得する */ 126 | get meme(): string { 127 | return this.metaData['lc_cell_meme']['current']; 128 | } 129 | 130 | /** このmemeのUUIDを取得する */ 131 | get memeUuid(): string { 132 | return this.meme ? this.meme.split('-').slice(0, 5).join('-') : ''; 133 | } 134 | 135 | /** このmemeの枝番数を取得する */ 136 | get memeBranchNumber(): number { 137 | const meme = this.meme || ''; 138 | const numStr = meme.split('-').slice(5, 6).pop() || ''; 139 | return numStr ? parseInt(numStr, 10) : 0; 140 | } 141 | 142 | /** 次のmemeを取得する */ 143 | get nextMeme(): string { 144 | return this.metaData['lc_cell_meme']['next']; 145 | } 146 | 147 | /** 前のmemeを取得する */ 148 | get prevMeme(): string { 149 | return this.metaData['lc_cell_meme']['previous']; 150 | } 151 | 152 | /** ソースを結合して取得する */ 153 | get sourceEscaped(): string { 154 | let html = ''; 155 | for (let i = 0; i < this.source.length; i++) { 156 | html += this.source[i] + '
'; 157 | } 158 | return html; 159 | } 160 | 161 | /** ソースを結合して取得する */ 162 | get sourceAll(): string { 163 | return this.source.join(''); 164 | } 165 | 166 | /** x座標を取得する */ 167 | get x(): number { 168 | const offset = this.$view.offset(); 169 | if (offset === undefined) { 170 | throw new Error('offset is undefined'); 171 | } 172 | return offset.left; 173 | } 174 | 175 | /** y座標を取得する */ 176 | get y(): number { 177 | const offset = this.$view.offset(); 178 | if (offset === undefined) { 179 | throw new Error('offset is undefined'); 180 | } 181 | return offset.top; 182 | } 183 | 184 | /** y座標を設定する */ 185 | set y(y: number) { 186 | this.$view.css('margin-top', y - this.y + 5); 187 | } 188 | 189 | /** y座標をリセットする */ 190 | resetY() { 191 | this.$view.css('margin-top', 5); 192 | } 193 | 194 | /** 幅を取得する */ 195 | get width(): number | undefined { 196 | return this.$view.width(); 197 | } 198 | 199 | /** 高さを取得する */ 200 | get height(): number | undefined { 201 | return this.$view.height(); 202 | } 203 | 204 | /** 選択を行う */ 205 | public select(selected: boolean): void { 206 | if (this.selected !== selected) { 207 | this.selected = selected; 208 | const $selectButton = this.$view.find('.select-button'); 209 | $selectButton.empty(); 210 | if (this.selected) { 211 | $selectButton.prepend('✔'); 212 | } else { 213 | $selectButton.prepend(' '); 214 | } 215 | } 216 | } 217 | 218 | /** マークする */ 219 | public mark(marked: boolean): void { 220 | if (this.marked !== marked) { 221 | this.marked = marked; 222 | const $selectButton = this.$view.find('.select-button'); 223 | if (this.marked) { 224 | $selectButton.addClass('marked'); 225 | } else { 226 | $selectButton.removeClass('marked'); 227 | } 228 | } 229 | } 230 | 231 | private checkChanged(bases: Cell[], cells: Cell[]): boolean { 232 | for (const base of bases) { 233 | for (const target of cells) { 234 | if (target.source.join('\n') !== base.source.join('\n')) { 235 | return true; 236 | } 237 | } 238 | } 239 | return false; 240 | } 241 | } 242 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc_notebook_diff", 3 | "version": "0.2.0", 4 | "description": "diff extension", 5 | "keywords": [ 6 | "jupyter", 7 | "jupyterlab", 8 | "jupyterlab-extension" 9 | ], 10 | "homepage": "https://github.com/NII-cloud-operation/Jupyter-LC_notebook_diff", 11 | "bugs": { 12 | "url": "https://github.com/NII-cloud-operation/Jupyter-LC_notebook_diff/issues" 13 | }, 14 | "license": "BSD-3-Clause", 15 | "author": { 16 | "name": "nii-cloud-operation", 17 | "email": "yazawa@yzwlab.net" 18 | }, 19 | "files": [ 20 | "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}", 21 | "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}" 22 | ], 23 | "main": "lib/index.js", 24 | "types": "lib/index.d.ts", 25 | "style": "style/index.css", 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/NII-cloud-operation/Jupyter-LC_notebook_diff.git" 29 | }, 30 | "scripts": { 31 | "build": "jlpm && jlpm build:lib && jlpm build:labextension:dev", 32 | "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension", 33 | "build:labextension": "jupyter labextension build .", 34 | "build:labextension:dev": "jupyter labextension build --development True .", 35 | "build:lib": "tsc --sourceMap", 36 | "build:lib:prod": "tsc", 37 | "clean": "jlpm clean:lib", 38 | "clean:lib": "rimraf lib tsconfig.tsbuildinfo", 39 | "clean:lintcache": "rimraf .eslintcache .stylelintcache", 40 | "clean:labextension": "rimraf lc_notebook_diff/labextension lc_notebook_diff/_version.py", 41 | "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache", 42 | "eslint": "jlpm eslint:check --fix", 43 | "eslint:check": "eslint . --cache --ext .ts,.tsx", 44 | "install:extension": "jlpm build", 45 | "lint": "jlpm stylelint && jlpm prettier && jlpm eslint", 46 | "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check", 47 | "prettier": "jlpm prettier:base --write --list-different", 48 | "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"", 49 | "prettier:check": "jlpm prettier:base --check", 50 | "stylelint": "jlpm stylelint:check --fix", 51 | "stylelint:check": "stylelint --cache \"style/**/*.css\"", 52 | "test": "jest --coverage", 53 | "watch": "run-p watch:src watch:labextension", 54 | "watch:src": "tsc -w --sourceMap", 55 | "watch:labextension": "jupyter labextension watch ." 56 | }, 57 | "dependencies": { 58 | "@emotion/react": "^11.11.3", 59 | "@emotion/styled": "^11.11.0", 60 | "@fortawesome/free-solid-svg-icons": "^6.5.1", 61 | "@jupyterlab/application": "^4.0.0", 62 | "@mui/icons-material": "^5.15.2", 63 | "@mui/material": "^5.15.2", 64 | "codemirror": "^5.65.14", 65 | "diff-match-patch": "^1.0.5", 66 | "jquery": "^3.7.1" 67 | }, 68 | "devDependencies": { 69 | "@jupyterlab/builder": "^4.0.0", 70 | "@jupyterlab/testutils": "^4.0.0", 71 | "@types/codemirror": "^5.60.8", 72 | "@types/diff-match-patch": "^1.0.32", 73 | "@types/jest": "^29.2.0", 74 | "@types/jquery": "^3.5.29", 75 | "@types/json-schema": "^7.0.11", 76 | "@types/react": "^18.0.26", 77 | "@types/react-addons-linked-state-mixin": "^0.14.22", 78 | "@typescript-eslint/eslint-plugin": "^6.1.0", 79 | "@typescript-eslint/parser": "^6.1.0", 80 | "css-loader": "^6.7.1", 81 | "eslint": "^8.36.0", 82 | "eslint-config-prettier": "^8.8.0", 83 | "eslint-plugin-prettier": "^5.0.0", 84 | "jest": "^29.2.0", 85 | "npm-run-all": "^4.1.5", 86 | "prettier": "^3.0.0", 87 | "rimraf": "^5.0.1", 88 | "source-map-loader": "^1.0.2", 89 | "style-loader": "^3.3.1", 90 | "stylelint": "^15.10.1", 91 | "stylelint-config-recommended": "^13.0.0", 92 | "stylelint-config-standard": "^34.0.0", 93 | "stylelint-csstree-validator": "^3.0.0", 94 | "stylelint-prettier": "^4.0.0", 95 | "typescript": "^5.0.2", 96 | "yjs": "^13.5.0" 97 | }, 98 | "sideEffects": [ 99 | "style/*.css", 100 | "style/index.js" 101 | ], 102 | "styleModule": "style/index.js", 103 | "publishConfig": { 104 | "access": "public" 105 | }, 106 | "jupyterlab": { 107 | "extension": true, 108 | "outputDir": "lc_notebook_diff/labextension" 109 | }, 110 | "eslintIgnore": [ 111 | "node_modules", 112 | "dist", 113 | "coverage", 114 | "**/*.d.ts", 115 | "tests", 116 | "**/__tests__", 117 | "ui-tests", 118 | "components", 119 | "nbextension", 120 | "work" 121 | ], 122 | "eslintConfig": { 123 | "extends": [ 124 | "eslint:recommended", 125 | "plugin:@typescript-eslint/eslint-recommended", 126 | "plugin:@typescript-eslint/recommended", 127 | "plugin:prettier/recommended" 128 | ], 129 | "parser": "@typescript-eslint/parser", 130 | "parserOptions": { 131 | "project": "tsconfig.json", 132 | "sourceType": "module" 133 | }, 134 | "plugins": [ 135 | "@typescript-eslint" 136 | ], 137 | "rules": { 138 | "@typescript-eslint/naming-convention": [ 139 | "error", 140 | { 141 | "selector": "interface", 142 | "format": [ 143 | "PascalCase" 144 | ], 145 | "custom": { 146 | "regex": "^I[A-Z]", 147 | "match": true 148 | } 149 | } 150 | ], 151 | "@typescript-eslint/no-unused-vars": [ 152 | "warn", 153 | { 154 | "args": "none" 155 | } 156 | ], 157 | "@typescript-eslint/no-explicit-any": "off", 158 | "@typescript-eslint/no-namespace": "off", 159 | "@typescript-eslint/no-use-before-define": "off", 160 | "@typescript-eslint/quotes": [ 161 | "error", 162 | "single", 163 | { 164 | "avoidEscape": true, 165 | "allowTemplateLiterals": false 166 | } 167 | ], 168 | "curly": [ 169 | "error", 170 | "all" 171 | ], 172 | "eqeqeq": "error", 173 | "prefer-arrow-callback": "error" 174 | } 175 | }, 176 | "prettier": { 177 | "singleQuote": true, 178 | "trailingComma": "none", 179 | "arrowParens": "avoid", 180 | "endOfLine": "auto", 181 | "overrides": [ 182 | { 183 | "files": "package.json", 184 | "options": { 185 | "tabWidth": 4 186 | } 187 | } 188 | ] 189 | }, 190 | "stylelint": { 191 | "extends": [ 192 | "stylelint-config-recommended", 193 | "stylelint-config-standard", 194 | "stylelint-prettier/recommended" 195 | ], 196 | "plugins": [ 197 | "stylelint-csstree-validator" 198 | ], 199 | "rules": { 200 | "csstree/validator": true, 201 | "property-no-vendor-prefix": null, 202 | "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$", 203 | "selector-no-vendor-prefix": null, 204 | "value-no-vendor-prefix": null 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /lc_notebook_diff/nbextension/main.js: -------------------------------------------------------------------------------- 1 | require.config({ 2 | map: { 3 | '*': { 4 | "diff_match_patch": "nbextensions/notebook_diff/diff_match_patch" 5 | }, 6 | }, 7 | }); 8 | 9 | define([ 10 | 'jquery', 11 | 'base/js/namespace', 12 | 'base/js/utils', 13 | 'require', 14 | 'codemirror/lib/codemirror', 15 | 'codemirror/addon/merge/merge', 16 | 'codemirror/addon/search/searchcursor', 17 | 'codemirror/addon/scroll/annotatescrollbar', 18 | 'codemirror/addon/search/matchesonscrollbar', 19 | './jupyter-notebook-diff' 20 | ], function( 21 | $, 22 | Jupyter, 23 | utils, 24 | require, 25 | CodeMirror, 26 | JupyterDiff 27 | ) { 28 | "use strict"; 29 | 30 | JupyterDiff = JupyterNotebook; 31 | 32 | var base_url = utils.get_body_data('baseUrl'); 33 | console.log(JupyterDiff, base_url); 34 | 35 | function createUI() { 36 | var main = $('
') 37 | .addClass('diff-content-panel'); 38 | var error = $('
') 39 | .attr('id', 'notebook_diff_error'); 40 | var files = $('
') 41 | .addClass('list-group col-xs-12') 42 | .appendTo(main); 43 | 44 | [0, 1, 2].forEach(i => { 45 | var fieldName = 'diff-file' + i; 46 | var displayName = 'File #' + (i + 1).toString(); 47 | const orderPanel = $('').addClass('nbdiff-order'); 48 | if (i > 0) { 49 | const icon = $('') 50 | .addClass('fa fa-arrow-up') 51 | const arrowButton = $('') 52 | .addClass('btn btn-link nbdiff-order') 53 | .append(icon); 54 | arrowButton.click(() => { 55 | console.log('UP', i - 1, i); 56 | const t1 = $(`#diff-file${i - 1}`); 57 | const t2 = $(`#diff-file${i}`); 58 | const v = t1.val(); 59 | t1.val(t2.val()); 60 | t2.val(v); 61 | }); 62 | orderPanel.append(arrowButton); 63 | } 64 | if (i < 2) { 65 | const icon = $('') 66 | .addClass('fa fa-arrow-down') 67 | const arrowButton = $('') 68 | .addClass('btn btn-link nbdiff-order') 69 | .append(icon); 70 | arrowButton.click(() => { 71 | console.log('DOWN', i, i + 1); 72 | const t1 = $(`#diff-file${i}`); 73 | const t2 = $(`#diff-file${i + 1}`); 74 | const v = t1.val(); 75 | t1.val(t2.val()); 76 | t2.val(v); 77 | }); 78 | orderPanel.append(arrowButton); 79 | } 80 | files.append($('
') 81 | .addClass('form-group list-group-item col-xs-12') 82 | .append($('') 83 | .attr('for', fieldName) 84 | .addClass('col-xs-2') 85 | .text(displayName)) 86 | .append($('') 87 | .attr('type', 'text') 88 | .addClass('col-xs-8') 89 | .attr('id', fieldName)) 90 | .append(orderPanel)); 91 | }); 92 | files.append($('
') 93 | .addClass('form-group list-group-item col-xs-12') 94 | .append($('') 95 | .attr('id', 'diff-search') 96 | .addClass('btn btn-primary col-xs-10') 97 | .text('Show diff'))); 98 | main.append($('
') 99 | .attr('id', 'diff-content-container')); 100 | 101 | return $('
') 102 | .append(error) 103 | .append(main); 104 | } 105 | 106 | function getFilesFromQuery() { 107 | var url = window.location.search; 108 | if (! /^\?.+/.test(url)) { 109 | return {}; 110 | } 111 | var r = {}; 112 | url.slice(1).split('&').map(function(v) { 113 | var elem = v.split('='); 114 | if(elem.length == 2) { 115 | r[elem[0]] = decodeURIComponent(elem[1]); 116 | }else{ 117 | console.error('Invalid query', elem); 118 | } 119 | }); 120 | return r; 121 | } 122 | 123 | function showError(message) { 124 | $('#notebook_diff_error').append($('
') 125 | .addClass('alert alert-danger') 126 | .text(message)); 127 | } 128 | 129 | function hideError() { 130 | $('#notebook_diff_error').empty(); 131 | } 132 | 133 | function insertTab () { 134 | var tab_text = 'Diff'; 135 | var tab_id = 'notebook_diff'; 136 | 137 | $('
') 138 | .attr('id', tab_id) 139 | .append(createUI()) 140 | .addClass('tab-pane') 141 | .appendTo('.tab-content'); 142 | 143 | var tab_link = $('') 144 | .text(tab_text) 145 | .attr('href', '#' + tab_id) 146 | .attr('data-toggle', 'tab') 147 | .on('click', function (evt) { 148 | window.history.pushState(null, null, '#' + tab_id); 149 | }); 150 | 151 | $('
  • ') 152 | .append(tab_link) 153 | .appendTo('#tabs'); 154 | var query = getFilesFromQuery(); 155 | for(var i = 0; i < 3; i ++) { 156 | var key = 'diffpath' + (i + 1); 157 | var fieldName = 'diff-file' + i; 158 | if (query[key]) { 159 | $('#' + fieldName).val(query[key]); 160 | } 161 | } 162 | $('#diff-search').click(function() { 163 | $('#diff-content-container').empty(); 164 | var filenames = []; 165 | for(var i = 0; i < 3; i ++) { 166 | var filename = $('#diff-file' + i).val(); 167 | if(filename.trim().length > 0) { 168 | filenames.push(base_url + 'files/' + filename); 169 | } 170 | } 171 | console.log(filenames); 172 | if(filenames.length < 2) { 173 | showError('Two or more filenames required'); 174 | }else{ 175 | hideError(); 176 | $('
    ') 177 | .attr('id', 'diff-content') 178 | .addClass('jupyter-notebook-diff') 179 | .appendTo($('#diff-content-container')); 180 | new JupyterDiff.DiffView($('#diff-content'), CodeMirror, filenames, [], 181 | function(url, jqXHR, textStatus) { 182 | showError('Cannot load ' + url + ': ' + textStatus); 183 | }); 184 | } 185 | }); 186 | 187 | // select tab if hash is set appropriately 188 | if (window.location.hash == '#' + tab_id) { 189 | tab_link.click(); 190 | } 191 | } 192 | 193 | function load_ipython_extension () { 194 | requirejs.config({ 195 | paths: { 196 | diff_match_patch: base_url + 'nbextensions/notebook_diff/diff_match_patch' 197 | } 198 | }); 199 | require(['codemirror/addon/merge/merge'], function(Merge) { 200 | // add css first 201 | $('') 202 | .attr('rel', 'stylesheet') 203 | .attr('type', 'text/css') 204 | .attr('href', require.toUrl('./main.css')) 205 | .appendTo('head'); 206 | $('') 207 | .attr('rel', 'stylesheet') 208 | .attr('type', 'text/css') 209 | .attr('href', require.toUrl('codemirror/lib/codemirror.css')) 210 | .appendTo('head'); 211 | $('') 212 | .attr('rel', 'stylesheet') 213 | .attr('type', 'text/css') 214 | .attr('href', require.toUrl('codemirror/addon/merge/merge.css')) 215 | .appendTo('head'); 216 | $('') 217 | .attr('rel', 'stylesheet') 218 | .attr('type', 'text/css') 219 | .attr('href', require.toUrl('./jupyter-notebook-diff.css')) 220 | .appendTo('head'); 221 | 222 | insertTab(); 223 | }); 224 | } 225 | 226 | return { 227 | load_ipython_extension : load_ipython_extension 228 | }; 229 | 230 | }); 231 | -------------------------------------------------------------------------------- /ui-tests/e2e-notebook/notebooks/scripts/playwright.py: -------------------------------------------------------------------------------- 1 | """Playwright utility helpers. 2 | 3 | Copied from https://github.com/RCOSDP/RDM-e2e-test-nb (scripts/playwright.py) 4 | commit dda7a5de4336c3c3a79537f1537de7d65a0034e6 with minimal path adjustments 5 | for nbsearch. 6 | """ 7 | 8 | from datetime import datetime 9 | import os 10 | import shutil 11 | import sys 12 | import tempfile 13 | import time 14 | import traceback 15 | 16 | from IPython.display import Image 17 | from playwright.async_api import async_playwright, expect 18 | 19 | playwright = None 20 | current_session_id = None 21 | current_browser = None 22 | current_browser_type = None 23 | current_contexts = None 24 | default_last_path = None 25 | context_close_on_fail = True 26 | temp_dir = None 27 | default_delay = None 28 | console_messages = [] 29 | test_execution_counter = 0 30 | 31 | 32 | async def run_pw( 33 | f, 34 | last_path=default_last_path, 35 | screenshot=True, 36 | permissions=None, 37 | new_context=False, 38 | new_page=False, 39 | ): 40 | global current_browser 41 | global current_browser_type 42 | if current_browser is None: 43 | browser_type = current_browser_type or 'chromium' 44 | if browser_type == 'firefox': 45 | current_browser = await playwright.firefox.launch( 46 | headless=True, 47 | args=[], 48 | ) 49 | else: 50 | current_browser = await playwright.chromium.launch( 51 | headless=True, 52 | args=["--no-sandbox", "--disable-dev-shm-usage", "--lang=ja"], 53 | ) 54 | 55 | global current_contexts 56 | if current_contexts is None or len(current_contexts) == 0 or new_context: 57 | videos_dir = os.path.join(temp_dir, "videos/") 58 | os.makedirs(videos_dir, exist_ok=True) 59 | har_path = os.path.join(temp_dir, "har.zip") 60 | 61 | context = await current_browser.new_context( 62 | locale="ja-JP", 63 | record_video_dir=videos_dir, 64 | record_har_path=har_path, 65 | ) 66 | if current_contexts is None: 67 | current_contexts = [(context, [])] 68 | else: 69 | current_contexts.append((context, [])) 70 | 71 | current_context, current_pages = current_contexts[-1] 72 | if len(current_pages) == 0 or new_page: 73 | page = await current_context.new_page() 74 | page.on("console", lambda msg: console_messages.append({ 75 | "timestamp": time.time(), 76 | "url": page.url, 77 | "type": msg.type, 78 | "text": msg.text 79 | })) 80 | current_pages.append(page) 81 | 82 | global test_execution_counter 83 | test_execution_counter += 1 84 | 85 | current_time = time.time() 86 | print(f"Start epoch: {current_time} seconds") 87 | if permissions is not None: 88 | await current_context.grant_permissions(permissions) 89 | 90 | # Test console.log capture 91 | await current_pages[-1].evaluate(f'console.log("[TEST-PLAYWRIGHT] counter={test_execution_counter}, timestamp={current_time}, url=" + window.location.href)') 92 | 93 | next_page = None 94 | if f is not None: 95 | try: 96 | next_page = await f(current_pages[-1]) 97 | # Apply default delay after function execution if specified 98 | if default_delay is not None and default_delay > 0: 99 | await current_pages[-1].wait_for_timeout(default_delay) 100 | except Exception: 101 | if context_close_on_fail: 102 | await finish_pw_context(screenshot=screenshot, last_path=last_path) 103 | raise 104 | if screenshot: 105 | await _save_screenshot() 106 | raise 107 | if next_page is not None: 108 | next_page.on("console", lambda msg: console_messages.append({ 109 | "timestamp": time.time(), 110 | "url": next_page.url, 111 | "type": msg.type, 112 | "text": msg.text 113 | })) 114 | current_pages.append(next_page) 115 | screenshot_path = os.path.join(temp_dir, "screenshot.png") 116 | await current_pages[-1].screenshot(path=screenshot_path) 117 | return Image(screenshot_path) 118 | 119 | 120 | async def close_latest_page(last_path=None): 121 | global current_contexts 122 | if current_contexts is None or len(current_contexts) == 0: 123 | raise Exception("No contexts") 124 | current_context, current_pages = current_contexts[-1] 125 | if len(current_contexts) <= 1 and len(current_pages) <= 1: 126 | raise Exception( 127 | "It is only possible to close when two or more contexts or pages are stacked" 128 | ) 129 | os.makedirs(last_path or default_last_path, exist_ok=True) 130 | last_page = current_pages[-1] 131 | if last_page in current_pages[:-1] or any( 132 | last_page in pages for _, pages in current_contexts[:-1] 133 | ): 134 | assert len(current_pages) > 0, current_pages 135 | current_contexts[-1] = (current_context, current_pages[:-1]) 136 | return 137 | video_path = await last_page.video.path() 138 | index = len(current_pages) 139 | dest_video_path = os.path.join(last_path or default_last_path, f"video-{index}.webm") 140 | shutil.copyfile(video_path, dest_video_path) 141 | current_pages = current_pages[:-1] 142 | current_contexts[-1] = (current_context, current_pages) 143 | await last_page.close() 144 | if len(current_pages) > 0: 145 | return 146 | current_contexts = current_contexts[:-1] 147 | await current_context.close() 148 | 149 | 150 | async def init_pw_context(close_on_fail=True, last_path=None, delay=None, browser_type='chromium'): 151 | global playwright 152 | global current_session_id 153 | global default_last_path 154 | global current_browser 155 | global current_browser_type 156 | global temp_dir 157 | global context_close_on_fail 158 | global current_contexts 159 | global default_delay 160 | global console_messages 161 | global test_execution_counter 162 | if current_browser is not None: 163 | await current_browser.close() 164 | current_browser = None 165 | if playwright is not None: 166 | await playwright.stop() 167 | playwright = None 168 | playwright = await async_playwright().start() 169 | current_session_id = datetime.now().strftime("%Y%m%d-%H%M%S") 170 | default_last_path = last_path or os.path.join( 171 | os.path.expanduser("~/last-screenshots"), current_session_id 172 | ) 173 | temp_dir = tempfile.mkdtemp() 174 | context_close_on_fail = close_on_fail 175 | current_browser_type = browser_type 176 | default_delay = delay 177 | console_messages = [] 178 | test_execution_counter = 0 179 | if current_contexts is not None: 180 | for current_context in current_contexts: 181 | await current_context.close() 182 | current_contexts = None 183 | return (current_session_id, temp_dir) 184 | 185 | 186 | async def finish_pw_context(screenshot=False, last_path=None): 187 | global current_browser 188 | await _finish_pw_context(screenshot=screenshot, last_path=last_path) 189 | if current_browser is not None: 190 | await current_browser.close() 191 | current_browser = None 192 | 193 | 194 | async def save_screenshot(path): 195 | if current_contexts is None or len(current_contexts) == 0: 196 | raise Exception("No contexts") 197 | _, current_pages = current_contexts[-1] 198 | if current_pages is None or len(current_pages) == 0: 199 | raise Exception("Unexpected state") 200 | await current_pages[-1].screenshot(path=path) 201 | return path 202 | 203 | 204 | async def _save_screenshot(last_path=None): 205 | if current_contexts is None or len(current_contexts) == 0: 206 | raise Exception("No contexts") 207 | _, current_pages = current_contexts[-1] 208 | os.makedirs(last_path or default_last_path, exist_ok=True) 209 | if current_pages is None or len(current_pages) == 0: 210 | return 211 | screenshot_path = os.path.join(temp_dir, "last-screenshot.png") 212 | await current_pages[-1].screenshot(path=screenshot_path) 213 | dest_screenshot_path = os.path.join( 214 | last_path or default_last_path, "last-screenshot.png" 215 | ) 216 | shutil.copyfile(screenshot_path, dest_screenshot_path) 217 | print(f"Screenshot: {dest_screenshot_path}") 218 | 219 | # Save console logs 220 | console_log_path = os.path.join( 221 | last_path or default_last_path, "console.log" 222 | ) 223 | with open(console_log_path, "w") as f: 224 | for msg in console_messages: 225 | f.write(f"{msg['timestamp']:.3f} {msg['url']} [{msg['type']}] {msg['text']}\n") 226 | print(f"Console log: {console_log_path}") 227 | 228 | 229 | async def _finish_pw_context(screenshot=False, last_path=None): 230 | global current_contexts 231 | if current_contexts is None or len(current_contexts) == 0: 232 | return 233 | current_context, current_pages = current_contexts[-1] 234 | os.makedirs(last_path or default_last_path, exist_ok=True) 235 | timeout_on_screenshot = False 236 | if screenshot and current_pages is not None and len(current_pages) > 0: 237 | try: 238 | await _save_screenshot(last_path=last_path) 239 | except Exception: 240 | print("スクリーンショットの取得に失敗しました。", file=sys.stderr) 241 | traceback.print_exc() 242 | timeout_on_screenshot = True 243 | if timeout_on_screenshot: 244 | return 245 | current_contexts = current_contexts[::-1] 246 | await current_context.close() 247 | for i, current_page in enumerate(current_pages): 248 | index = i + 1 249 | try: 250 | video_path = await current_page.video.path() 251 | dest_video_path = os.path.join( 252 | last_path or default_last_path, f"video-{index}.webm" 253 | ) 254 | shutil.copyfile(video_path, dest_video_path) 255 | print(f"Video: {dest_video_path}") 256 | except Exception: 257 | print("スクリーンキャプチャ動画の取得に失敗しました。", file=sys.stderr) 258 | traceback.print_exc() 259 | timeout_on_screenshot = True 260 | if timeout_on_screenshot: 261 | return 262 | har_path = os.path.join(temp_dir, "har.zip") 263 | dest_har_path = os.path.join(last_path or default_last_path, "har.zip") 264 | if os.path.exists(har_path): 265 | shutil.copyfile(har_path, dest_har_path) 266 | print(f"HAR: {dest_har_path}") 267 | else: 268 | print(".harファイルの取得に失敗しました。", file=sys.stderr) 269 | 270 | # Save console logs (always, not just on screenshot) 271 | console_log_path = os.path.join( 272 | last_path or default_last_path, "console.log" 273 | ) 274 | with open(console_log_path, "w") as f: 275 | for msg in console_messages: 276 | f.write(f"{msg['timestamp']:.3f} {msg['url']} [{msg['type']}] {msg['text']}\n") 277 | print(f"Console log: {console_log_path}") 278 | 279 | shutil.rmtree(temp_dir) 280 | for page in current_pages: 281 | await page.close() 282 | if len(current_contexts) == 0: 283 | return 284 | await _finish_pw_context(screenshot=False, last_path=last_path) 285 | 286 | 287 | __all__ = [ 288 | "async_playwright", 289 | "expect", 290 | "run_pw", 291 | "close_latest_page", 292 | "init_pw_context", 293 | "finish_pw_context", 294 | "save_screenshot", 295 | ] 296 | -------------------------------------------------------------------------------- /components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig to read more about this file */ 4 | 5 | /* Projects */ 6 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ 7 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ 8 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */ 9 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ 10 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ 11 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ 12 | 13 | /* Language and Environment */ 14 | "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ 15 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ 16 | // "jsx": "preserve", /* Specify what JSX code is generated. */ 17 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ 18 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ 19 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ 20 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */ 21 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */ 22 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */ 23 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ 24 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ 25 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ 26 | 27 | /* Modules */ 28 | // "module": "commonjs", /* Specify what module code is generated. */ 29 | // "rootDir": "./", /* Specify the root folder within your source files. */ 30 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ 31 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ 32 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ 33 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ 34 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ 35 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */ 36 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 37 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ 38 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */ 39 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */ 40 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */ 41 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */ 42 | // "resolveJsonModule": true, /* Enable importing .json files. */ 43 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */ 44 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ 45 | 46 | /* JavaScript Support */ 47 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ 48 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ 49 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ 50 | 51 | /* Emit */ 52 | "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ 53 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */ 54 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */ 55 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */ 56 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */ 57 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */ 58 | "outDir": "./lib/", /* Specify an output folder for all emitted files. */ 59 | // "removeComments": true, /* Disable emitting comments. */ 60 | // "noEmit": true, /* Disable emitting files from a compilation. */ 61 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */ 62 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */ 63 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */ 64 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */ 67 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */ 68 | // "newLine": "crlf", /* Set the newline character for emitting files. */ 69 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */ 70 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */ 71 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */ 72 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ 73 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ 74 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ 75 | 76 | /* Interop Constraints */ 77 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ 78 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */ 79 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ 80 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ 81 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ 82 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ 83 | 84 | /* Type Checking */ 85 | "strict": true, /* Enable all strict type-checking options. */ 86 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ 87 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ 88 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ 89 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */ 90 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */ 91 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */ 92 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */ 93 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */ 94 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */ 95 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */ 96 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ 97 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ 98 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ 99 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ 100 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ 101 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ 102 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ 103 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ 104 | 105 | /* Completeness */ 106 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ 107 | "skipLibCheck": true /* Skip type checking all .d.ts files. */ 108 | }, 109 | "include": ["src/*"] 110 | } 111 | -------------------------------------------------------------------------------- /components/src/DiffView.ts: -------------------------------------------------------------------------------- 1 | import { Notebook } from './Notebook'; 2 | import { Cell } from './Cell'; 3 | import { Relation, RelationMatchType } from './Relation'; 4 | 5 | export type MergeViewResult = { 6 | edit: { 7 | focus: () => void; 8 | }; 9 | }; 10 | 11 | export type MergeViewOptions = { 12 | value: string | undefined; 13 | origLeft: string | undefined; 14 | origRight: string | undefined; 15 | lineNumbers: boolean; 16 | mode: string; 17 | highlightDifferences: boolean; 18 | collapseIdentical: boolean; 19 | readOnly: boolean; 20 | extraKeys: { 21 | Esc: () => void; 22 | }; 23 | }; 24 | 25 | export type MergeViewProvider = { 26 | MergeView: ( 27 | element: HTMLElement, 28 | options: MergeViewOptions 29 | ) => MergeViewResult; 30 | }; 31 | 32 | export class DiffView { 33 | $: JQueryStatic; 34 | 35 | /** セレクタ */ 36 | rootSelector: string; 37 | 38 | codeMirror: MergeViewProvider; 39 | 40 | /** コンテナ */ 41 | $container: JQuery; 42 | 43 | /** マージするためのビュー */ 44 | $mergeView: JQuery; 45 | 46 | /** ファイル名の配列 */ 47 | loadingFilenames: string[]; 48 | 49 | /** ファイルデータの配列 */ 50 | loadingFilecontents: any[]; 51 | 52 | /** ロードされたノートブック */ 53 | notebooks: Notebook[]; 54 | 55 | /** リレーション */ 56 | relations: Relation[]; 57 | 58 | /** マッチタイプ */ 59 | matchType: RelationMatchType; 60 | 61 | /** 初期化 */ 62 | constructor( 63 | $: JQueryStatic, 64 | rootSelector: string, 65 | codeMirror: MergeViewProvider, 66 | filenames: string[], 67 | filecontents: any[], 68 | errorCallback?: ( 69 | url: string, 70 | jqXHR: any, 71 | textStatus: string, 72 | errorThrown: any 73 | ) => void, 74 | { 75 | matchType = RelationMatchType.Fuzzy 76 | }: { matchType?: RelationMatchType } = {} 77 | ) { 78 | this.$ = $; 79 | this.rootSelector = rootSelector; 80 | this.codeMirror = codeMirror; 81 | this.$container = $(this.rootSelector); 82 | this.$mergeView = $('
    '); 83 | this.loadingFilenames = filenames; 84 | this.loadingFilecontents = filecontents; 85 | this.notebooks = []; 86 | this.relations = []; 87 | this.matchType = matchType; 88 | this.loadNext( 89 | errorCallback !== undefined 90 | ? errorCallback 91 | : url => { 92 | console.error('Failed to load content', url); 93 | } 94 | ); 95 | } 96 | 97 | /** 次のNotebookをロードする */ 98 | private loadNext( 99 | errorCallback: ( 100 | url: string, 101 | jqXHR: any, 102 | textStatus: string, 103 | errorThrown: any 104 | ) => void 105 | ): void { 106 | console.log('loadNext', this.loadingFilenames); 107 | if (this.loadingFilenames.length === 0) { 108 | // 描画 109 | this.render(); 110 | } else { 111 | // ロード 112 | const rawFilename = this.loadingFilenames.shift() as string; 113 | if (this.loadingFilecontents.length === 0) { 114 | const filename = encodeURI(rawFilename); 115 | this.$.getJSON(filename, data => { 116 | this.notebooks.push(new Notebook(this.$, rawFilename, data)); 117 | if (this.notebooks.length >= 2) { 118 | const i = this.notebooks.length - 2; 119 | this.relations.push( 120 | new Relation(this.$, this.notebooks[i], this.notebooks[i + 1], { 121 | matchType: this.matchType 122 | }) 123 | ); 124 | } 125 | this.loadNext(errorCallback); 126 | }).fail((jqXHR, textStatus, errorThrown) => { 127 | errorCallback(filename, jqXHR, textStatus, errorThrown); 128 | }); 129 | } else { 130 | const data = this.loadingFilecontents.shift(); 131 | this.notebooks.push(new Notebook(this.$, rawFilename, data)); 132 | if (this.notebooks.length >= 2) { 133 | const i = this.notebooks.length - 2; 134 | this.relations.push( 135 | new Relation(this.$, this.notebooks[i], this.notebooks[i + 1], { 136 | matchType: this.matchType 137 | }) 138 | ); 139 | } 140 | this.loadNext(errorCallback); 141 | } 142 | } 143 | } 144 | 145 | /** セルをハイライトする */ 146 | private highlightCell(cellId: string | null): void { 147 | this.$container.find('.cell').removeClass('highlight'); 148 | if (cellId !== null) { 149 | this.$container.find(`.cell[data-id="${cellId}"]`).addClass('highlight'); 150 | for (const cell of this.getRelatedCellsById(cellId)) { 151 | this.$container 152 | .find(`.cell[data-id="${cell.id}"]`) 153 | .addClass('highlight'); 154 | } 155 | } 156 | } 157 | 158 | /** リレーションを更新する */ 159 | private updateRelationsView(): void { 160 | for (const relation of this.relations) { 161 | relation.updateView(); 162 | } 163 | } 164 | 165 | /** リレーションを計算する */ 166 | private updateRelations(): void { 167 | for (const relation of this.relations) { 168 | relation.updateRelation(); 169 | } 170 | } 171 | 172 | /** マージビューを表示する */ 173 | private showMergeView(cellId: string | undefined): void { 174 | if (cellId === undefined) { 175 | throw new Error('cellId is undefined'); 176 | } 177 | const mergeViewElem = this.$mergeView[0]; 178 | let notebooks: Array = []; 179 | if (this.notebooks.length === 2) { 180 | notebooks = [null, this.notebooks[0], this.notebooks[1]]; 181 | } else { 182 | notebooks = this.notebooks; 183 | } 184 | 185 | const relatedCells = this.getRelatedCellsById(cellId); 186 | const targetCell = this.cellById(cellId) as Cell; 187 | const sources: Array = [ 188 | undefined, 189 | undefined, 190 | undefined 191 | ]; 192 | for (let i = 0; i < 3; i++) { 193 | if (!notebooks[i]) { 194 | continue; 195 | } 196 | const notebook = notebooks[i] as Notebook; 197 | if (notebook === targetCell.notebook) { 198 | sources[i] = targetCell.sourceAll; 199 | } else { 200 | const cell = relatedCells 201 | .filter(cell => notebook.cellList.indexOf(cell) !== -1) 202 | .shift(); 203 | if (!cell) { 204 | continue; 205 | } 206 | sources[i] = cell.sourceAll; 207 | } 208 | } 209 | 210 | /* eslint-disable-next-line @typescript-eslint/no-this-alias */ 211 | const self = this; 212 | const options = { 213 | value: sources[1], 214 | origLeft: sources[0], 215 | origRight: sources[2], 216 | lineNumbers: true, 217 | mode: 'text/html', 218 | highlightDifferences: true, 219 | collapseIdentical: false, 220 | readOnly: true, 221 | extraKeys: { 222 | Esc: function () { 223 | self.hideMergeView(); 224 | } 225 | } 226 | }; 227 | this.$mergeView.show(); 228 | this.$container.find('.dark').show(); 229 | const mv = this.codeMirror.MergeView(mergeViewElem, options); 230 | mv.edit.focus(); 231 | } 232 | 233 | /** マージビューを閉じる */ 234 | private hideMergeView(): void { 235 | this.$mergeView.empty(); 236 | this.$mergeView.hide(); 237 | this.$container.find('.dark').hide(); 238 | } 239 | 240 | /** 選択中の揃えるべきY座標を求める */ 241 | private maxCellY(): number { 242 | let y: number = 0; 243 | for (const notebook of this.notebooks) { 244 | const cell = notebook.selectedCell(); 245 | if (cell !== null) { 246 | y = Math.max(y, cell.y); 247 | } 248 | } 249 | return y; 250 | } 251 | 252 | /** セルの揃えをリセットする */ 253 | private resetCellY() { 254 | this.$container.find('.cell').css('margin-top', 5); 255 | } 256 | 257 | /** 指定したセルを揃える */ 258 | private alignCellY(y: number) { 259 | for (const notebook of this.notebooks) { 260 | const cell = notebook.selectedCell(); 261 | if (cell !== null) { 262 | cell.y = y; 263 | } 264 | } 265 | } 266 | 267 | /** セルを揃える */ 268 | private alignSelected(): void { 269 | this.resetCellY(); 270 | this.alignCellY(this.maxCellY()); 271 | } 272 | 273 | /** idからセルを検索する */ 274 | private cellById(id: string | undefined): Cell | null { 275 | if (id === undefined) { 276 | return null; 277 | } 278 | for (const notebook of this.notebooks) { 279 | for (const cell of notebook.cellList) { 280 | if (cell.id === id) { 281 | return cell; 282 | } 283 | } 284 | } 285 | return null; 286 | } 287 | 288 | /** セルを選択する */ 289 | private select(cell: Cell): void { 290 | for (const notebook of this.notebooks) { 291 | notebook.unselectAll(); 292 | notebook.unmarkAll(); 293 | } 294 | 295 | cell.select(true); 296 | cell.mark(true); 297 | for (const relatedCell of this.getRelatedCellsById(cell.id)) { 298 | relatedCell.select(true); 299 | relatedCell.mark(true); 300 | } 301 | } 302 | 303 | /** 描画を行う */ 304 | private render(): void { 305 | this.updateRelations(); 306 | 307 | // HTMLを生成する 308 | const $wrapper = this.$('
    '); 309 | this.$container.empty(); 310 | this.$container.append($wrapper); 311 | for (let i = 0; i < this.notebooks.length; i++) { 312 | $wrapper.append(this.notebooks[i].$view); 313 | if (i !== this.notebooks.length - 1) { 314 | $wrapper.append(this.relations[i].$view); 315 | } 316 | } 317 | this.$container.append('
    '); 318 | this.$container.append(this.$mergeView); 319 | 320 | // イベントを設定する 321 | this.$container.on('click', '.open-button', e => { 322 | this.$(e.target).parent().parent().removeClass('closed'); 323 | this.resetCellY(); 324 | this.updateRelationsView(); 325 | return false; 326 | }); 327 | this.$container.on('click', '.close-button', e => { 328 | this.$(e.target).parent().parent().addClass('closed'); 329 | this.resetCellY(); 330 | this.updateRelationsView(); 331 | return false; 332 | }); 333 | this.$container.on('click', '.select-button', e => { 334 | const $cell = this.$(e.target).parent().parent(); 335 | const cell = this.cellById($cell.attr('data-id')); 336 | if (cell !== null) { 337 | this.select(cell); 338 | this.alignSelected(); 339 | } 340 | return false; 341 | }); 342 | this.$container.on('click', '.cell', e => { 343 | this.showMergeView(this.$(e.currentTarget).attr('data-id')); 344 | }); 345 | this.$container.on('mouseenter', '.cell', e => { 346 | this.highlightCell(this.$(e.currentTarget).attr('data-id') || null); 347 | }); 348 | this.$container.on('mouseleave', '.cell', e => { 349 | this.highlightCell(null); 350 | }); 351 | this.$container.on('click', '.dark', e => { 352 | this.hideMergeView(); 353 | }); 354 | 355 | if (this.notebooks.length === 2) { 356 | this.updateCellsStyle(this.notebooks[1], this.relations.slice(0, 1)); 357 | } else { 358 | this.updateCellsStyle(this.notebooks[1], this.relations.slice(0, 1)); 359 | this.updateCellsStyle(this.notebooks[2], this.relations.slice(0, 2)); 360 | } 361 | 362 | setInterval(() => { 363 | this.updateRelationsView(); 364 | }); 365 | } 366 | 367 | /** 指定したNotebook内のすべてのCellのスタイルを更新する */ 368 | private updateCellsStyle(notebook: Notebook, relations: Relation[]): void { 369 | relations.reverse(); 370 | for (const cell of notebook.cellList) { 371 | const leftCellsList: Cell[][] = []; 372 | let rightCells: Cell[] = [cell]; 373 | for (const relation of relations) { 374 | const leftCells: Cell[] = []; 375 | for (const rightCell of rightCells) { 376 | for (const leftCell of relation.relatedLeftCells[rightCell.id] || 377 | []) { 378 | leftCells.push(leftCell); 379 | } 380 | } 381 | leftCellsList.push(leftCells); 382 | rightCells = leftCells; 383 | } 384 | cell.updateStyle(leftCellsList.reverse()); 385 | } 386 | } 387 | 388 | /** 指定したCellに関連するCellを関連度順にすべて取得する */ 389 | private getRelatedCellsById(cellId: string): Cell[] { 390 | const queue: string[] = [cellId]; 391 | const related: Cell[] = []; 392 | const used: { [key: string]: boolean } = {}; 393 | used[cellId] = true; 394 | while (queue.length) { 395 | const current = queue.shift() as string; 396 | for (const relation of this.relations) { 397 | for (const cell of relation.getRelatedCells(current)) { 398 | if (!used[cell.id]) { 399 | used[cell.id] = true; 400 | related.push(cell); 401 | queue.push(cell.id); 402 | } 403 | } 404 | } 405 | } 406 | return related; 407 | } 408 | } 409 | -------------------------------------------------------------------------------- /nbextension/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lc_notebook_diff_nbextension", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "lc_notebook_diff_nbextension", 9 | "version": "0.1.0", 10 | "license": "BSD-3-Clause", 11 | "dependencies": { 12 | "lc_notebook_diff_components": "file:../components" 13 | }, 14 | "devDependencies": { 15 | "esbuild": "^0.19.11", 16 | "typescript": "^5.3.3" 17 | } 18 | }, 19 | "../components": { 20 | "name": "lc_notebook_diff_components", 21 | "version": "0.1.0", 22 | "license": "BSD-3-Clause", 23 | "devDependencies": { 24 | "@types/jquery": "^3.5.29", 25 | "typescript": "^5.3.3" 26 | } 27 | }, 28 | "node_modules/@esbuild/aix-ppc64": { 29 | "version": "0.19.11", 30 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.11.tgz", 31 | "integrity": "sha512-FnzU0LyE3ySQk7UntJO4+qIiQgI7KoODnZg5xzXIrFJlKd2P2gwHsHY4927xj9y5PJmJSzULiUCWmv7iWnNa7g==", 32 | "cpu": [ 33 | "ppc64" 34 | ], 35 | "dev": true, 36 | "optional": true, 37 | "os": [ 38 | "aix" 39 | ], 40 | "engines": { 41 | "node": ">=12" 42 | } 43 | }, 44 | "node_modules/@esbuild/android-arm": { 45 | "version": "0.19.11", 46 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.11.tgz", 47 | "integrity": "sha512-5OVapq0ClabvKvQ58Bws8+wkLCV+Rxg7tUVbo9xu034Nm536QTII4YzhaFriQ7rMrorfnFKUsArD2lqKbFY4vw==", 48 | "cpu": [ 49 | "arm" 50 | ], 51 | "dev": true, 52 | "optional": true, 53 | "os": [ 54 | "android" 55 | ], 56 | "engines": { 57 | "node": ">=12" 58 | } 59 | }, 60 | "node_modules/@esbuild/android-arm64": { 61 | "version": "0.19.11", 62 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.11.tgz", 63 | "integrity": "sha512-aiu7K/5JnLj//KOnOfEZ0D90obUkRzDMyqd/wNAUQ34m4YUPVhRZpnqKV9uqDGxT7cToSDnIHsGooyIczu9T+Q==", 64 | "cpu": [ 65 | "arm64" 66 | ], 67 | "dev": true, 68 | "optional": true, 69 | "os": [ 70 | "android" 71 | ], 72 | "engines": { 73 | "node": ">=12" 74 | } 75 | }, 76 | "node_modules/@esbuild/android-x64": { 77 | "version": "0.19.11", 78 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.11.tgz", 79 | "integrity": "sha512-eccxjlfGw43WYoY9QgB82SgGgDbibcqyDTlk3l3C0jOVHKxrjdc9CTwDUQd0vkvYg5um0OH+GpxYvp39r+IPOg==", 80 | "cpu": [ 81 | "x64" 82 | ], 83 | "dev": true, 84 | "optional": true, 85 | "os": [ 86 | "android" 87 | ], 88 | "engines": { 89 | "node": ">=12" 90 | } 91 | }, 92 | "node_modules/@esbuild/darwin-arm64": { 93 | "version": "0.19.11", 94 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.11.tgz", 95 | "integrity": "sha512-ETp87DRWuSt9KdDVkqSoKoLFHYTrkyz2+65fj9nfXsaV3bMhTCjtQfw3y+um88vGRKRiF7erPrh/ZuIdLUIVxQ==", 96 | "cpu": [ 97 | "arm64" 98 | ], 99 | "dev": true, 100 | "optional": true, 101 | "os": [ 102 | "darwin" 103 | ], 104 | "engines": { 105 | "node": ">=12" 106 | } 107 | }, 108 | "node_modules/@esbuild/darwin-x64": { 109 | "version": "0.19.11", 110 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.11.tgz", 111 | "integrity": "sha512-fkFUiS6IUK9WYUO/+22omwetaSNl5/A8giXvQlcinLIjVkxwTLSktbF5f/kJMftM2MJp9+fXqZ5ezS7+SALp4g==", 112 | "cpu": [ 113 | "x64" 114 | ], 115 | "dev": true, 116 | "optional": true, 117 | "os": [ 118 | "darwin" 119 | ], 120 | "engines": { 121 | "node": ">=12" 122 | } 123 | }, 124 | "node_modules/@esbuild/freebsd-arm64": { 125 | "version": "0.19.11", 126 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.11.tgz", 127 | "integrity": "sha512-lhoSp5K6bxKRNdXUtHoNc5HhbXVCS8V0iZmDvyWvYq9S5WSfTIHU2UGjcGt7UeS6iEYp9eeymIl5mJBn0yiuxA==", 128 | "cpu": [ 129 | "arm64" 130 | ], 131 | "dev": true, 132 | "optional": true, 133 | "os": [ 134 | "freebsd" 135 | ], 136 | "engines": { 137 | "node": ">=12" 138 | } 139 | }, 140 | "node_modules/@esbuild/freebsd-x64": { 141 | "version": "0.19.11", 142 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.11.tgz", 143 | "integrity": "sha512-JkUqn44AffGXitVI6/AbQdoYAq0TEullFdqcMY/PCUZ36xJ9ZJRtQabzMA+Vi7r78+25ZIBosLTOKnUXBSi1Kw==", 144 | "cpu": [ 145 | "x64" 146 | ], 147 | "dev": true, 148 | "optional": true, 149 | "os": [ 150 | "freebsd" 151 | ], 152 | "engines": { 153 | "node": ">=12" 154 | } 155 | }, 156 | "node_modules/@esbuild/linux-arm": { 157 | "version": "0.19.11", 158 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.11.tgz", 159 | "integrity": "sha512-3CRkr9+vCV2XJbjwgzjPtO8T0SZUmRZla+UL1jw+XqHZPkPgZiyWvbDvl9rqAN8Zl7qJF0O/9ycMtjU67HN9/Q==", 160 | "cpu": [ 161 | "arm" 162 | ], 163 | "dev": true, 164 | "optional": true, 165 | "os": [ 166 | "linux" 167 | ], 168 | "engines": { 169 | "node": ">=12" 170 | } 171 | }, 172 | "node_modules/@esbuild/linux-arm64": { 173 | "version": "0.19.11", 174 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.11.tgz", 175 | "integrity": "sha512-LneLg3ypEeveBSMuoa0kwMpCGmpu8XQUh+mL8XXwoYZ6Be2qBnVtcDI5azSvh7vioMDhoJFZzp9GWp9IWpYoUg==", 176 | "cpu": [ 177 | "arm64" 178 | ], 179 | "dev": true, 180 | "optional": true, 181 | "os": [ 182 | "linux" 183 | ], 184 | "engines": { 185 | "node": ">=12" 186 | } 187 | }, 188 | "node_modules/@esbuild/linux-ia32": { 189 | "version": "0.19.11", 190 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.11.tgz", 191 | "integrity": "sha512-caHy++CsD8Bgq2V5CodbJjFPEiDPq8JJmBdeyZ8GWVQMjRD0sU548nNdwPNvKjVpamYYVL40AORekgfIubwHoA==", 192 | "cpu": [ 193 | "ia32" 194 | ], 195 | "dev": true, 196 | "optional": true, 197 | "os": [ 198 | "linux" 199 | ], 200 | "engines": { 201 | "node": ">=12" 202 | } 203 | }, 204 | "node_modules/@esbuild/linux-loong64": { 205 | "version": "0.19.11", 206 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.11.tgz", 207 | "integrity": "sha512-ppZSSLVpPrwHccvC6nQVZaSHlFsvCQyjnvirnVjbKSHuE5N24Yl8F3UwYUUR1UEPaFObGD2tSvVKbvR+uT1Nrg==", 208 | "cpu": [ 209 | "loong64" 210 | ], 211 | "dev": true, 212 | "optional": true, 213 | "os": [ 214 | "linux" 215 | ], 216 | "engines": { 217 | "node": ">=12" 218 | } 219 | }, 220 | "node_modules/@esbuild/linux-mips64el": { 221 | "version": "0.19.11", 222 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.11.tgz", 223 | "integrity": "sha512-B5x9j0OgjG+v1dF2DkH34lr+7Gmv0kzX6/V0afF41FkPMMqaQ77pH7CrhWeR22aEeHKaeZVtZ6yFwlxOKPVFyg==", 224 | "cpu": [ 225 | "mips64el" 226 | ], 227 | "dev": true, 228 | "optional": true, 229 | "os": [ 230 | "linux" 231 | ], 232 | "engines": { 233 | "node": ">=12" 234 | } 235 | }, 236 | "node_modules/@esbuild/linux-ppc64": { 237 | "version": "0.19.11", 238 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.11.tgz", 239 | "integrity": "sha512-MHrZYLeCG8vXblMetWyttkdVRjQlQUb/oMgBNurVEnhj4YWOr4G5lmBfZjHYQHHN0g6yDmCAQRR8MUHldvvRDA==", 240 | "cpu": [ 241 | "ppc64" 242 | ], 243 | "dev": true, 244 | "optional": true, 245 | "os": [ 246 | "linux" 247 | ], 248 | "engines": { 249 | "node": ">=12" 250 | } 251 | }, 252 | "node_modules/@esbuild/linux-riscv64": { 253 | "version": "0.19.11", 254 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.11.tgz", 255 | "integrity": "sha512-f3DY++t94uVg141dozDu4CCUkYW+09rWtaWfnb3bqe4w5NqmZd6nPVBm+qbz7WaHZCoqXqHz5p6CM6qv3qnSSQ==", 256 | "cpu": [ 257 | "riscv64" 258 | ], 259 | "dev": true, 260 | "optional": true, 261 | "os": [ 262 | "linux" 263 | ], 264 | "engines": { 265 | "node": ">=12" 266 | } 267 | }, 268 | "node_modules/@esbuild/linux-s390x": { 269 | "version": "0.19.11", 270 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.11.tgz", 271 | "integrity": "sha512-A5xdUoyWJHMMlcSMcPGVLzYzpcY8QP1RtYzX5/bS4dvjBGVxdhuiYyFwp7z74ocV7WDc0n1harxmpq2ePOjI0Q==", 272 | "cpu": [ 273 | "s390x" 274 | ], 275 | "dev": true, 276 | "optional": true, 277 | "os": [ 278 | "linux" 279 | ], 280 | "engines": { 281 | "node": ">=12" 282 | } 283 | }, 284 | "node_modules/@esbuild/linux-x64": { 285 | "version": "0.19.11", 286 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.11.tgz", 287 | "integrity": "sha512-grbyMlVCvJSfxFQUndw5mCtWs5LO1gUlwP4CDi4iJBbVpZcqLVT29FxgGuBJGSzyOxotFG4LoO5X+M1350zmPA==", 288 | "cpu": [ 289 | "x64" 290 | ], 291 | "dev": true, 292 | "optional": true, 293 | "os": [ 294 | "linux" 295 | ], 296 | "engines": { 297 | "node": ">=12" 298 | } 299 | }, 300 | "node_modules/@esbuild/netbsd-x64": { 301 | "version": "0.19.11", 302 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.11.tgz", 303 | "integrity": "sha512-13jvrQZJc3P230OhU8xgwUnDeuC/9egsjTkXN49b3GcS5BKvJqZn86aGM8W9pd14Kd+u7HuFBMVtrNGhh6fHEQ==", 304 | "cpu": [ 305 | "x64" 306 | ], 307 | "dev": true, 308 | "optional": true, 309 | "os": [ 310 | "netbsd" 311 | ], 312 | "engines": { 313 | "node": ">=12" 314 | } 315 | }, 316 | "node_modules/@esbuild/openbsd-x64": { 317 | "version": "0.19.11", 318 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.11.tgz", 319 | "integrity": "sha512-ysyOGZuTp6SNKPE11INDUeFVVQFrhcNDVUgSQVDzqsqX38DjhPEPATpid04LCoUr2WXhQTEZ8ct/EgJCUDpyNw==", 320 | "cpu": [ 321 | "x64" 322 | ], 323 | "dev": true, 324 | "optional": true, 325 | "os": [ 326 | "openbsd" 327 | ], 328 | "engines": { 329 | "node": ">=12" 330 | } 331 | }, 332 | "node_modules/@esbuild/sunos-x64": { 333 | "version": "0.19.11", 334 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.11.tgz", 335 | "integrity": "sha512-Hf+Sad9nVwvtxy4DXCZQqLpgmRTQqyFyhT3bZ4F2XlJCjxGmRFF0Shwn9rzhOYRB61w9VMXUkxlBy56dk9JJiQ==", 336 | "cpu": [ 337 | "x64" 338 | ], 339 | "dev": true, 340 | "optional": true, 341 | "os": [ 342 | "sunos" 343 | ], 344 | "engines": { 345 | "node": ">=12" 346 | } 347 | }, 348 | "node_modules/@esbuild/win32-arm64": { 349 | "version": "0.19.11", 350 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.11.tgz", 351 | "integrity": "sha512-0P58Sbi0LctOMOQbpEOvOL44Ne0sqbS0XWHMvvrg6NE5jQ1xguCSSw9jQeUk2lfrXYsKDdOe6K+oZiwKPilYPQ==", 352 | "cpu": [ 353 | "arm64" 354 | ], 355 | "dev": true, 356 | "optional": true, 357 | "os": [ 358 | "win32" 359 | ], 360 | "engines": { 361 | "node": ">=12" 362 | } 363 | }, 364 | "node_modules/@esbuild/win32-ia32": { 365 | "version": "0.19.11", 366 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.11.tgz", 367 | "integrity": "sha512-6YOrWS+sDJDmshdBIQU+Uoyh7pQKrdykdefC1avn76ss5c+RN6gut3LZA4E2cH5xUEp5/cA0+YxRaVtRAb0xBg==", 368 | "cpu": [ 369 | "ia32" 370 | ], 371 | "dev": true, 372 | "optional": true, 373 | "os": [ 374 | "win32" 375 | ], 376 | "engines": { 377 | "node": ">=12" 378 | } 379 | }, 380 | "node_modules/@esbuild/win32-x64": { 381 | "version": "0.19.11", 382 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.11.tgz", 383 | "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", 384 | "cpu": [ 385 | "x64" 386 | ], 387 | "dev": true, 388 | "optional": true, 389 | "os": [ 390 | "win32" 391 | ], 392 | "engines": { 393 | "node": ">=12" 394 | } 395 | }, 396 | "node_modules/esbuild": { 397 | "version": "0.19.11", 398 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.11.tgz", 399 | "integrity": "sha512-HJ96Hev2hX/6i5cDVwcqiJBBtuo9+FeIJOtZ9W1kA5M6AMJRHUZlpYZ1/SbEwtO0ioNAW8rUooVpC/WehY2SfA==", 400 | "dev": true, 401 | "hasInstallScript": true, 402 | "bin": { 403 | "esbuild": "bin/esbuild" 404 | }, 405 | "engines": { 406 | "node": ">=12" 407 | }, 408 | "optionalDependencies": { 409 | "@esbuild/aix-ppc64": "0.19.11", 410 | "@esbuild/android-arm": "0.19.11", 411 | "@esbuild/android-arm64": "0.19.11", 412 | "@esbuild/android-x64": "0.19.11", 413 | "@esbuild/darwin-arm64": "0.19.11", 414 | "@esbuild/darwin-x64": "0.19.11", 415 | "@esbuild/freebsd-arm64": "0.19.11", 416 | "@esbuild/freebsd-x64": "0.19.11", 417 | "@esbuild/linux-arm": "0.19.11", 418 | "@esbuild/linux-arm64": "0.19.11", 419 | "@esbuild/linux-ia32": "0.19.11", 420 | "@esbuild/linux-loong64": "0.19.11", 421 | "@esbuild/linux-mips64el": "0.19.11", 422 | "@esbuild/linux-ppc64": "0.19.11", 423 | "@esbuild/linux-riscv64": "0.19.11", 424 | "@esbuild/linux-s390x": "0.19.11", 425 | "@esbuild/linux-x64": "0.19.11", 426 | "@esbuild/netbsd-x64": "0.19.11", 427 | "@esbuild/openbsd-x64": "0.19.11", 428 | "@esbuild/sunos-x64": "0.19.11", 429 | "@esbuild/win32-arm64": "0.19.11", 430 | "@esbuild/win32-ia32": "0.19.11", 431 | "@esbuild/win32-x64": "0.19.11" 432 | } 433 | }, 434 | "node_modules/lc_notebook_diff_components": { 435 | "resolved": "../components", 436 | "link": true 437 | }, 438 | "node_modules/typescript": { 439 | "version": "5.3.3", 440 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", 441 | "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", 442 | "dev": true, 443 | "bin": { 444 | "tsc": "bin/tsc", 445 | "tsserver": "bin/tsserver" 446 | }, 447 | "engines": { 448 | "node": ">=14.17" 449 | } 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /html/02.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "lc_cell_meme": { 7 | "current": "fe210874-deff-11e7-9edb-0242ac110002", 8 | "history": [], 9 | "next": "fe2109dc-deff-11e7-9edb-0242ac110002", 10 | "previous": null 11 | } 12 | }, 13 | "source": [ 14 | "# About: AWS操作用ツールのインストール\n", 15 | "\n", 16 | "----\n", 17 | "\n", 18 | "AWSをJupyter Notebook環境からコントロールできるよう、ツールをインストールします。" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": { 24 | "lc_cell_meme": { 25 | "current": "fe2109dc-deff-11e7-9edb-0242ac110002", 26 | "history": [], 27 | "next": "fe210b08-deff-11e7-9edb-0242ac110002", 28 | "previous": "fe210874-deff-11e7-9edb-0242ac110002" 29 | } 30 | }, 31 | "source": [ 32 | "## *Operation Note*\n", 33 | "\n", 34 | "*This is a cell for your own recording. ここに経緯を記述*" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": { 40 | "lc_cell_meme": { 41 | "current": "fe210b08-deff-11e7-9edb-0242ac110002", 42 | "history": [], 43 | "next": "fe210c2a-deff-11e7-9edb-0242ac110002", 44 | "previous": "fe2109dc-deff-11e7-9edb-0242ac110002" 45 | } 46 | }, 47 | "source": [ 48 | "# AWS CLIのインストール\n", 49 | "\n", 50 | "Jupyter NotebookからAWSに対する操作は、AWS CLI https://aws.amazon.com/jp/cli/ を用いて実施します。\n", 51 | "\n", 52 | "ここでは、AWS CLIに必要なパッケージ(`groff`)をインストールした後、`awscli`パッケージをインストールしています。\n", 53 | "\n", 54 | "> Notebook(Python Kernel)中では、 `!` を行頭に書くことで、Shellコマンドを実行することができます。" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": { 61 | "lc_cell_meme": { 62 | "current": "fe210c2a-deff-11e7-9edb-0242ac110002", 63 | "history": [], 64 | "next": "fe210d42-deff-11e7-9edb-0242ac110002", 65 | "previous": "fe210b08-deff-11e7-9edb-0242ac110002" 66 | }, 67 | "scrolled": true 68 | }, 69 | "outputs": [], 70 | "source": [ 71 | "!sudo apt-get update && sudo apt-get install -y groff\n", 72 | "!sudo pip install awscli" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": { 78 | "lc_cell_meme": { 79 | "current": "fe210d42-deff-11e7-9edb-0242ac110002", 80 | "history": [], 81 | "next": "fe210e50-deff-11e7-9edb-0242ac110002", 82 | "previous": "fe210c2a-deff-11e7-9edb-0242ac110002" 83 | } 84 | }, 85 | "source": [ 86 | "コマンド名は `aws` です。インストールされていることを確認します。" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": { 93 | "lc_cell_meme": { 94 | "current": "fe210e50-deff-11e7-9edb-0242ac110002", 95 | "history": [], 96 | "next": "fe210f68-deff-11e7-9edb-0242ac110002", 97 | "previous": "fe210d42-deff-11e7-9edb-0242ac110002" 98 | } 99 | }, 100 | "outputs": [], 101 | "source": [ 102 | "!which aws" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": { 108 | "lc_cell_meme": { 109 | "current": "fe210f68-deff-11e7-9edb-0242ac110002", 110 | "history": [], 111 | "next": "fe211076-deff-11e7-9edb-0242ac110002", 112 | "previous": "fe210e50-deff-11e7-9edb-0242ac110002" 113 | } 114 | }, 115 | "source": [ 116 | "使い方は `help` サブコマンドで参照することができます。" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": { 123 | "lc_cell_meme": { 124 | "current": "fe211076-deff-11e7-9edb-0242ac110002", 125 | "history": [], 126 | "next": "fe211184-deff-11e7-9edb-0242ac110002", 127 | "previous": "fe210f68-deff-11e7-9edb-0242ac110002" 128 | } 129 | }, 130 | "outputs": [], 131 | "source": [ 132 | "!aws help" 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "metadata": { 138 | "lc_cell_meme": { 139 | "current": "fe211184-deff-11e7-9edb-0242ac110002", 140 | "history": [], 141 | "next": "fe211292-deff-11e7-9edb-0242ac110002", 142 | "previous": "fe211076-deff-11e7-9edb-0242ac110002" 143 | } 144 | }, 145 | "source": [ 146 | "# AWS CLIの設定\n" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": { 152 | "lc_cell_meme": { 153 | "current": "fe211292-deff-11e7-9edb-0242ac110002", 154 | "history": [], 155 | "next": "fe2113a0-deff-11e7-9edb-0242ac110002", 156 | "previous": "fe211184-deff-11e7-9edb-0242ac110002" 157 | } 158 | }, 159 | "source": [ 160 | "## Regionの設定\n", 161 | "\n", 162 | "AWS CLIに対しては、デフォルトの操作対象リージョンを設定することができます。\n", 163 | "ここでは例として、東京リージョン(ap-northeast-1)を使うことを、AWS CLIに対して設定します。" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": { 170 | "lc_cell_meme": { 171 | "current": "fe2113a0-deff-11e7-9edb-0242ac110002", 172 | "history": [], 173 | "next": "fe2114ae-deff-11e7-9edb-0242ac110002", 174 | "previous": "fe211292-deff-11e7-9edb-0242ac110002" 175 | } 176 | }, 177 | "outputs": [], 178 | "source": [ 179 | "target_region = 'ap-northeast-1'\n", 180 | "target_region" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "metadata": { 186 | "lc_cell_meme": { 187 | "current": "fe2114ae-deff-11e7-9edb-0242ac110002", 188 | "history": [], 189 | "next": "fe2115bc-deff-11e7-9edb-0242ac110002", 190 | "previous": "fe2113a0-deff-11e7-9edb-0242ac110002" 191 | } 192 | }, 193 | "source": [ 194 | "セルにはPythonコードを記述することができ、上記のように変数として各種環境に関する値を定義した上で、以下のようにその値をコマンドに与えることができます。" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": null, 200 | "metadata": { 201 | "lc_cell_meme": { 202 | "current": "fe2115bc-deff-11e7-9edb-0242ac110002", 203 | "history": [], 204 | "next": "fe2116c0-deff-11e7-9edb-0242ac110002", 205 | "previous": "fe2114ae-deff-11e7-9edb-0242ac110002" 206 | } 207 | }, 208 | "outputs": [], 209 | "source": [ 210 | "!aws configure set default.region {target_region}" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": { 216 | "lc_cell_meme": { 217 | "current": "fe2116c0-deff-11e7-9edb-0242ac110002", 218 | "history": [], 219 | "next": "fe2117ce-deff-11e7-9edb-0242ac110002", 220 | "previous": "fe2115bc-deff-11e7-9edb-0242ac110002" 221 | } 222 | }, 223 | "source": [ 224 | "## Output formatの設定\n", 225 | "\n", 226 | "Output formatは、Notebook上のPythonコードでのパースしやすさを考慮し、JSONを利用します。人間が確認するためだけなら、text, tableなどでも問題ありません。用途によって使い分けてください。" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": null, 232 | "metadata": { 233 | "lc_cell_meme": { 234 | "current": "fe2117ce-deff-11e7-9edb-0242ac110002", 235 | "history": [], 236 | "next": "fe2118e6-deff-11e7-9edb-0242ac110002", 237 | "previous": "fe2116c0-deff-11e7-9edb-0242ac110002" 238 | } 239 | }, 240 | "outputs": [], 241 | "source": [ 242 | "!aws configure set default.output json" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": { 248 | "lc_cell_meme": { 249 | "current": "fe2118e6-deff-11e7-9edb-0242ac110002", 250 | "history": [], 251 | "next": "fe2119f4-deff-11e7-9edb-0242ac110002", 252 | "previous": "fe2117ce-deff-11e7-9edb-0242ac110002" 253 | } 254 | }, 255 | "source": [ 256 | "## Credentialsの設定\n", 257 | "\n", 258 | "AWS CLIが、どのような認証情報をともなってAWS APIを呼び出すかを設定する必要があります。\n", 259 | "認証情報の設定方法は、利用される状況により異なります。" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "metadata": { 265 | "lc_cell_meme": { 266 | "current": "fe2119f4-deff-11e7-9edb-0242ac110002", 267 | "history": [], 268 | "next": "fe211b02-deff-11e7-9edb-0242ac110002", 269 | "previous": "fe2118e6-deff-11e7-9edb-0242ac110002" 270 | } 271 | }, 272 | "source": [ 273 | "### Access Key / Secret Keyを用いる場合\n", 274 | "\n", 275 | "認証情報の与え方として、AWS consoleから発行できるAccess Key, Secret Keyを用いる方法があります。\n", 276 | "コマンドに対する対話的な操作が必要ですから、JupyterのTerminal(treeページの[New] - [Terminal]から選択できます)から、 `aws configure` を実施してください。\n", 277 | "\n", 278 | "`aws configure` の実施例:\n", 279 | "\n", 280 | "```\n", 281 | "$ aws configure\n", 282 | "AWS Access Key ID [None]: (自身のアカウントのアクセスキー)\n", 283 | "AWS Secret Access Key [None]: (自身のアカウントのシークレットアクセスキー)\n", 284 | "Default region name [None]: (Enter)\n", 285 | "Default output format [None]: (Enter)\n", 286 | "```" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": { 292 | "lc_cell_meme": { 293 | "current": "fe211b02-deff-11e7-9edb-0242ac110002", 294 | "history": [], 295 | "next": "fe211c10-deff-11e7-9edb-0242ac110002", 296 | "previous": "fe2119f4-deff-11e7-9edb-0242ac110002" 297 | } 298 | }, 299 | "source": [ 300 | "### インスタンスロールを用いる場合\n", 301 | "\n", 302 | "このJupyter Notebook環境をAWS上のVMで動作させている場合、このVMにインスタンスロールを割り当てることで、AWS CLIには認証情報を与えずAWSのAPIにアクセスすることが可能です。\n" 303 | ] 304 | }, 305 | { 306 | "cell_type": "markdown", 307 | "metadata": { 308 | "lc_cell_meme": { 309 | "current": "fe211c10-deff-11e7-9edb-0242ac110002", 310 | "history": [], 311 | "next": "fe211d1e-deff-11e7-9edb-0242ac110002", 312 | "previous": "fe211b02-deff-11e7-9edb-0242ac110002" 313 | } 314 | }, 315 | "source": [ 316 | "# AWS CLIの接続確認\n", 317 | "\n", 318 | "AWS CLIに必要な設定を与えたら、試しに、アカウントの属性値取得コマンドをAWS CLIにて実施してみます。" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": { 324 | "lc_cell_meme": { 325 | "current": "fe211d1e-deff-11e7-9edb-0242ac110002", 326 | "history": [], 327 | "next": "fe211e2c-deff-11e7-9edb-0242ac110002", 328 | "previous": "fe211c10-deff-11e7-9edb-0242ac110002" 329 | } 330 | }, 331 | "source": [ 332 | "\n", 333 | "> これが失敗する場合、(以下のタブ `Out[6]`のようになります)認証情報が正しく設定されていない可能性があります。" 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": null, 339 | "metadata": { 340 | "lc_cell_meme": { 341 | "current": "fe211e2c-deff-11e7-9edb-0242ac110002", 342 | "history": [], 343 | "next": "fe211f3a-deff-11e7-9edb-0242ac110002", 344 | "previous": "fe211d1e-deff-11e7-9edb-0242ac110002" 345 | }, 346 | "pinned_outputs": [ 347 | { 348 | "execution_count": 6, 349 | "outputs": [ 350 | { 351 | "name": "stdout", 352 | "output_type": "stream", 353 | "text": "Unable to locate credentials. You can configure credentials by running \"aws configure\".\r\n" 354 | }, 355 | { 356 | "ename": "RuntimeError", 357 | "evalue": "Unexpected exit code: -127", 358 | "output_type": "error", 359 | "traceback": [ 360 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 361 | "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", 362 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msystem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'aws ec2 describe-account-attributes'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 363 | "\u001b[0;32m~/.ipython/profile_default/startup/10-custom-get_ipython_system.py\u001b[0m in \u001b[0;36m\u001b[0;34m(x)\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0msave_get_ipython_system\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msystem\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msystem\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mlambda\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mget_ipython_system\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 8\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0;31m# interactiveshell.py's system_piped() function comment saids:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 364 | "\u001b[0;32m~/.ipython/profile_default/startup/10-custom-get_ipython_system.py\u001b[0m in \u001b[0;36mget_ipython_system\u001b[0;34m(cmd)\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0msave_get_ipython_system\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcmd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0muser_ns\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'_exit_code'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 15\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Unexpected exit code: %d'\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0muser_ns\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'_exit_code'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 365 | "\u001b[0;31mRuntimeError\u001b[0m: Unexpected exit code: -127" 366 | ] 367 | } 368 | ] 369 | } 370 | ], 371 | "scrolled": true 372 | }, 373 | "outputs": [], 374 | "source": [ 375 | "!aws ec2 describe-account-attributes" 376 | ] 377 | }, 378 | { 379 | "cell_type": "markdown", 380 | "metadata": { 381 | "lc_cell_meme": { 382 | "current": "fe211f3a-deff-11e7-9edb-0242ac110002", 383 | "history": [], 384 | "next": "fe212048-deff-11e7-9edb-0242ac110002", 385 | "previous": "fe211e2c-deff-11e7-9edb-0242ac110002" 386 | } 387 | }, 388 | "source": [ 389 | "以上でJupyter Notebookから、AWS操作用ツールを実行できるようになります。" 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": null, 395 | "metadata": { 396 | "lc_cell_meme": { 397 | "current": "fe212048-deff-11e7-9edb-0242ac110002", 398 | "history": [], 399 | "next": null, 400 | "previous": "fe211f3a-deff-11e7-9edb-0242ac110002" 401 | } 402 | }, 403 | "outputs": [], 404 | "source": [] 405 | } 406 | ], 407 | "metadata": { 408 | "kernelspec": { 409 | "display_name": "Python 3 (LC_wrapper)", 410 | "language": "python", 411 | "name": "python3-wrapper" 412 | }, 413 | "language_info": { 414 | "file_extension": ".py", 415 | "mimetype": "text/x-python", 416 | "name": "python", 417 | "nbconvert_exporter": "python", 418 | "pygments_lexer": "ipython3", 419 | "version": "3.5.2" 420 | }, 421 | "lc_notebook_meme": { 422 | "current": "fe2104d2-deff-11e7-9edb-0242ac110002", 423 | "history": [], 424 | "root_cells": [ 425 | "fe210874-deff-11e7-9edb-0242ac110002", 426 | "fe2109dc-deff-11e7-9edb-0242ac110002", 427 | "fe210b08-deff-11e7-9edb-0242ac110002", 428 | "fe210c2a-deff-11e7-9edb-0242ac110002", 429 | "fe210d42-deff-11e7-9edb-0242ac110002", 430 | "fe210e50-deff-11e7-9edb-0242ac110002", 431 | "fe210f68-deff-11e7-9edb-0242ac110002", 432 | "fe211076-deff-11e7-9edb-0242ac110002", 433 | "fe211184-deff-11e7-9edb-0242ac110002", 434 | "fe211292-deff-11e7-9edb-0242ac110002", 435 | "fe2113a0-deff-11e7-9edb-0242ac110002", 436 | "fe2114ae-deff-11e7-9edb-0242ac110002", 437 | "fe2115bc-deff-11e7-9edb-0242ac110002", 438 | "fe2116c0-deff-11e7-9edb-0242ac110002", 439 | "fe2117ce-deff-11e7-9edb-0242ac110002", 440 | "fe2118e6-deff-11e7-9edb-0242ac110002", 441 | "fe2119f4-deff-11e7-9edb-0242ac110002", 442 | "fe211b02-deff-11e7-9edb-0242ac110002", 443 | "fe211c10-deff-11e7-9edb-0242ac110002", 444 | "fe211d1e-deff-11e7-9edb-0242ac110002", 445 | "fe211e2c-deff-11e7-9edb-0242ac110002", 446 | "fe211f3a-deff-11e7-9edb-0242ac110002", 447 | "fe212048-deff-11e7-9edb-0242ac110002" 448 | ] 449 | }, 450 | "toc": { 451 | "nav_menu": {}, 452 | "number_sections": true, 453 | "sideBar": true, 454 | "skip_h1_title": false, 455 | "title_cell": "Table of Contents", 456 | "title_sidebar": "Contents", 457 | "toc_cell": false, 458 | "toc_position": {}, 459 | "toc_section_display": "block", 460 | "toc_window_display": true 461 | } 462 | }, 463 | "nbformat": 4, 464 | "nbformat_minor": 2 465 | } 466 | -------------------------------------------------------------------------------- /html/03.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": { 6 | "lc_cell_meme": { 7 | "current": "fe210874-deff-11e7-9edb-0242ac110002", 8 | "history": [], 9 | "next": "fe2109dc-deff-11e7-9edb-0242ac110002", 10 | "previous": null 11 | } 12 | }, 13 | "source": [ 14 | "# About: AWS操作用ツールのインストールtest2\n", 15 | "\n", 16 | "----\n", 17 | "\n", 18 | "AWSをJupyter Notebook環境からコントロールできるよう、ツールをインストールします。" 19 | ] 20 | }, 21 | { 22 | "cell_type": "markdown", 23 | "metadata": { 24 | "lc_cell_meme": { 25 | "current": "fe2109dc-deff-11e7-9edb-0242ac110002", 26 | "history": [], 27 | "next": "fe210b08-deff-11e7-9edb-0242ac110002", 28 | "previous": "fe210874-deff-11e7-9edb-0242ac110002" 29 | } 30 | }, 31 | "source": [ 32 | "## *Operation Note*\n", 33 | "\n", 34 | "*This is a cell for your own recording. ここに経緯を記述*" 35 | ] 36 | }, 37 | { 38 | "cell_type": "markdown", 39 | "metadata": { 40 | "lc_cell_meme": { 41 | "current": "fe210b08-deff-11e7-9edb-0242ac110002", 42 | "history": [], 43 | "next": "fe210c2a-deff-11e7-9edb-0242ac110002", 44 | "previous": "fe2109dc-deff-11e7-9edb-0242ac110002" 45 | } 46 | }, 47 | "source": [ 48 | "# AWS CLIのインストール\n", 49 | "\n", 50 | "Jupyter NotebookからAWSに対する操作は、AWS CLI https://aws.amazon.com/jp/cli/ を用いて実施します。\n", 51 | "\n", 52 | "ここでは、AWS CLIに必要なパッケージ(`groff`)をインストールした後、`awscli`パッケージをインストールしています。\n", 53 | "\n", 54 | "> Notebook(Python Kernel)中では、 `!` を行頭に書くことで、Shellコマンドを実行することができます。" 55 | ] 56 | }, 57 | { 58 | "cell_type": "code", 59 | "execution_count": null, 60 | "metadata": { 61 | "lc_cell_meme": { 62 | "current": "fe210c2a-deff-11e7-9edb-0242ac110002", 63 | "history": [], 64 | "next": "fe210d42-deff-11e7-9edb-0242ac110002", 65 | "previous": "fe210b08-deff-11e7-9edb-0242ac110002" 66 | }, 67 | "scrolled": true 68 | }, 69 | "outputs": [], 70 | "source": [ 71 | "!sudo apt-get update && sudo apt-get install -y groff\n", 72 | "!sudo pip install awscli" 73 | ] 74 | }, 75 | { 76 | "cell_type": "markdown", 77 | "metadata": { 78 | "lc_cell_meme": { 79 | "current": "fe210d42-deff-11e7-9edb-0242ac110002", 80 | "history": [], 81 | "next": "fe210e50-deff-11e7-9edb-0242ac110002", 82 | "previous": "fe210c2a-deff-11e7-9edb-0242ac110002" 83 | } 84 | }, 85 | "source": [ 86 | "コマンド名は `aws` です。インストールされていることを確認します。" 87 | ] 88 | }, 89 | { 90 | "cell_type": "code", 91 | "execution_count": null, 92 | "metadata": { 93 | "lc_cell_meme": { 94 | "current": "fe210e50-deff-11e7-9edb-0242ac110002", 95 | "history": [], 96 | "next": "fe210f68-deff-11e7-9edb-0242ac110002", 97 | "previous": "fe210d42-deff-11e7-9edb-0242ac110002" 98 | } 99 | }, 100 | "outputs": [], 101 | "source": [ 102 | "!which aws" 103 | ] 104 | }, 105 | { 106 | "cell_type": "markdown", 107 | "metadata": { 108 | "lc_cell_meme": { 109 | "current": "fe210f68-deff-11e7-9edb-0242ac110002", 110 | "history": [], 111 | "next": "fe211076-deff-11e7-9edb-0242ac110002", 112 | "previous": "fe210e50-deff-11e7-9edb-0242ac110002" 113 | } 114 | }, 115 | "source": [ 116 | "使い方は `help` サブコマンドで参照することができます。" 117 | ] 118 | }, 119 | { 120 | "cell_type": "code", 121 | "execution_count": null, 122 | "metadata": { 123 | "lc_cell_meme": { 124 | "current": "fe211076-deff-11e7-9edb-0242ac110002", 125 | "history": [], 126 | "next": "fe211184-deff-11e7-9edb-0242ac110002", 127 | "previous": "fe210f68-deff-11e7-9edb-0242ac110002" 128 | } 129 | }, 130 | "outputs": [], 131 | "source": [ 132 | "!aws help" 133 | ] 134 | }, 135 | { 136 | "cell_type": "markdown", 137 | "metadata": { 138 | "lc_cell_meme": { 139 | "current": "fe211184-deff-11e7-9edb-0242ac110002", 140 | "history": [], 141 | "next": "fe211292-deff-11e7-9edb-0242ac110002", 142 | "previous": "fe211076-deff-11e7-9edb-0242ac110002" 143 | } 144 | }, 145 | "source": [ 146 | "# AWS CLIの設定\n" 147 | ] 148 | }, 149 | { 150 | "cell_type": "markdown", 151 | "metadata": { 152 | "lc_cell_meme": { 153 | "current": "fe211292-deff-11e7-9edb-0242ac110002", 154 | "history": [], 155 | "next": "fe2113a0-deff-11e7-9edb-0242ac110002", 156 | "previous": "fe211184-deff-11e7-9edb-0242ac110002" 157 | } 158 | }, 159 | "source": [ 160 | "## Regionの設定\n", 161 | "\n", 162 | "AWS CLIに対しては、デフォルトの操作対象リージョンを設定することができます。\n", 163 | "ここでは例として、東京リージョン(ap-northeast-1)を使うことを、AWS CLIに対して設定します。" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": { 170 | "lc_cell_meme": { 171 | "current": "fe2113a0-deff-11e7-9edb-0242ac110002", 172 | "history": [], 173 | "next": "fe2114ae-deff-11e7-9edb-0242ac110002", 174 | "previous": "fe211292-deff-11e7-9edb-0242ac110002" 175 | } 176 | }, 177 | "outputs": [], 178 | "source": [ 179 | "target_region = 'ap-northeast-1'\n", 180 | "target_region" 181 | ] 182 | }, 183 | { 184 | "cell_type": "markdown", 185 | "metadata": { 186 | "lc_cell_meme": { 187 | "current": "fe2114ae-deff-11e7-9edb-0242ac110002", 188 | "history": [], 189 | "next": "fe2115bc-deff-11e7-9edb-0242ac110002", 190 | "previous": "fe2113a0-deff-11e7-9edb-0242ac110002" 191 | } 192 | }, 193 | "source": [ 194 | "セルにはPythonコードを記述することができ、上記のように変数として各種環境に関する値を定義した上で、以下のようにその値をコマンドに与えることができます。" 195 | ] 196 | }, 197 | { 198 | "cell_type": "code", 199 | "execution_count": null, 200 | "metadata": { 201 | "lc_cell_meme": { 202 | "current": "fe2115bc-deff-11e7-9edb-0242ac110002", 203 | "history": [], 204 | "next": "fe2116c0-deff-11e7-9edb-0242ac110002", 205 | "previous": "fe2114ae-deff-11e7-9edb-0242ac110002" 206 | } 207 | }, 208 | "outputs": [], 209 | "source": [ 210 | "!aws configure set default.region {target_region}" 211 | ] 212 | }, 213 | { 214 | "cell_type": "markdown", 215 | "metadata": { 216 | "lc_cell_meme": { 217 | "current": "fe2116c0-deff-11e7-9edb-0242ac110002", 218 | "history": [], 219 | "next": "fe2117ce-deff-11e7-9edb-0242ac110002", 220 | "previous": "fe2115bc-deff-11e7-9edb-0242ac110002" 221 | } 222 | }, 223 | "source": [ 224 | "## Output formatの設定\n", 225 | "\n", 226 | "Output formatは、Notebook上のPythonコードでのパースしやすさを考慮し、JSONを利用します。人間が確認するためだけなら、text, tableなどでも問題ありません。用途によって使い分けてください。" 227 | ] 228 | }, 229 | { 230 | "cell_type": "code", 231 | "execution_count": null, 232 | "metadata": { 233 | "lc_cell_meme": { 234 | "current": "fe2117ce-deff-11e7-9edb-0242ac110002", 235 | "history": [], 236 | "next": "fe2118e6-deff-11e7-9edb-0242ac110002", 237 | "previous": "fe2116c0-deff-11e7-9edb-0242ac110002" 238 | } 239 | }, 240 | "outputs": [], 241 | "source": [ 242 | "!aws configure set default.output json" 243 | ] 244 | }, 245 | { 246 | "cell_type": "markdown", 247 | "metadata": { 248 | "lc_cell_meme": { 249 | "current": "fe2118e6-deff-11e7-9edb-0242ac110002", 250 | "history": [], 251 | "next": "fe2119f4-deff-11e7-9edb-0242ac110002", 252 | "previous": "fe2117ce-deff-11e7-9edb-0242ac110002" 253 | } 254 | }, 255 | "source": [ 256 | "## Credentialsの設定\n", 257 | "\n", 258 | "AWS CLIが、どのような認証情報をともなってAWS APIを呼び出すかを設定する必要があります。\n", 259 | "認証情報の設定方法は、利用される状況により異なります。" 260 | ] 261 | }, 262 | { 263 | "cell_type": "markdown", 264 | "metadata": { 265 | "lc_cell_meme": { 266 | "current": "fe2119f4-deff-11e7-9edb-0242ac110002", 267 | "history": [], 268 | "next": "fe211b02-deff-11e7-9edb-0242ac110002", 269 | "previous": "fe2118e6-deff-11e7-9edb-0242ac110002" 270 | } 271 | }, 272 | "source": [ 273 | "### Access Key / Secret Keyを用いる場合\n", 274 | "\n", 275 | "認証情報の与え方として、AWS consoleから発行できるAccess Key, Secret Keyを用いる方法があります。\n", 276 | "コマンドに対する対話的な操作が必要ですから、JupyterのTerminal(treeページの[New] - [Terminal]から選択できます)から、 `aws configure` を実施してください。\n", 277 | "\n", 278 | "`aws configure` の実施例:\n", 279 | "\n", 280 | "```\n", 281 | "$ aws configure\n", 282 | "AWS Access Key ID [None]: (自身のアカウントのアクセスキー)\n", 283 | "AWS Secret Access Key [None]: (自身のアカウントのシークレットアクセスキー)\n", 284 | "Default region name [None]: (Enter)\n", 285 | "Default output format [None]: (Enter)\n", 286 | "```" 287 | ] 288 | }, 289 | { 290 | "cell_type": "markdown", 291 | "metadata": { 292 | "lc_cell_meme": { 293 | "current": "fe211b02-deff-11e7-9edb-0242ac110002", 294 | "history": [], 295 | "next": "fe211c10-deff-11e7-9edb-0242ac110002", 296 | "previous": "fe2119f4-deff-11e7-9edb-0242ac110002" 297 | } 298 | }, 299 | "source": [ 300 | "### インスタンスロールを用いる場合\n", 301 | "\n", 302 | "このJupyter Notebook環境をAWS上のVMで動作させている場合、このVMにインスタンスロールを割り当てることで、AWS CLIには認証情報を与えずAWSのAPIにアクセスすることが可能です。\n" 303 | ] 304 | }, 305 | { 306 | "cell_type": "markdown", 307 | "metadata": { 308 | "lc_cell_meme": { 309 | "current": "fe211c10-deff-11e7-9edb-0242ac110002", 310 | "history": [], 311 | "next": "fe211d1e-deff-11e7-9edb-0242ac110002", 312 | "previous": "fe211b02-deff-11e7-9edb-0242ac110002" 313 | } 314 | }, 315 | "source": [ 316 | "# AWS CLIの接続確認\n", 317 | "\n", 318 | "AWS CLIに必要な設定を与えたら、試しに、アカウントの属性値取得コマンドをAWS CLIにて実施してみます。" 319 | ] 320 | }, 321 | { 322 | "cell_type": "markdown", 323 | "metadata": { 324 | "lc_cell_meme": { 325 | "current": "fe211d1e-deff-11e7-9edb-0242ac110002", 326 | "history": [], 327 | "next": "fe211e2c-deff-11e7-9edb-0242ac110002", 328 | "previous": "fe211c10-deff-11e7-9edb-0242ac110002" 329 | } 330 | }, 331 | "source": [ 332 | "\n", 333 | "> これが失敗する場合、(以下のタブ `Out[6]`のようになります)認証情報が正しく設定されていない可能性があります。" 334 | ] 335 | }, 336 | { 337 | "cell_type": "code", 338 | "execution_count": null, 339 | "metadata": { 340 | "lc_cell_meme": { 341 | "current": "fe211e2c-deff-11e7-9edb-0242ac110002", 342 | "history": [], 343 | "next": "fe211f3a-deff-11e7-9edb-0242ac110002", 344 | "previous": "fe211d1e-deff-11e7-9edb-0242ac110002" 345 | }, 346 | "pinned_outputs": [ 347 | { 348 | "execution_count": 6, 349 | "outputs": [ 350 | { 351 | "name": "stdout", 352 | "output_type": "stream", 353 | "text": "Unable to locate credentials. You can configure credentials by running \"aws configure\".\r\n" 354 | }, 355 | { 356 | "ename": "RuntimeError", 357 | "evalue": "Unexpected exit code: -127", 358 | "output_type": "error", 359 | "traceback": [ 360 | "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", 361 | "\u001b[0;31mRuntimeError\u001b[0m Traceback (most recent call last)", 362 | "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msystem\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'aws ec2 describe-account-attributes'\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 363 | "\u001b[0;32m~/.ipython/profile_default/startup/10-custom-get_ipython_system.py\u001b[0m in \u001b[0;36m\u001b[0;34m(x)\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 6\u001b[0m \u001b[0msave_get_ipython_system\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msystem\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 7\u001b[0;31m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msystem\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0;32mlambda\u001b[0m \u001b[0mx\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mget_ipython_system\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mx\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 8\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 9\u001b[0m \u001b[0;31m# interactiveshell.py's system_piped() function comment saids:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", 364 | "\u001b[0;32m~/.ipython/profile_default/startup/10-custom-get_ipython_system.py\u001b[0m in \u001b[0;36mget_ipython_system\u001b[0;34m(cmd)\u001b[0m\n\u001b[1;32m 13\u001b[0m \u001b[0msave_get_ipython_system\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mcmd\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 14\u001b[0m \u001b[0;32mif\u001b[0m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0muser_ns\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'_exit_code'\u001b[0m\u001b[0;34m]\u001b[0m \u001b[0;34m!=\u001b[0m \u001b[0;36m0\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 15\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m'Unexpected exit code: %d'\u001b[0m \u001b[0;34m%\u001b[0m \u001b[0mget_ipython\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0muser_ns\u001b[0m\u001b[0;34m[\u001b[0m\u001b[0;34m'_exit_code'\u001b[0m\u001b[0;34m]\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", 365 | "\u001b[0;31mRuntimeError\u001b[0m: Unexpected exit code: -127" 366 | ] 367 | } 368 | ] 369 | } 370 | ], 371 | "scrolled": true 372 | }, 373 | "outputs": [], 374 | "source": [ 375 | "!aws ec2 describe-account-attributes" 376 | ] 377 | }, 378 | { 379 | "cell_type": "markdown", 380 | "metadata": { 381 | "lc_cell_meme": { 382 | "current": "fe211f3a-deff-11e7-9edb-0242ac110002", 383 | "history": [], 384 | "next": "fe212048-deff-11e7-9edb-0242ac110002", 385 | "previous": "fe211e2c-deff-11e7-9edb-0242ac110002" 386 | } 387 | }, 388 | "source": [ 389 | "以上でJupyter Notebookから、AWS操作用ツールを実行できるようになります。" 390 | ] 391 | }, 392 | { 393 | "cell_type": "code", 394 | "execution_count": null, 395 | "metadata": { 396 | "lc_cell_meme": { 397 | "current": "fe212048-deff-11e7-9edb-0242ac110002", 398 | "history": [], 399 | "next": null, 400 | "previous": "fe211f3a-deff-11e7-9edb-0242ac110002" 401 | } 402 | }, 403 | "outputs": [], 404 | "source": [] 405 | } 406 | ], 407 | "metadata": { 408 | "kernelspec": { 409 | "display_name": "Python 3 (LC_wrapper)", 410 | "language": "python", 411 | "name": "python3-wrapper" 412 | }, 413 | "language_info": { 414 | "file_extension": ".py", 415 | "mimetype": "text/x-python", 416 | "name": "python", 417 | "nbconvert_exporter": "python", 418 | "pygments_lexer": "ipython3", 419 | "version": "3.5.2" 420 | }, 421 | "lc_notebook_meme": { 422 | "current": "fe2104d2-deff-11e7-9edb-0242ac110002", 423 | "history": [], 424 | "root_cells": [ 425 | "fe210874-deff-11e7-9edb-0242ac110002", 426 | "fe2109dc-deff-11e7-9edb-0242ac110002", 427 | "fe210b08-deff-11e7-9edb-0242ac110002", 428 | "fe210c2a-deff-11e7-9edb-0242ac110002", 429 | "fe210d42-deff-11e7-9edb-0242ac110002", 430 | "fe210e50-deff-11e7-9edb-0242ac110002", 431 | "fe210f68-deff-11e7-9edb-0242ac110002", 432 | "fe211076-deff-11e7-9edb-0242ac110002", 433 | "fe211184-deff-11e7-9edb-0242ac110002", 434 | "fe211292-deff-11e7-9edb-0242ac110002", 435 | "fe2113a0-deff-11e7-9edb-0242ac110002", 436 | "fe2114ae-deff-11e7-9edb-0242ac110002", 437 | "fe2115bc-deff-11e7-9edb-0242ac110002", 438 | "fe2116c0-deff-11e7-9edb-0242ac110002", 439 | "fe2117ce-deff-11e7-9edb-0242ac110002", 440 | "fe2118e6-deff-11e7-9edb-0242ac110002", 441 | "fe2119f4-deff-11e7-9edb-0242ac110002", 442 | "fe211b02-deff-11e7-9edb-0242ac110002", 443 | "fe211c10-deff-11e7-9edb-0242ac110002", 444 | "fe211d1e-deff-11e7-9edb-0242ac110002", 445 | "fe211e2c-deff-11e7-9edb-0242ac110002", 446 | "fe211f3a-deff-11e7-9edb-0242ac110002", 447 | "fe212048-deff-11e7-9edb-0242ac110002" 448 | ] 449 | }, 450 | "toc": { 451 | "nav_menu": {}, 452 | "number_sections": true, 453 | "sideBar": true, 454 | "skip_h1_title": false, 455 | "title_cell": "Table of Contents", 456 | "title_sidebar": "Contents", 457 | "toc_cell": false, 458 | "toc_position": {}, 459 | "toc_section_display": "block", 460 | "toc_window_display": true 461 | } 462 | }, 463 | "nbformat": 4, 464 | "nbformat_minor": 2 465 | } 466 | --------------------------------------------------------------------------------