├── .dockerignore
├── .gitattributes
├── .github
└── workflows
│ ├── backend_ci.yml
│ └── frontend_ci.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── alembic.ini
├── alembic
├── env.py
├── script.py.mako
└── versions
│ └── 675e708a78c0_initial_migration.py
├── docker-compose.yml
├── docs
├── notebooks_import_navigate.gif
├── pr_notebook_diff.png
└── python_file_diff.png
├── package-lock.json
├── package.json
├── public
├── css
│ └── notebook.css
├── img
│ ├── favicon.ico
│ ├── icon.png
│ ├── icon_blue.png
│ └── icon_bluelight.png
└── index.html
├── requirements.txt
├── run.py
├── setup.cfg
└── src
├── __init__.py
├── backend
├── __init__.py
├── api
│ ├── __init__.py
│ ├── code_repositories.py
│ └── notebooks.py
├── api_service.py
├── config.py
├── domain
│ └── code_comment.py
└── model
│ ├── code_repository.py
│ └── notebook.py
└── frontend
├── App.vue
├── components
├── Loader.vue
├── NavBar.vue
├── SubNav.vue
└── codeComment.js
├── main.js
├── router.js
├── shared
├── credentialManager.js
├── getAPIUrl.js
└── userCredentials.js
├── store.js
└── views
├── Home.vue
├── Login.vue
├── PullRequest.vue
├── PullRequests.vue
└── notebooks
├── ImportNotebooks.vue
├── Notebook.vue
└── Notebooks.vue
/.dockerignore:
--------------------------------------------------------------------------------
1 | venv
2 | node_modules
3 | README.md
4 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | *.vue linguist-detectable=false
2 |
3 | # Set the default behavior, in case people don't have core.autocrlf set.
4 | * text=auto
5 |
--------------------------------------------------------------------------------
/.github/workflows/backend_ci.yml:
--------------------------------------------------------------------------------
1 | name: Backend CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 | strategy:
10 | max-parallel: 2
11 | matrix:
12 | python-version: [3.8]
13 |
14 | steps:
15 | - uses: actions/checkout@v1
16 | - name: Set up Python ${{ matrix.python-version }}
17 | uses: actions/setup-python@v1
18 | with:
19 | python-version: ${{ matrix.python-version }}
20 | - name: Install dependencies
21 | run: |
22 | python -m pip install --upgrade pip
23 | pip install -r requirements.txt
24 | - name: Lint with pylint
25 | run: |
26 | pip install pylint
27 | pylint -E src/backend
28 |
--------------------------------------------------------------------------------
/.github/workflows/frontend_ci.yml:
--------------------------------------------------------------------------------
1 | name: Frontend CI
2 |
3 | on: [push]
4 |
5 | jobs:
6 | build:
7 |
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - uses: actions/checkout@v1
12 | - name: Use Node.js 12.x
13 | uses: actions/setup-node@v1
14 | with:
15 | node-version: 12.x
16 | - name: npm install, lint, and build
17 | run: |
18 | npm install
19 | npm run lint
20 | npm run build
21 | env:
22 | CI: true
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | __pycache__
3 | .idea
4 | .vscode
5 | venv
6 | node_modules
7 | src/backend/static/
8 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nikolaik/python-nodejs:python3.8-nodejs16
2 |
3 | LABEL org.opencontainers.image.authors="rsn_4_91@hotmail.com"
4 |
5 | WORKDIR /app
6 |
7 | COPY . /app
8 |
9 | RUN pip install -r requirements.txt
10 | RUN npm install
11 | RUN npm run build
12 |
13 | ENTRYPOINT [ "python" ]
14 |
15 | CMD ["run.py" ]
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Ricardo Neves
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Koopera
2 | =========
3 | 
4 | 
5 | 
6 |
7 |
12 |
13 | **Koopera** is a collaboration app that enables Data Science teams to share and review their jupyter notebooks.
14 |
15 | # Features
16 | * Review jupyter notebooks by looking at the diffs and commenting directly on the cells
17 | * Review source code with syntax highlight for python, scala, etc. (using [diff2html](https://github.com/rtfpessoa/diff2html))
18 | * Import notebooks from your GitHub repositories
19 | * Navigate through your notebooks
20 | * GitHub authentication via personal access token
21 |
22 | # Screenshots
23 |
24 | **Review jupyter notebooks**
25 |
26 | 
27 |
28 | **Review python code**
29 |
30 | 
31 |
32 | **Import and navigate through your notebooks**
33 |
34 | 
35 |
36 | # Quickstart
37 | You can easily start using Koopera with docker in just two steps.
38 |
39 | 1. Build and start Koopera + dependencies:
40 |
41 | `docker-compose up --build -d`
42 |
43 | 1. Run migrations:
44 |
45 | `docker-compose run --rm koopera alembic upgrade head`
46 |
47 | When the migration ends, you can now access Koopera at: [http://0.0.0.0:8080](http://0.0.0.0:8080)
48 |
49 |
50 | # Setting up dev environment
51 |
52 | ## Dependencies
53 | * Python 3.6
54 | * nodejs 16
55 |
56 | ## Setting up backend for local development
57 |
58 | 1. Enable CORS by setting `ALLOW_CORS` to `True` in [src/backend/config.py](src/backend/config.py)
59 |
60 | 2. Create virtualenv
61 |
62 | `python -m venv venv`
63 |
64 | 3. Activate virtualenv
65 |
66 | `source venv/bin/activate`
67 |
68 | 4. Install dependencies
69 |
70 | `pip install -r requirements.txt`
71 |
72 | 5. Run migrations
73 | `alembic upgrade head`
74 |
75 | 6. Start flask app on default port (5000)
76 |
77 | `python run.py`
78 |
79 | ## Setting up frontend for local development
80 |
81 | 1. Install dependencies
82 |
83 | `npm install`
84 | 2. Run web server for serving frontend
85 |
86 | `npm run serve`
87 |
88 | A web server for serving frontend with hot reload will be listening on
89 | [http://localhost:8080](http://localhost:8080)
90 |
91 | # Credits
92 |
93 |
94 |
--------------------------------------------------------------------------------
/alembic.ini:
--------------------------------------------------------------------------------
1 | # A generic, single database configuration.
2 |
3 | [alembic]
4 | # path to migration scripts
5 | script_location = alembic
6 |
7 | # template used to generate migration files
8 | # file_template = %%(rev)s_%%(slug)s
9 |
10 | # timezone to use when rendering the date
11 | # within the migration file as well as the filename.
12 | # string value is passed to dateutil.tz.gettz()
13 | # leave blank for localtime
14 | # timezone =
15 |
16 | # max length of characters to apply to the
17 | # "slug" field
18 | # truncate_slug_length = 40
19 |
20 | # set to 'true' to run the environment during
21 | # the 'revision' command, regardless of autogenerate
22 | # revision_environment = false
23 |
24 | # set to 'true' to allow .pyc and .pyo files without
25 | # a source .py file to be detected as revisions in the
26 | # versions/ directory
27 | # sourceless = false
28 |
29 | # version location specification; this defaults
30 | # to alembic/versions. When using multiple version
31 | # directories, initial revisions must be specified with --version-path
32 | # version_locations = %(here)s/bar %(here)s/bat alembic/versions
33 |
34 | # the output encoding used when revision files
35 | # are written from script.py.mako
36 | # output_encoding = utf-8
37 |
38 | # sqlalchemy.url = driver://user:pass@localhost/dbname
39 |
40 |
41 | [post_write_hooks]
42 | # post_write_hooks defines scripts or Python functions that are run
43 | # on newly generated revision scripts. See the documentation for further
44 | # detail and examples
45 |
46 | # format using "black" - use the console_scripts runner, against the "black" entrypoint
47 | # hooks=black
48 | # black.type=console_scripts
49 | # black.entrypoint=black
50 | # black.options=-l 79
51 |
52 | # Logging configuration
53 | [loggers]
54 | keys = root,sqlalchemy,alembic
55 |
56 | [handlers]
57 | keys = console
58 |
59 | [formatters]
60 | keys = generic
61 |
62 | [logger_root]
63 | level = WARN
64 | handlers = console
65 | qualname =
66 |
67 | [logger_sqlalchemy]
68 | level = WARN
69 | handlers =
70 | qualname = sqlalchemy.engine
71 |
72 | [logger_alembic]
73 | level = INFO
74 | handlers =
75 | qualname = alembic
76 |
77 | [handler_console]
78 | class = StreamHandler
79 | args = (sys.stderr,)
80 | level = NOTSET
81 | formatter = generic
82 |
83 | [formatter_generic]
84 | format = %(levelname)-5.5s [%(name)s] %(message)s
85 | datefmt = %H:%M:%S
86 |
--------------------------------------------------------------------------------
/alembic/env.py:
--------------------------------------------------------------------------------
1 | import os
2 | import sys
3 | from logging.config import fileConfig
4 |
5 | from alembic import context
6 | from sqlalchemy import engine_from_config, pool
7 |
8 | sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
9 |
10 | from src.backend.config import DATABASE_URI
11 |
12 | from src.backend.model.code_repository import CodeRepositoryBase
13 | from src.backend.model.notebook import NotebookBase
14 |
15 |
16 | # this is the Alembic Config object, which provides
17 | # access to the values within the .ini file in use.
18 | config = context.config
19 |
20 | # Interpret the config file for Python logging.
21 | # This line sets up loggers basically.
22 | fileConfig(config.config_file_name)
23 | config.set_main_option('sqlalchemy.url', DATABASE_URI)
24 |
25 | # add your model's MetaData object here
26 | # for 'autogenerate' support
27 | # from myapp import mymodel
28 | # target_metadata = mymodel.Base.metadata
29 | target_metadata = [CodeRepositoryBase.metadata, NotebookBase.metadata]
30 |
31 | # other values from the config, defined by the needs of env.py,
32 | # can be acquired:
33 | # my_important_option = config.get_main_option("my_important_option")
34 | # ... etc.
35 |
36 |
37 | def run_migrations_offline():
38 | """Run migrations in 'offline' mode.
39 |
40 | This configures the context with just a URL
41 | and not an Engine, though an Engine is acceptable
42 | here as well. By skipping the Engine creation
43 | we don't even need a DBAPI to be available.
44 |
45 | Calls to context.execute() here emit the given string to the
46 | script output.
47 |
48 | """
49 | url = config.get_main_option("sqlalchemy.url")
50 | context.configure(
51 | url=url,
52 | target_metadata=target_metadata,
53 | literal_binds=True,
54 | dialect_opts={"paramstyle": "named"},
55 | )
56 |
57 | with context.begin_transaction():
58 | context.run_migrations()
59 |
60 |
61 | def run_migrations_online():
62 | """Run migrations in 'online' mode.
63 |
64 | In this scenario we need to create an Engine
65 | and associate a connection with the context.
66 |
67 | """
68 | connectable = engine_from_config(
69 | config.get_section(config.config_ini_section),
70 | prefix="sqlalchemy.",
71 | poolclass=pool.NullPool,
72 | )
73 |
74 | with connectable.connect() as connection:
75 | context.configure(
76 | connection=connection, target_metadata=target_metadata
77 | )
78 |
79 | with context.begin_transaction():
80 | context.run_migrations()
81 |
82 |
83 | if context.is_offline_mode():
84 | run_migrations_offline()
85 | else:
86 | run_migrations_online()
87 |
--------------------------------------------------------------------------------
/alembic/script.py.mako:
--------------------------------------------------------------------------------
1 | """${message}
2 |
3 | Revision ID: ${up_revision}
4 | Revises: ${down_revision | comma,n}
5 | Create Date: ${create_date}
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 | ${imports if imports else ""}
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = ${repr(up_revision)}
14 | down_revision = ${repr(down_revision)}
15 | branch_labels = ${repr(branch_labels)}
16 | depends_on = ${repr(depends_on)}
17 |
18 |
19 | def upgrade():
20 | ${upgrades if upgrades else "pass"}
21 |
22 |
23 | def downgrade():
24 | ${downgrades if downgrades else "pass"}
25 |
--------------------------------------------------------------------------------
/alembic/versions/675e708a78c0_initial_migration.py:
--------------------------------------------------------------------------------
1 | """Initial migration
2 |
3 | Revision ID: 675e708a78c0
4 | Revises:
5 | Create Date: 2020-01-03 22:05:19.343989
6 |
7 | """
8 | from alembic import op
9 | import sqlalchemy as sa
10 |
11 |
12 | # revision identifiers, used by Alembic.
13 | revision = '675e708a78c0'
14 | down_revision = None
15 | branch_labels = None
16 | depends_on = None
17 |
18 |
19 | def upgrade():
20 | # ### commands auto generated by Alembic - please adjust! ###
21 | op.create_table('code_repositories',
22 | sa.Column('id', sa.Integer(), nullable=False),
23 | sa.Column('name', sa.String(), nullable=True),
24 | sa.Column('owner', sa.String(), nullable=True),
25 | sa.PrimaryKeyConstraint('id')
26 | )
27 | op.create_table('notebooks',
28 | sa.Column('id', sa.Integer(), nullable=False),
29 | sa.Column('code_repo_id', sa.Integer(), nullable=True),
30 | sa.Column('sha', sa.String(), nullable=True),
31 | sa.Column('path', sa.String(), nullable=True),
32 | sa.Column('title', sa.String(), nullable=True),
33 | sa.Column('summary', sa.String(), nullable=True),
34 | sa.ForeignKeyConstraint(['code_repo_id'], ['code_repositories.id'], ),
35 | sa.PrimaryKeyConstraint('id')
36 | )
37 | # ### end Alembic commands ###
38 |
39 |
40 | def downgrade():
41 | # ### commands auto generated by Alembic - please adjust! ###
42 | op.drop_table('notebooks')
43 | op.drop_table('code_repositories')
44 | # ### end Alembic commands ###
45 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | koopera:
5 | image: koopera
6 | build: .
7 | links:
8 | - postgres:postgres
9 | ports:
10 | - "5000:5000"
11 | environment:
12 | - DATABASE_URI=postgres+psycopg2://postgres:password@postgres:5432/koopera
13 | postgres:
14 | image: postgres:12.0-alpine
15 | volumes:
16 | - postgres_data:/var/lib/postgresql/data/
17 | environment:
18 | - POSTGRES_USER=postgres
19 | - POSTGRES_PASSWORD=password
20 | - POSTGRES_DB=koopera
21 | expose:
22 | - "5432"
23 |
24 | volumes:
25 | postgres_data:
26 |
--------------------------------------------------------------------------------
/docs/notebooks_import_navigate.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/docs/notebooks_import_navigate.gif
--------------------------------------------------------------------------------
/docs/pr_notebook_diff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/docs/pr_notebook_diff.png
--------------------------------------------------------------------------------
/docs/python_file_diff.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/docs/python_file_diff.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "serve": "vue-cli-service serve",
7 | "build": "vue-cli-service build",
8 | "lint": "vue-cli-service lint"
9 | },
10 | "dependencies": {
11 | "bootstrap": "^5.2.1",
12 | "diff2html": "^3.4.19",
13 | "htmldiff-js": "^1.0.5",
14 | "vue": "^2.6.12",
15 | "vue-router": "^3.5.1"
16 | },
17 | "devDependencies": {
18 | "@babel/core": "^7.19.1",
19 | "@babel/eslint-parser": "^7.19.1",
20 | "@vue/cli-plugin-babel": "^5.0.8",
21 | "@vue/cli-plugin-eslint": "^5.0.8",
22 | "@vue/cli-service": "^5.0.8",
23 | "eslint": "^8.23.1",
24 | "eslint-plugin-vue": "^9.5.1",
25 | "vue-template-compiler": "^2.7.10"
26 | },
27 | "eslintConfig": {
28 | "root": true,
29 | "env": {
30 | "node": true
31 | },
32 | "extends": [
33 | "plugin:vue/essential"
34 | ],
35 | "rules": {
36 | "vue/multi-word-component-names": "off",
37 | "no-underscore-dangle": "off",
38 | "comma-dangle": "off",
39 | "no-alert": "off",
40 | "linebreak-style": "off"
41 | },
42 | "parserOptions": {
43 | "parser": "@babel/eslint-parser"
44 | }
45 | },
46 | "eslintIgnore": [
47 | "src/backend/static"
48 | ],
49 | "postcss": {
50 | "plugins": {
51 | "autoprefixer": {}
52 | }
53 | },
54 | "browserslist": [
55 | "> 1%",
56 | "last 2 versions"
57 | ],
58 | "babel": {
59 | "presets": [
60 | "@vue/app"
61 | ]
62 | },
63 | "vue": {
64 | "outputDir": "src/backend/static",
65 | "pages": {
66 | "index": {
67 | "entry": "src/frontend/main.js"
68 | }
69 | }
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/public/css/notebook.css:
--------------------------------------------------------------------------------
1 | .koopera-nb .img-responsive,.koopera-nb .thumbnail>img,.koopera-nb .thumbnail a>img,.koopera-nb .carousel-inner>.item>img,.koopera-nb .carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.koopera-nb .img-rounded{border-radius:3px}.koopera-nb .img-thumbnail{padding:4px;line-height:1.42857143;background-color:#fff;border:1px solid #ddd;border-radius:2px;-webkit-transition:all .2s ease-in-out;-o-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.koopera-nb .img-circle{border-radius:50%}.koopera-nb hr{margin-top:18px;margin-bottom:18px;border:0;border-top:1px solid #eee}.koopera-nb .sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);border:0}.koopera-nb .sr-only-focusable:active,.koopera-nb .sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto}[role="button"]{cursor:pointer}.koopera-nb h1,.koopera-nb h2,.koopera-nb h3,.koopera-nb h4,.koopera-nb h5,.koopera-nb h6,.koopera-nb .h1,.koopera-nb .h2,.koopera-nb .h3,.koopera-nb .h4,.koopera-nb .h5,.koopera-nb .h6{font-family:inherit;font-weight:500;line-height:1.1;color:inherit}.koopera-nb h1 small,.koopera-nb h2 small,.koopera-nb h3 small,.koopera-nb h4 small,.koopera-nb h5 small,.koopera-nb h6 small,.koopera-nb .h1 small,.koopera-nb .h2 small,.koopera-nb .h3 small,.koopera-nb .h4 small,.koopera-nb .h5 small,.koopera-nb .h6 small,.koopera-nb h1 .small,.koopera-nb h2 .small,.koopera-nb h3 .small,.koopera-nb h4 .small,.koopera-nb h5 .small,.koopera-nb h6 .small,.koopera-nb .h1 .small,.koopera-nb .h2 .small,.koopera-nb .h3 .small,.koopera-nb .h4 .small,.koopera-nb .h5 .small,.koopera-nb .h6 .small{font-weight:400;line-height:1;color:#777}.koopera-nb h1,.koopera-nb .h1,.koopera-nb h2,.koopera-nb .h2,.koopera-nb h3,.koopera-nb .h3{margin-top:18px;margin-bottom:9px}.koopera-nb h1 small,.koopera-nb .h1 small,.koopera-nb h2 small,.koopera-nb .h2 small,.koopera-nb h3 small,.koopera-nb .h3 small,.koopera-nb h1 .small,.koopera-nb .h1 .small,.koopera-nb h2 .small,.koopera-nb .h2 .small,.koopera-nb h3 .small,.koopera-nb .h3 .small{font-size:65%}.koopera-nb h4,.koopera-nb .h4,.koopera-nb h5,.koopera-nb .h5,.koopera-nb h6,.koopera-nb .h6{margin-top:9px;margin-bottom:9px}.koopera-nb h4 small,.koopera-nb .h4 small,.koopera-nb h5 small,.koopera-nb .h5 small,.koopera-nb h6 small,.koopera-nb .h6 small,.koopera-nb h4 .small,.koopera-nb .h4 .small,.koopera-nb h5 .small,.koopera-nb .h5 .small,.koopera-nb h6 .small,.koopera-nb .h6 .small{font-size:75%}.koopera-nb h1,.koopera-nb .h1{font-size:33px}.koopera-nb h2,.koopera-nb .h2{font-size:27px}.koopera-nb h3,.koopera-nb .h3{font-size:23px}.koopera-nb h4,.koopera-nb .h4{font-size:17px}.koopera-nb h5,.koopera-nb .h5{font-size:13px}.koopera-nb h6,.koopera-nb .h6{font-size:12px}.koopera-nb p{margin:0 0 9px}.koopera-nb .lead{margin-bottom:18px;font-size:14px;font-weight:300;line-height:1.4}@media (min-width:768px){.lead{font-size:19.5px}}.koopera-nb small,.koopera-nb .small{font-size:92%}.koopera-nb mark,.koopera-nb .mark{background-color:#fcf8e3;padding:.2em}.koopera-nb .text-left{text-align:left}.koopera-nb .text-right{text-align:right}.koopera-nb .text-center{text-align:center}.koopera-nb .text-justify{text-align:justify}.koopera-nb .text-nowrap{white-space:nowrap}.koopera-nb .text-lowercase{text-transform:lowercase}.koopera-nb .text-uppercase{text-transform:uppercase}.koopera-nb .text-capitalize{text-transform:capitalize}.koopera-nb .text-muted{color:#777}.koopera-nb .text-primary{color:#337ab7}.koopera-nb a.text-primary:hover,.koopera-nb a.text-primary:focus{color:#286090}.koopera-nb .text-success{color:#3c763d}.koopera-nb a.text-success:hover,.koopera-nb a.text-success:focus{color:#2b542c}.koopera-nb .text-info{color:#31708f}.koopera-nb a.text-info:hover,.koopera-nb a.text-info:focus{color:#245269}.koopera-nb .text-warning{color:#8a6d3b}.koopera-nb a.text-warning:hover,.koopera-nb a.text-warning:focus{color:#66512c}.koopera-nb .text-danger{color:#a94442}.koopera-nb a.text-danger:hover,.koopera-nb a.text-danger:focus{color:#843534}.koopera-nb .bg-primary{color:#fff;background-color:#337ab7}.koopera-nb a.bg-primary:hover,.koopera-nb a.bg-primary:focus{background-color:#286090}.koopera-nb .bg-success{background-color:#dff0d8}.koopera-nb a.bg-success:hover,.koopera-nb a.bg-success:focus{background-color:#c1e2b3}.koopera-nb .bg-info{background-color:#d9edf7}.koopera-nb a.bg-info:hover,.koopera-nb a.bg-info:focus{background-color:#afd9ee}.koopera-nb .bg-warning{background-color:#fcf8e3}.koopera-nb a.bg-warning:hover,.koopera-nb a.bg-warning:focus{background-color:#f7ecb5}.koopera-nb .bg-danger{background-color:#f2dede}.koopera-nb a.bg-danger:hover,.koopera-nb a.bg-danger:focus{background-color:#e4b9b9}.koopera-nb .page-header{padding-bottom:8px;margin:36px 0 18px;border-bottom:1px solid #eee}.koopera-nb ul,.koopera-nb ol{margin-top:0;margin-bottom:9px}.koopera-nb ul ul,.koopera-nb ol ul,.koopera-nb ul ol,.koopera-nb ol ol{margin-bottom:0}.koopera-nb .list-unstyled{padding-left:0;list-style:none}.koopera-nb .list-inline{padding-left:0;list-style:none;margin-left:-5px}.koopera-nb .list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}.koopera-nb dl{margin-top:0;margin-bottom:18px}.koopera-nb dt,.koopera-nb dd{line-height:1.42857143}.koopera-nb dt{font-weight:700}.koopera-nb dd{margin-left:0}@media (min-width:541px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}.koopera-nb abbr[title],.koopera-nb abbr[data-original-title]{cursor:help;border-bottom:1px dotted #777}.koopera-nb .initialism{font-size:90%;text-transform:uppercase}.koopera-nb blockquote{padding:9px 18px;margin:0 0 18px;font-size:inherit;border-left:5px solid #eee}.koopera-nb blockquote p:last-child,.koopera-nb blockquote ul:last-child,.koopera-nb blockquote ol:last-child{margin-bottom:0}.koopera-nb blockquote footer,.koopera-nb blockquote small,.koopera-nb blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#777}.koopera-nb blockquote footer:before,.koopera-nb blockquote small:before,.koopera-nb blockquote .small:before{content:\'\\2014 \\00A0\'}.koopera-nb .blockquote-reverse,.koopera-nb blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #eee;border-left:0;text-align:right}.koopera-nb .blockquote-reverse footer:before,.koopera-nb blockquote.pull-right footer:before,.koopera-nb .blockquote-reverse small:before,.koopera-nb blockquote.pull-right small:before,.koopera-nb .blockquote-reverse .small:before,.koopera-nb blockquote.pull-right .small:before{content:\'\'}.koopera-nb .blockquote-reverse footer:after,.koopera-nb blockquote.pull-right footer:after,.koopera-nb .blockquote-reverse small:after,.koopera-nb blockquote.pull-right small:after,.koopera-nb .blockquote-reverse .small:after,.koopera-nb blockquote.pull-right .small:after{content:\'\\00A0 \\2014\'}.koopera-nb address{margin-bottom:18px;font-style:normal;line-height:1.42857143}.koopera-nb code,.koopera-nb kbd,.koopera-nb pre,.koopera-nb samp{font-family:monospace}.koopera-nb code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;border-radius:2px}.koopera-nb kbd{padding:2px 4px;font-size:90%;color:#888;background-color:transparent;border-radius:1px;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}.koopera-nb kbd kbd{padding:0;font-size:100%;font-weight:700;box-shadow:none}.koopera-nb pre{display:block;padding:8.5px;margin:0 0 9px;font-size:12px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333;background-color:#f5f5f5;border:1px solid #ccc;border-radius:2px}.koopera-nb pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.koopera-nb .pre-scrollable{max-height:340px;overflow-y:scroll}@media (min-width: 768px){.container{width:768px}}@media (min-width: 992px){.container{width:940px}}@media (min-width: 1200px){.container{width:1140px}}.koopera-nb .container-fluid{margin-right:auto;margin-left:auto;padding-left:0;padding-right:0}.koopera-nb .row{margin-left:0;margin-right:0}.koopera-nb .koopera-nb .col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:0;padding-right:0}.koopera-nb .koopera-nb .col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.koopera-nb .col-xs-12{width:100%}.koopera-nb .col-xs-11{width:91.66666667%}.koopera-nb .col-xs-10{width:83.33333333%}.koopera-nb .col-xs-9{width:75%}.koopera-nb .col-xs-8{width:66.66666667%}.koopera-nb .col-xs-7{width:58.33333333%}.koopera-nb .col-xs-6{width:50%}.koopera-nb .col-xs-5{width:41.66666667%}.koopera-nb .col-xs-4{width:33.33333333%}.koopera-nb .col-xs-3{width:25%}.koopera-nb .col-xs-2{width:16.66666667%}.koopera-nb .col-xs-1{width:8.33333333%}.koopera-nb .col-xs-pull-12{right:100%}.koopera-nb .col-xs-pull-11{right:91.66666667%}.koopera-nb .col-xs-pull-10{right:83.33333333%}.koopera-nb .col-xs-pull-9{right:75%}.koopera-nb .col-xs-pull-8{right:66.66666667%}.koopera-nb .col-xs-pull-7{right:58.33333333%}.koopera-nb .col-xs-pull-6{right:50%}.koopera-nb .col-xs-pull-5{right:41.66666667%}.koopera-nb .col-xs-pull-4{right:33.33333333%}.koopera-nb .col-xs-pull-3{right:25%}.koopera-nb .col-xs-pull-2{right:16.66666667%}.koopera-nb .col-xs-pull-1{right:8.33333333%}.koopera-nb .col-xs-pull-0{right:auto}.koopera-nb .col-xs-push-12{left:100%}.koopera-nb .col-xs-push-11{left:91.66666667%}.koopera-nb .col-xs-push-10{left:83.33333333%}.koopera-nb .col-xs-push-9{left:75%}.koopera-nb .col-xs-push-8{left:66.66666667%}.koopera-nb .col-xs-push-7{left:58.33333333%}.koopera-nb .col-xs-push-6{left:50%}.koopera-nb .col-xs-push-5{left:41.66666667%}.koopera-nb .col-xs-push-4{left:33.33333333%}.koopera-nb .col-xs-push-3{left:25%}.koopera-nb .col-xs-push-2{left:16.66666667%}.koopera-nb .col-xs-push-1{left:8.33333333%}.koopera-nb .col-xs-push-0{left:auto}.koopera-nb .col-xs-offset-12{margin-left:100%}.koopera-nb .col-xs-offset-11{margin-left:91.66666667%}.koopera-nb .col-xs-offset-10{margin-left:83.33333333%}.koopera-nb .col-xs-offset-9{margin-left:75%}.koopera-nb .col-xs-offset-8{margin-left:66.66666667%}.koopera-nb .col-xs-offset-7{margin-left:58.33333333%}.koopera-nb .col-xs-offset-6{margin-left:50%}.koopera-nb .col-xs-offset-5{margin-left:41.66666667%}.koopera-nb .col-xs-offset-4{margin-left:33.33333333%}.koopera-nb .col-xs-offset-3{margin-left:25%}.koopera-nb .col-xs-offset-2{margin-left:16.66666667%}.koopera-nb .col-xs-offset-1{margin-left:8.33333333%}.koopera-nb .col-xs-offset-0{margin-left:0}@media (min-width: 768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:auto}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:auto}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0}}@media (min-width: 992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:auto}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:auto}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0}}@media (min-width: 1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:auto}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:auto}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0}}.koopera-nb table{background-color:transparent}.koopera-nb caption{padding-top:8px;padding-bottom:8px;color:#777;text-align:left}.koopera-nb th{text-align:left}.koopera-nb .table{width:100%;max-width:100%;margin-bottom:18px}.koopera-nb .table > thead > tr > th,.koopera-nb .table > tbody > tr > th,.koopera-nb .table > tfoot > tr > th,.koopera-nb .table > thead > tr > td,.koopera-nb .table > tbody > tr > td,.koopera-nb .table > tfoot > tr > td{padding:8px;line-height:1.42857143;vertical-align:top;border-top:1px solid #ddd}.koopera-nb .table > thead > tr > th{vertical-align:bottom;border-bottom:2px solid #ddd}.koopera-nb .table > caption + thead > tr:first-child > th,.koopera-nb .table > colgroup + thead > tr:first-child > th,.koopera-nb .table > thead:first-child > tr:first-child > th,.koopera-nb .table > caption + thead > tr:first-child > td,.koopera-nb .table > colgroup + thead > tr:first-child > td,.koopera-nb .table > thead:first-child > tr:first-child > td{border-top:0}.koopera-nb .table > tbody + tbody{border-top:2px solid #ddd}.koopera-nb .table .table{background-color:#fff}.koopera-nb .table-condensed > thead > tr > th,.koopera-nb .table-condensed > tbody > tr > th,.koopera-nb .table-condensed > tfoot > tr > th,.koopera-nb .table-condensed > thead > tr > td,.koopera-nb .table-condensed > tbody > tr > td,.koopera-nb .table-condensed > tfoot > tr > td{padding:5px}.koopera-nb .table-bordered{border:1px solid #ddd}.koopera-nb .table-bordered > thead > tr > th,.koopera-nb .table-bordered > tbody > tr > th,.koopera-nb .table-bordered > tfoot > tr > th,.koopera-nb .table-bordered > thead > tr > td,.koopera-nb .table-bordered > tbody > tr > td,.koopera-nb .table-bordered > tfoot > tr > td{border:1px solid #ddd}.koopera-nb .table-bordered > thead > tr > th,.koopera-nb .table-bordered > thead > tr > td{border-bottom-width:2px}.koopera-nb .table-striped > tbody > tr:nth-of-type(odd){background-color:#f9f9f9}.koopera-nb .table-hover > tbody > tr:hover{background-color:#f5f5f5}.koopera-nb table col[class*="col-"]{position:static;float:none;display:table-column}.koopera-nb table td[class*="col-"],.koopera-nb table th[class*="col-"]{position:static;float:none;display:table-cell}.koopera-nb .table > thead > tr > td.active,.koopera-nb .table > tbody > tr > td.active,.koopera-nb .table > tfoot > tr > td.active,.koopera-nb .table > thead > tr > th.active,.koopera-nb .table > tbody > tr > th.active,.koopera-nb .table > tfoot > tr > th.active,.koopera-nb .table > thead > tr.active > td,.koopera-nb .table > tbody > tr.active > td,.koopera-nb .table > tfoot > tr.active > td,.koopera-nb .table > thead > tr.active > th,.koopera-nb .table > tbody > tr.active > th,.koopera-nb .table > tfoot > tr.active > th{background-color:#f5f5f5}.koopera-nb .table-hover > tbody > tr > td.active:hover,.koopera-nb .table-hover > tbody > tr > th.active:hover,.koopera-nb .table-hover > tbody > tr.active:hover > td,.koopera-nb .table-hover > tbody > tr:hover > .active,.koopera-nb .table-hover > tbody > tr.active:hover > th{background-color:#e8e8e8}.koopera-nb .table > thead > tr > td.success,.koopera-nb .table > tbody > tr > td.success,.koopera-nb .table > tfoot > tr > td.success,.koopera-nb .table > thead > tr > th.success,.koopera-nb .table > tbody > tr > th.success,.koopera-nb .table > tfoot > tr > th.success,.koopera-nb .table > thead > tr.success > td,.koopera-nb .table > tbody > tr.success > td,.koopera-nb .table > tfoot > tr.success > td,.koopera-nb .table > thead > tr.success > th,.koopera-nb .table > tbody > tr.success > th,.koopera-nb .table > tfoot > tr.success > th{background-color:#dff0d8}.koopera-nb .table-hover > tbody > tr > td.success:hover,.koopera-nb .table-hover > tbody > tr > th.success:hover,.koopera-nb .table-hover > tbody > tr.success:hover > td,.koopera-nb .table-hover > tbody > tr:hover > .success,.koopera-nb .table-hover > tbody > tr.success:hover > th{background-color:#d0e9c6}.koopera-nb .table > thead > tr > td.info,.koopera-nb .table > tbody > tr > td.info,.koopera-nb .table > tfoot > tr > td.info,.koopera-nb .table > thead > tr > th.info,.koopera-nb .table > tbody > tr > th.info,.koopera-nb .table > tfoot > tr > th.info,.koopera-nb .table > thead > tr.info > td,.koopera-nb .table > tbody > tr.info > td,.koopera-nb .table > tfoot > tr.info > td,.koopera-nb .table > thead > tr.info > th,.koopera-nb .table > tbody > tr.info > th,.koopera-nb .table > tfoot > tr.info > th{background-color:#d9edf7}.koopera-nb .table-hover > tbody > tr > td.info:hover,.koopera-nb .table-hover > tbody > tr > th.info:hover,.koopera-nb .table-hover > tbody > tr.info:hover > td,.koopera-nb .table-hover > tbody > tr:hover > .info,.koopera-nb .table-hover > tbody > tr.info:hover > th{background-color:#c4e3f3}.koopera-nb .command_mode .modal_indicator:before.fa-pull-left{margin-right:.3em}.koopera-nb .command_mode .modal_indicator:before.fa-pull-right{margin-left:.3em}.koopera-nb .command_mode .modal_indicator:before.pull-left{margin-right:.3em}.koopera-nb .command_mode .modal_indicator:before.pull-right{margin-left:.3em}.koopera-nb .kernel_idle_icon:before{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\\f10c"}.koopera-nb .kernel_idle_icon:before.fa-pull-left{margin-right:.3em}.koopera-nb .kernel_idle_icon:before.fa-pull-right{margin-left:.3em}.koopera-nb .kernel_idle_icon:before.pull-left{margin-right:.3em}.koopera-nb .kernel_idle_icon:before.pull-right{margin-left:.3em}.koopera-nb .kernel_busy_icon:before{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\\f111"}.koopera-nb .kernel_busy_icon:before.fa-pull-left{margin-right:.3em}.koopera-nb .kernel_busy_icon:before.fa-pull-right{margin-left:.3em}.koopera-nb .kernel_busy_icon:before.pull-left{margin-right:.3em}.koopera-nb .kernel_busy_icon:before.pull-right{margin-left:.3em}.koopera-nb .kernel_dead_icon:before{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\\f1e2"}.koopera-nb .kernel_dead_icon:before.fa-pull-left{margin-right:.3em}.koopera-nb .kernel_dead_icon:before.fa-pull-right{margin-left:.3em}.koopera-nb .kernel_dead_icon:before.pull-left{margin-right:.3em}.koopera-nb .kernel_dead_icon:before.pull-right{margin-left:.3em}.koopera-nb .kernel_disconnected_icon:before{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;content:"\\f127"}.koopera-nb .kernel_disconnected_icon:before.fa-pull-left{margin-right:.3em}.koopera-nb .kernel_disconnected_icon:before.fa-pull-right{margin-left:.3em}.koopera-nb .kernel_disconnected_icon:before.pull-left{margin-right:.3em}.koopera-nb .kernel_disconnected_icon:before.pull-right{margin-left:.3em}.koopera-nb .notification_widget{color:#777;z-index:10;background:rgba(240,240,240,0.5);margin-right:4px;color:#333;background-color:#fff;border-color:#ccc}.koopera-nb .notification_widget:focus,.koopera-nb .notification_widget.focus{color:#333;background-color:#e6e6e6;border-color:#8c8c8c}.koopera-nb .notification_widget:hover{color:#333;background-color:#e6e6e6;border-color:#adadad}.koopera-nb .notification_widget:active,.koopera-nb .notification_widget.active,.koopera-nb .open > .dropdown-toggle.notification_widget{color:#333;background-color:#e6e6e6;border-color:#adadad}.koopera-nb .notification_widget:active:hover,.koopera-nb .notification_widget.active:hover,.koopera-nb .open > .dropdown-toggle.notification_widget:hover,.koopera-nb .notification_widget:active:focus,.koopera-nb .notification_widget.active:focus,.koopera-nb .open > .dropdown-toggle.notification_widget:focus,.koopera-nb .notification_widget:active.focus,.koopera-nb .notification_widget.active.focus,.koopera-nb .open > .dropdown-toggle.notification_widget.focus{color:#333;background-color:#d4d4d4;border-color:#8c8c8c}.koopera-nb .notification_widget:active,.koopera-nb .notification_widget.active,.koopera-nb .open > .dropdown-toggle.notification_widget{background-image:none}.koopera-nb .notification_widget.disabled:hover,.koopera-nb .notification_widget[disabled]:hover,.koopera-nb fieldset[disabled] .notification_widget:hover,.koopera-nb .notification_widget.disabled:focus,.koopera-nb .notification_widget[disabled]:focus,.koopera-nb fieldset[disabled] .notification_widget:focus,.koopera-nb .notification_widget.disabled.focus,.koopera-nb .notification_widget[disabled].focus,.koopera-nb fieldset[disabled] .notification_widget.focus{background-color:#fff;border-color:#ccc}.koopera-nb .notification_widget .badge{color:#fff;background-color:#333}.koopera-nb .notification_widget.warning{color:#fff;background-color:#f0ad4e;border-color:#eea236}.koopera-nb .notification_widget.warning:focus,.koopera-nb .notification_widget.warning.focus{color:#fff;background-color:#ec971f;border-color:#985f0d}.koopera-nb .notification_widget.warning:hover{color:#fff;background-color:#ec971f;border-color:#d58512}.koopera-nb .notification_widget.warning:active,.koopera-nb .notification_widget.warning.active,.koopera-nb .open > .dropdown-toggle.notification_widget.warning{color:#fff;background-color:#ec971f;border-color:#d58512}.koopera-nb .notification_widget.warning:active:hover,.koopera-nb .notification_widget.warning.active:hover,.koopera-nb .open > .dropdown-toggle.notification_widget.warning:hover,.koopera-nb .notification_widget.warning:active:focus,.koopera-nb .notification_widget.warning.active:focus,.koopera-nb .open > .dropdown-toggle.notification_widget.warning:focus,.koopera-nb .notification_widget.warning:active.focus,.koopera-nb .notification_widget.warning.active.focus,.koopera-nb .open > .dropdown-toggle.notification_widget.warning.focus{color:#fff;background-color:#d58512;border-color:#985f0d}.koopera-nb .notification_widget.warning:active,.koopera-nb .notification_widget.warning.active,.koopera-nb .open > .dropdown-toggle.notification_widget.warning{background-image:none}.koopera-nb .notification_widget.warning.disabled:hover,.koopera-nb .notification_widget.warning[disabled]:hover,.koopera-nb fieldset[disabled] .notification_widget.warning:hover,.koopera-nb .notification_widget.warning.disabled:focus,.koopera-nb .notification_widget.warning[disabled]:focus,.koopera-nb fieldset[disabled] .notification_widget.warning:focus,.koopera-nb .notification_widget.warning.disabled.focus,.koopera-nb .notification_widget.warning[disabled].focus,.koopera-nb fieldset[disabled] .notification_widget.warning.focus{background-color:#f0ad4e;border-color:#eea236}.koopera-nb .notification_widget.warning .badge{color:#f0ad4e;background-color:#fff}.koopera-nb .notification_widget.success{color:#fff;background-color:#5cb85c;border-color:#4cae4c}.koopera-nb .notification_widget.success:focus,.koopera-nb .notification_widget.success.focus{color:#fff;background-color:#449d44;border-color:#255625}.koopera-nb .notification_widget.success:hover{color:#fff;background-color:#449d44;border-color:#398439}.koopera-nb .notification_widget.success:active,.koopera-nb .notification_widget.success.active,.koopera-nb .open > .dropdown-toggle.notification_widget.success{color:#fff;background-color:#449d44;border-color:#398439}.koopera-nb .notification_widget.success:active:hover,.koopera-nb .notification_widget.success.active:hover,.koopera-nb .open > .dropdown-toggle.notification_widget.success:hover,.koopera-nb .notification_widget.success:active:focus,.koopera-nb .notification_widget.success.active:focus,.koopera-nb .open > .dropdown-toggle.notification_widget.success:focus,.koopera-nb .notification_widget.success:active.focus,.koopera-nb .notification_widget.success.active.focus,.koopera-nb .open > .dropdown-toggle.notification_widget.success.focus{color:#fff;background-color:#398439;border-color:#255625}.koopera-nb .notification_widget.success:active,.koopera-nb .notification_widget.success.active,.koopera-nb .open > .dropdown-toggle.notification_widget.success{background-image:none}.koopera-nb .notification_widget.success.disabled:hover,.koopera-nb .notification_widget.success[disabled]:hover,.koopera-nb fieldset[disabled] .notification_widget.success:hover,.koopera-nb .notification_widget.success.disabled:focus,.koopera-nb .notification_widget.success[disabled]:focus,.koopera-nb fieldset[disabled] .notification_widget.success:focus,.koopera-nb .notification_widget.success.disabled.focus,.koopera-nb .notification_widget.success[disabled].focus,.koopera-nb fieldset[disabled] .notification_widget.success.focus{background-color:#5cb85c;border-color:#4cae4c}.koopera-nb .notification_widget.success .badge{color:#5cb85c;background-color:#fff}.koopera-nb .notification_widget.info{color:#fff;background-color:#5bc0de;border-color:#46b8da}.koopera-nb .notification_widget.info:focus,.koopera-nb .notification_widget.info.focus{color:#fff;background-color:#31b0d5;border-color:#1b6d85}.koopera-nb .notification_widget.info:hover{color:#fff;background-color:#31b0d5;border-color:#269abc}.koopera-nb .notification_widget.info:active,.koopera-nb .notification_widget.info.active,.koopera-nb .open > .dropdown-toggle.notification_widget.info{color:#fff;background-color:#31b0d5;border-color:#269abc}.koopera-nb .notification_widget.info:active:hover,.koopera-nb .notification_widget.info.active:hover,.koopera-nb .open > .dropdown-toggle.notification_widget.info:hover,.koopera-nb .notification_widget.info:active:focus,.koopera-nb .notification_widget.info.active:focus,.koopera-nb .open > .dropdown-toggle.notification_widget.info:focus,.koopera-nb .notification_widget.info:active.focus,.koopera-nb .notification_widget.info.active.focus,.koopera-nb .open > .dropdown-toggle.notification_widget.info.focus{color:#fff;background-color:#269abc;border-color:#1b6d85}.koopera-nb .notification_widget.info:active,.koopera-nb .notification_widget.info.active,.koopera-nb .open > .dropdown-toggle.notification_widget.info{background-image:none}.koopera-nb .notification_widget.info.disabled:hover,.koopera-nb .notification_widget.info[disabled]:hover,.koopera-nb fieldset[disabled] .notification_widget.info:hover,.koopera-nb .notification_widget.info.disabled:focus,.koopera-nb .notification_widget.info[disabled]:focus,.koopera-nb fieldset[disabled] .notification_widget.info:focus,.koopera-nb .notification_widget.info.disabled.focus,.koopera-nb .notification_widget.info[disabled].focus,.koopera-nb fieldset[disabled] .notification_widget.info.focus{background-color:#5bc0de;border-color:#46b8da}.koopera-nb .notification_widget.info .badge{color:#5bc0de;background-color:#fff}.koopera-nb .notification_widget.danger{color:#fff;background-color:#d9534f;border-color:#d43f3a}.koopera-nb .notification_widget.danger:focus,.koopera-nb .notification_widget.danger.focus{color:#fff;background-color:#c9302c;border-color:#761c19}.koopera-nb .notification_widget.danger:hover{color:#fff;background-color:#c9302c;border-color:#ac2925}.koopera-nb .notification_widget.danger:active,.koopera-nb .notification_widget.danger.active,.koopera-nb .open > .dropdown-toggle.notification_widget.danger{color:#fff;background-color:#c9302c;border-color:#ac2925}.koopera-nb .notification_widget.danger:active:hover,.koopera-nb .notification_widget.danger.active:hover,.koopera-nb .open > .dropdown-toggle.notification_widget.danger:hover,.koopera-nb .notification_widget.danger:active:focus,.koopera-nb .notification_widget.danger.active:focus,.koopera-nb .open > .dropdown-toggle.notification_widget.danger:focus,.koopera-nb .notification_widget.danger:active.focus,.koopera-nb .notification_widget.danger.active.focus,.koopera-nb .open > .dropdown-toggle.notification_widget.danger.focus{color:#fff;background-color:#ac2925;border-color:#761c19}.koopera-nb .notification_widget.danger:active,.koopera-nb .notification_widget.danger.active,.koopera-nb .open > .dropdown-toggle.notification_widget.danger{background-image:none}.koopera-nb .notification_widget.danger.disabled:hover,.koopera-nb .notification_widget.danger[disabled]:hover,.koopera-nb fieldset[disabled] .notification_widget.danger:hover,.koopera-nb .notification_widget.danger.disabled:focus,.koopera-nb .notification_widget.danger[disabled]:focus,.koopera-nb fieldset[disabled] .notification_widget.danger:focus,.koopera-nb .notification_widget.danger.disabled.focus,.koopera-nb .notification_widget.danger[disabled].focus,.koopera-nb fieldset[disabled] .notification_widget.danger.focus{background-color:#d9534f;border-color:#d43f3a}.koopera-nb .notification_widget.danger .badge{color:#d9534f;background-color:#fff}.koopera-nb div#pager{background-color:#fff;font-size:14px;line-height:20px;overflow:hidden;display:none;position:fixed;bottom:0;width:100%;max-height:50%;padding-top:8px;-webkit-box-shadow:0 0 12px 1px rgba(87,87,87,0.2);box-shadow:0 0 12px 1px rgba(87,87,87,0.2);z-index:100;top:auto!important}.koopera-nb div#pager pre{line-height:1.21429em;color:#000;background-color:#f7f7f7;padding:.4em}.koopera-nb div#pager #pager-button-area{position:absolute;top:8px;right:20px}.koopera-nb div#pager #pager-contents{position:relative;overflow:auto;width:100%;height:100%}.koopera-nb div#pager #pager-contents #pager-container{position:relative;padding:15px 0;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.koopera-nb div#pager .ui-resizable-handle{top:0;height:8px;background:#f7f7f7;border-top:1px solid #cfcfcf;border-bottom:1px solid #cfcfcf}.koopera-nb div#pager .ui-resizable-handle::after{content:\'\';top:2px;left:50%;height:3px;width:30px;margin-left:-15px;position:absolute;border-top:1px solid #cfcfcf}.koopera-nb .quickhelp{display:-webkit-box;-webkit-box-orient:horizontal;-webkit-box-align:stretch;display:-moz-box;-moz-box-orient:horizontal;-moz-box-align:stretch;display:box;box-orient:horizontal;box-align:stretch;display:flex;flex-direction:row;align-items:stretch;line-height:1.8em}.koopera-nb .shortcut_key{display:inline-block;width:21ex;text-align:right;font-family:monospace}.koopera-nb .shortcut_descr{display:inline-block;-webkit-box-flex:1;-moz-box-flex:1;box-flex:1;flex:1}.koopera-nb span.save_widget{height:30px;margin-top:4px;display:flex;justify-content:flex-start;align-items:baseline;width:50%;flex:1}.koopera-nb span.save_widget span.filename{height:100%;line-height:1em;margin-left:16px;border:none;font-size:146.5%;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;border-radius:2px}.koopera-nb span.save_widget span.filename:hover{background-color:#e6e6e6}[dir="rtl"] span.save_widget.pull-left{float:right!important;float:right}[dir="rtl"] span.save_widget span.filename{margin-left:0;margin-right:16px}.koopera-nb span.checkpoint_status,.koopera-nb span.autosave_status{font-size:small;white-space:nowrap;padding:0 5px}@media (max-width: 767px){span.save_widget{font-size:small;padding:0 0 0 5px}span.checkpoint_status,span.autosave_status{display:none}}@media (min-width: 768px) and (max-width: 991px){span.checkpoint_status{display:none}span.autosave_status{font-size:x-small}}.koopera-nb .toolbar{padding:0;margin-left:-5px;margin-top:2px;margin-bottom:5px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box}.koopera-nb .toolbar select,.koopera-nb .toolbar label{width:auto;vertical-align:middle;margin-right:2px;margin-bottom:0;display:inline;font-size:92%;margin-left:.3em;margin-right:.3em;padding:0;padding-top:3px}.koopera-nb .toolbar .btn{padding:2px 8px}.koopera-nb .toolbar .btn-group{margin-top:0;margin-left:5px}.koopera-nb .toolbar-btn-label{margin-left:6px}#maintoolbar{margin-bottom:-3px;margin-top:-8px;border:0;min-height:27px;margin-left:0;padding-top:11px;padding-bottom:3px}#maintoolbar .navbar-text{float:none;vertical-align:middle;text-align:right;margin-left:5px;margin-right:0;margin-top:0}.koopera-nb .select-xs{height:24px}[dir="rtl"] .btn-group > .btn,.koopera-nb .btn-group-vertical > .btn{float:right}.koopera-nb .pulse,.koopera-nb .dropdown-menu > li > a.pulse,.koopera-nb li.pulse > a.dropdown-toggle,.koopera-nb li.pulse.open > a.dropdown-toggle{background-color:#F37626;color:#fff}@-moz-keyframes fadeOut{from{opacity:1}to{opacity:0}}@-webkit-keyframes fadeOut{from{opacity:1}to{opacity:0}}@-moz-keyframes fadeIn{from{opacity:0}to{opacity:1}}@-webkit-keyframes fadeIn{from{opacity:0}to{opacity:1}}.koopera-nb .bigtooltip{overflow:auto;height:200px;-webkit-transition-property:height;-webkit-transition-duration:500ms;-moz-transition-property:height;-moz-transition-duration:500ms;transition-property:height;transition-duration:500ms}.koopera-nb .smalltooltip{-webkit-transition-property:height;-webkit-transition-duration:500ms;-moz-transition-property:height;-moz-transition-duration:500ms;transition-property:height;transition-duration:500ms;text-overflow:ellipsis;overflow:hidden;height:80px}.koopera-nb .tooltipbuttons{position:absolute;padding-right:15px;top:0;right:0}.koopera-nb .tooltiptext{padding-right:30px}.koopera-nb .ipython_tooltip{max-width:700px;-webkit-animation:fadeOut 400ms;-moz-animation:fadeOut 400ms;animation:fadeOut 400ms;-webkit-animation:fadeIn 400ms;-moz-animation:fadeIn 400ms;animation:fadeIn 400ms;vertical-align:middle;background-color:#f7f7f7;overflow:visible;border:#ababab 1px solid;outline:none;padding:3px;margin:0;padding-left:7px;font-family:monospace;min-height:50px;-moz-box-shadow:0 6px 10px -1px #adadad;-webkit-box-shadow:0 6px 10px -1px #adadad;box-shadow:0 6px 10px -1px #adadad;border-radius:2px;position:absolute;z-index:1000}.koopera-nb .ipython_tooltip a{float:right}.koopera-nb .ipython_tooltip .tooltiptext pre{border:0;border-radius:0;font-size:100%;background-color:#f7f7f7}.koopera-nb .pretooltiparrow{left:0;margin:0;top:-16px;width:40px;height:16px;overflow:hidden;position:absolute}.koopera-nb .pretooltiparrow:before{background-color:#f7f7f7;border:1px #ababab solid;z-index:11;content:"";position:absolute;left:15px;top:10px;width:25px;height:25px;-webkit-transform:rotate(45deg);-moz-transform:rotate(45deg);-ms-transform:rotate(45deg);-o-transform:rotate(45deg)}.koopera-nb ul.typeahead-list i{margin-left:-10px;width:18px}[dir="rtl"] ul.typeahead-list i{margin-left:0;margin-right:-10px}.koopera-nb ul.typeahead-list{max-height:80vh;overflow:auto}.koopera-nb ul.typeahead-list > li > a{white-space:normal}.koopera-nb ul.typeahead-list > li > a.pull-right{float:left!important;float:left}[dir="rtl"] .typeahead-list{text-align:right}.koopera-nb .cmd-palette .modal-body{padding:7px}.koopera-nb .cmd-palette form{background:#fff}.koopera-nb .cmd-palette input{outline:none}.koopera-nb .no-shortcut{min-width:20px;color:transparent}[dir="rtl"] .no-shortcut.pull-right{float:left!important;float:left}[dir="rtl"] .command-shortcut.pull-right{float:left!important;float:left}.koopera-nb .command-shortcut:before{content:"(command mode)";padding-right:3px;color:#777}.koopera-nb .edit-shortcut:before{content:"(edit)";padding-right:3px;color:#777}[dir="rtl"] .edit-shortcut.pull-right{float:left!important;float:left}#find-and-replace #replace-preview .match,#find-and-replace #replace-preview .insert{background-color:#BBDEFB;border-color:#90CAF9;border-style:solid;border-width:1px;border-radius:0}[dir="ltr"] #find-and-replace .input-group-btn + .form-control{border-left:none}[dir="rtl"] #find-and-replace .input-group-btn + .form-control{border-right:none}#find-and-replace #replace-preview .replace .match{background-color:#FFCDD2;border-color:#EF9A9A;border-radius:0}#find-and-replace #replace-preview .replace .insert{background-color:#C8E6C9;border-color:#A5D6A7;border-radius:0}#find-and-replace #replace-preview{max-height:60vh;overflow:auto}#find-and-replace #replace-preview pre{padding:5px 10px}.koopera-nb .terminal-app{background:#EEE}.koopera-nb .terminal-app #header{background:#fff;-webkit-box-shadow:0 0 12px 1px rgba(87,87,87,0.2);box-shadow:0 0 12px 1px rgba(87,87,87,0.2)}.koopera-nb .terminal-app .terminal{width:100%;float:left;font-family:monospace;color:#fff;background:#000;padding:.4em;border-radius:2px;-webkit-box-shadow:0 0 12px 1px rgba(87,87,87,0.4);box-shadow:0 0 12px 1px rgba(87,87,87,0.4)}.koopera-nb .terminal-app .terminal,.koopera-nb .terminal-app .terminal dummy-screen{line-height:1em;font-size:14px}.koopera-nb .terminal-app .terminal .xterm-rows{padding:10px}.koopera-nb .terminal-app .terminal-cursor{color:#000;background:#fff}.koopera-nb .terminal-app #terminado-container{margin-top:20px}.koopera-nb .highlight .hll{background-color:#ffc}.koopera-nb .highlight{background:#f8f8f8}.koopera-nb .highlight .c{color:#408080;font-style:italic}.koopera-nb .highlight .err{border:1px solid red}.koopera-nb .highlight .k{color:green;font-weight:700}.koopera-nb .highlight .o{color:#666}.koopera-nb .highlight .ch{color:#408080;font-style:italic}.koopera-nb .highlight .cm{color:#408080;font-style:italic}.koopera-nb .highlight .cp{color:#BC7A00}.koopera-nb .highlight .cpf{color:#408080;font-style:italic}.koopera-nb .highlight .c1{color:#408080;font-style:italic}.koopera-nb .highlight .cs{color:#408080;font-style:italic}.koopera-nb .highlight .gd{color:#A00000}.koopera-nb .highlight .ge{font-style:italic}.koopera-nb .highlight .gr{color:red}.koopera-nb .highlight .gh{color:navy;font-weight:700}.koopera-nb .highlight .gi{color:#00A000}.koopera-nb .highlight .go{color:#888}.koopera-nb .highlight .gp{color:navy;font-weight:700}.koopera-nb .highlight .gs{font-weight:700}.koopera-nb .highlight .gu{color:purple;font-weight:700}.koopera-nb .highlight .gt{color:#04D}.koopera-nb .highlight .kc{color:green;font-weight:700}.koopera-nb .highlight .kd{color:green;font-weight:700}.koopera-nb .highlight .kn{color:green;font-weight:700}.koopera-nb .highlight .kp{color:green}.koopera-nb .highlight .kr{color:green;font-weight:700}.koopera-nb .highlight .kt{color:#B00040}.koopera-nb .highlight .m{color:#666}.koopera-nb .highlight .s{color:#BA2121}.koopera-nb .highlight .na{color:#7D9029}.koopera-nb .highlight .nb{color:green}.koopera-nb .highlight .nc{color:#00F;font-weight:700}.koopera-nb .highlight .no{color:#800}.koopera-nb .highlight .nd{color:#A2F}.koopera-nb .highlight .ni{color:#999;font-weight:700}.koopera-nb .highlight .ne{color:#D2413A;font-weight:700}.koopera-nb .highlight .nf{color:#00F}.koopera-nb .highlight .nl{color:#A0A000}.koopera-nb .highlight .nn{color:#00F;font-weight:700}.koopera-nb .highlight .nt{color:green;font-weight:700}.koopera-nb .highlight .nv{color:#19177C}.koopera-nb .highlight .ow{color:#A2F;font-weight:700}.koopera-nb .highlight .w{color:#bbb}.koopera-nb .highlight .mb{color:#666}.koopera-nb .highlight .mf{color:#666}.koopera-nb .highlight .mh{color:#666}.koopera-nb .highlight .mi{color:#666}.koopera-nb .highlight .mo{color:#666}.koopera-nb .highlight .sa{color:#BA2121}.koopera-nb .highlight .sb{color:#BA2121}.koopera-nb .highlight .sc{color:#BA2121}.koopera-nb .highlight .dl{color:#BA2121}.koopera-nb .highlight .sd{color:#BA2121;font-style:italic}.koopera-nb .highlight .s2{color:#BA2121}.koopera-nb .highlight .se{color:#B62;font-weight:700}.koopera-nb .highlight .sh{color:#BA2121}.koopera-nb .highlight .si{color:#B68;font-weight:700}.koopera-nb .highlight .sx{color:green}.koopera-nb .highlight .sr{color:#B68}.koopera-nb .highlight .s1{color:#BA2121}.koopera-nb .highlight .ss{color:#19177C}.koopera-nb .highlight .bp{color:green}.koopera-nb .highlight .fm{color:#00F}.koopera-nb .highlight .vc{color:#19177C}.koopera-nb .highlight .vg{color:#19177C}.koopera-nb .highlight .vi{color:#19177C}.koopera-nb .highlight .vm{color:#19177C}.koopera-nb .highlight .il{color:#666}
2 |
--------------------------------------------------------------------------------
/public/img/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/public/img/favicon.ico
--------------------------------------------------------------------------------
/public/img/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/public/img/icon.png
--------------------------------------------------------------------------------
/public/img/icon_blue.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/public/img/icon_blue.png
--------------------------------------------------------------------------------
/public/img/icon_bluelight.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/public/img/icon_bluelight.png
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | Koopera
18 |
19 |
20 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask==1.1.1
2 | nbconvert==5.6.0
3 | nbformat==4.4.0
4 | pygments==2.9.0
5 | PyGithub==1.55
6 | Flask-Cors==3.0.10
7 | flask-jwt-extended==4.2.0
8 | SQLAlchemy==1.3.12
9 | alembic==1.3.2
10 | psycopg2-binary==2.8.4
11 | markdown==2.6.11
12 | py-gfm==0.1.4
13 |
--------------------------------------------------------------------------------
/run.py:
--------------------------------------------------------------------------------
1 | from src.backend.api_service import APIService
2 |
3 | app = APIService.get()
4 |
5 | if __name__ == '__main__':
6 | app.run(host='0.0.0.0')
7 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [yapf]
2 | based_on_style = google
3 | ALLOW_SPLIT_BEFORE_DICT_VALUE = False
4 |
--------------------------------------------------------------------------------
/src/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/src/__init__.py
--------------------------------------------------------------------------------
/src/backend/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/src/backend/__init__.py
--------------------------------------------------------------------------------
/src/backend/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rsn491/koopera/7760786a024388575f85366a951993114e123e99/src/backend/api/__init__.py
--------------------------------------------------------------------------------
/src/backend/api/code_repositories.py:
--------------------------------------------------------------------------------
1 | import base64
2 | from datetime import datetime
3 | from typing import Iterable
4 |
5 | import markdown
6 | import nbformat
7 | from flask import Blueprint, jsonify, request
8 | from flask_jwt_extended import get_jwt_identity, jwt_required
9 | from github import Github, GithubException, Repository
10 | from mdx_gfm import GithubFlavoredMarkdownExtension
11 | from nbconvert import HTMLExporter
12 | from pygments import highlight
13 | from pygments.formatters.html import HtmlFormatter
14 | from pygments.lexers import guess_lexer_for_filename
15 |
16 | from src.backend.domain.code_comment import CodeComment
17 |
18 | CODE_REPOSITORIES_BLUEPRINT = Blueprint('code_repositories', __name__)
19 |
20 |
21 | @CODE_REPOSITORIES_BLUEPRINT.route('/coderepositories')
22 | @jwt_required()
23 | def get_code_repositories():
24 | github = Github(get_jwt_identity())
25 | code_repos: Iterable[Repository] = github.get_user().get_repos()
26 |
27 | return jsonify({
28 | "codeRepositories": list(
29 | map(
30 | lambda repo: {
31 | "id": repo.id,
32 | "owner": repo.owner.login,
33 | "name": repo.name,
34 | "openPullRequestCount": repo.get_pulls().totalCount
35 | }, code_repos))
36 | })
37 |
38 |
39 | @CODE_REPOSITORIES_BLUEPRINT.route(
40 | '/coderepositories//pullrequests')
41 | @jwt_required()
42 | def get_pull_requests(code_repository_id: int):
43 | github = Github(get_jwt_identity())
44 | code_repository = github.get_repo(full_name_or_id=int(code_repository_id))
45 |
46 | return jsonify({
47 | "codeRepoName": code_repository.name,
48 | "pullRequests": list(
49 | map(
50 | lambda pull_request: {
51 | "id": pull_request.id,
52 | "number": pull_request.number,
53 | "title": pull_request.title,
54 | "userName": pull_request.user.name,
55 | "userAvatarUrl": pull_request.user.avatar_url
56 | },
57 | code_repository.get_pulls().get_page(0)))
58 | })
59 |
60 |
61 | @CODE_REPOSITORIES_BLUEPRINT.route(
62 | '/coderepositories//pullrequests/')
63 | @jwt_required()
64 | def get_pull_request(code_repository_id, pull_request_id):
65 | github = Github(get_jwt_identity())
66 | code_repository: Repository = github.get_repo(
67 | full_name_or_id=int(code_repository_id))
68 | pull_request = code_repository.get_pull(number=int(pull_request_id))
69 |
70 | code_comments = map(
71 | lambda code_comment: CodeComment.from_github_code_comment(code_comment),
72 | pull_request.get_comments())
73 |
74 | return jsonify({
75 | "title": pull_request.title,
76 | "codeRepoName": code_repository.name,
77 | "userAvatarUrl": pull_request.user.avatar_url,
78 | "body": markdown.markdown(
79 | pull_request.body, extensions=[GithubFlavoredMarkdownExtension()]),
80 | "state": pull_request.state.capitalize(),
81 | "comments": list(
82 | map(
83 | lambda code_comment: code_comment.__dict__,
84 | filter(lambda code_comment: code_comment is not None,
85 | code_comments))),
86 | "files": list(
87 | map(
88 | lambda file: {
89 | "path": file.filename,
90 | "ref": pull_request.base.ref if file.status == 'removed'
91 | else pull_request.head.ref,
92 | "prevRef": pull_request.base.ref,
93 | "rawUrl": file.raw_url,
94 | "status": file.status,
95 | "sha": file.sha,
96 | "patch": file.patch
97 | }, pull_request.get_files()))
98 | })
99 |
100 |
101 | @CODE_REPOSITORIES_BLUEPRINT.route(
102 | '/coderepositories//pullrequests//merge',
103 | methods=['POST'])
104 | @jwt_required()
105 | def merge_pull_request(code_repository_id, pull_request_id):
106 | github = Github(get_jwt_identity())
107 | code_repository: Repository = github.get_repo(
108 | full_name_or_id=int(code_repository_id))
109 | pull_request = code_repository.get_pull(number=int(pull_request_id))
110 |
111 | merge_strategy = request.json['mergeStrategy']
112 |
113 | try:
114 | pull_request.merge(merge_method=merge_strategy)
115 | except GithubException as exception:
116 | return jsonify({'message': exception.data['message']}), 400
117 |
118 | return '', 200
119 |
120 |
121 | @CODE_REPOSITORIES_BLUEPRINT.route(
122 | '/coderepositories//pullrequests//comment',
123 | methods=['POST'])
124 | @jwt_required()
125 | def add_comment(code_repository_id, pull_request_id):
126 | body = request.json
127 |
128 | github = Github(get_jwt_identity())
129 |
130 | in_reply_to_comment_id = body['inReplyToCommentId']
131 | code_comment = CodeComment(file_path=body['path'],
132 | code_block_id=body['codeBlockId'],
133 | body=body['comment'],
134 | author=github.get_user().name,
135 | updated_at=datetime.utcnow())
136 |
137 | code_repo: Repository = github.get_repo(int(code_repository_id))
138 | pull_request = code_repo.get_pull(number=int(pull_request_id))
139 |
140 | if in_reply_to_comment_id:
141 | created_comment = pull_request.create_review_comment_reply(
142 | in_reply_to_comment_id, code_comment.get_github_comment())
143 | else:
144 | commit = code_repo.get_commit(pull_request.head.sha)
145 | created_comment = pull_request.create_comment(
146 | code_comment.get_github_comment(), commit,
147 | code_comment.get_github_path(), code_comment.get_github_position())
148 |
149 | return jsonify({'id': created_comment.id})
150 |
151 | def convert_github_file_to_html(file_path, content):
152 | file_content = base64.b64decode(content)
153 |
154 | if file_path.endswith('.ipynb'):
155 | html_exporter = HTMLExporter()
156 | html_exporter.template_file = 'basic'
157 | notebook = nbformat.reads(file_content, as_version=4)
158 | (body, _) = html_exporter.from_notebook_node(notebook)
159 | else:
160 | lexer = guess_lexer_for_filename(file_path, '', stripall=True)
161 | formatter = HtmlFormatter(linenos='inline',
162 | full=True,
163 | noclasses=False,
164 | nobackground=True,
165 | lineseparator="
",
166 | classprefix='koopera-code-viewer')
167 | body = highlight(file_content, lexer, formatter)
168 |
169 | return body
170 |
171 | @CODE_REPOSITORIES_BLUEPRINT.route('/coderepositories//file'
172 | )
173 | @jwt_required()
174 | def get_file(code_repository_id):
175 | file_sha = request.args.get("sha")
176 | file_path = request.args.get("path")
177 | file_ref = request.args.get("ref")
178 |
179 | github = Github(get_jwt_identity())
180 | code_repo: Repository = github.get_repo(int(code_repository_id))
181 |
182 | if file_sha:
183 | content = code_repo.get_git_blob(file_sha).content
184 | else:
185 | content = code_repo.get_contents(file_path, file_ref).content
186 |
187 | return jsonify({'body': convert_github_file_to_html(file_path, content)})
188 |
--------------------------------------------------------------------------------
/src/backend/api/notebooks.py:
--------------------------------------------------------------------------------
1 | from flask import Blueprint, jsonify, redirect, request
2 | from flask_jwt_extended import get_jwt_identity, jwt_required
3 | from github import Github
4 | from sqlalchemy import create_engine
5 | from sqlalchemy.orm import sessionmaker
6 |
7 | from src.backend.config import DATABASE_URI
8 | from src.backend.model.code_repository import CodeRepository
9 | from src.backend.model.notebook import Notebook
10 |
11 | # pylint: disable=no-member
12 | # Remove this when pylint no longer shows a false-positive "no-member" for SQLAlchemy "Session"
13 | # Issue: https://github.com/PyCQA/pylint/issues/3610
14 |
15 | SESSION = sessionmaker(create_engine(DATABASE_URI))
16 |
17 | NOTEBOOKS_BLUEPRINT = Blueprint('notebooks', __name__)
18 |
19 | @NOTEBOOKS_BLUEPRINT.route('/notebooks')
20 | @jwt_required()
21 | def get_all_notebooks():
22 | session = SESSION()
23 |
24 | return jsonify({
25 | "notebooks": list(
26 | map(
27 | lambda notebook: {
28 | 'id': notebook.id,
29 | 'title': notebook.title,
30 | 'summary': notebook.summary,
31 | 'sha': notebook.sha,
32 | 'repoId': notebook.code_repo_id,
33 | 'repoName': notebook.code_repo.name
34 | },
35 | session.query(Notebook).all()))
36 | })
37 |
38 |
39 | @NOTEBOOKS_BLUEPRINT.route('/notebooks/')
40 | @jwt_required()
41 | def get_notebook(notebook_id):
42 | session = SESSION()
43 |
44 | notebook = session.query(Notebook).get(int(notebook_id))
45 |
46 | if notebook is None:
47 | return jsonify({}), 404
48 |
49 | return redirect(
50 | f'/api/coderepositories/{notebook.code_repo_id}/file?path={notebook.path}&sha={notebook.sha}'
51 | )
52 |
53 |
54 | @NOTEBOOKS_BLUEPRINT.route('/notebooks/', methods=['DELETE'])
55 | @jwt_required()
56 | def delete_notebook(notebook_id):
57 | session = SESSION()
58 |
59 | session.query(Notebook).filter(Notebook.id == int(notebook_id)).delete()
60 | session.commit()
61 |
62 | return jsonify({})
63 |
64 |
65 | @NOTEBOOKS_BLUEPRINT.route('/notebooks', methods=['POST'])
66 | @jwt_required()
67 | def import_notebooks():
68 | session = SESSION()
69 | body = request.json if request.data else None
70 |
71 | github = Github(get_jwt_identity())
72 |
73 | if body and 'codeRepositories' in body:
74 | code_repos = body['codeRepositories']
75 | code_repos_ids = set(map(lambda repo: repo['id'], code_repos))
76 | code_repos_owners = set(map(lambda repo: repo['owner'], code_repos))
77 | else:
78 | # no repos passed!
79 | # update notebooks for current repos
80 | code_repos = session.query(CodeRepository).all()
81 | code_repos_ids = set(map(lambda repo: repo.id, code_repos))
82 | code_repos_owners = set(map(lambda repo: repo.owner, code_repos))
83 |
84 | notebooks_added = 0
85 | notebooks_updated = 0
86 |
87 | for owner in code_repos_owners:
88 | notebooks = filter(
89 | lambda notebook: notebook.repository.id in code_repos_ids,
90 | github.search_code(f'user:{owner} extension:ipynb'))
91 |
92 | for notebook in notebooks:
93 | notebook_db = session.query(Notebook).filter(
94 | Notebook.path == notebook.path and
95 | Notebook.code_repo_id == notebook.repository.id).first()
96 |
97 | if notebook_db:
98 | notebook_db.sha = notebook.sha
99 | notebooks_updated += 1
100 | else:
101 | if session.query(CodeRepository).get(
102 | notebook.repository.id) is None:
103 | # create repo
104 | session.add(
105 | CodeRepository(id=notebook.repository.id,
106 | name=notebook.repository.name,
107 | owner=owner))
108 |
109 | session.add(
110 | Notebook(code_repo_id=notebook.repository.id,
111 | sha=notebook.sha,
112 | path=notebook.path,
113 | title=notebook.name,
114 | summary=''))
115 | notebooks_added += 1
116 |
117 | session.commit()
118 |
119 | return jsonify({
120 | 'notebooksAdded': notebooks_added,
121 | 'notebooksUpdated': notebooks_updated
122 | })
123 |
--------------------------------------------------------------------------------
/src/backend/api_service.py:
--------------------------------------------------------------------------------
1 | from flask import Flask, jsonify, request, send_from_directory
2 | from flask_cors import CORS
3 | from flask_jwt_extended import (JWTManager, create_access_token,
4 | get_jwt_identity, jwt_required)
5 | from github import Github, NamedUser, BadCredentialsException
6 |
7 | from src.backend.api.code_repositories import CODE_REPOSITORIES_BLUEPRINT
8 | from src.backend.api.notebooks import NOTEBOOKS_BLUEPRINT
9 |
10 | from .config import ALLOW_CORS, JWT_SECRET_KEY
11 |
12 |
13 | class APIService:
14 |
15 | @staticmethod
16 | def get() -> Flask:
17 | app = Flask(__name__)
18 |
19 | if ALLOW_CORS:
20 | CORS(app, resources={r'/*': {'origins': '*'}})
21 |
22 | app.config['JWT_SECRET_KEY'] = JWT_SECRET_KEY
23 | JWTManager(app)
24 |
25 | app.register_blueprint(CODE_REPOSITORIES_BLUEPRINT, url_prefix='/api')
26 | app.register_blueprint(NOTEBOOKS_BLUEPRINT, url_prefix='/api')
27 |
28 | @app.route('/img/')
29 | def get_img_file(path):
30 | return send_from_directory('static/img', path)
31 |
32 | @app.route('/js/')
33 | def get_js_file(path):
34 | return send_from_directory('static/js', path)
35 |
36 | @app.route('/css/')
37 | def get_css_file(path):
38 | return send_from_directory('static/css', path)
39 |
40 | @app.route('/api/login', methods=['POST'])
41 | @jwt_required(optional=True)
42 | def login():
43 | personal_access_token = request.json['personalAccessToken']
44 | github = Github(personal_access_token)
45 |
46 | try:
47 | github.get_user().login
48 | except BadCredentialsException:
49 | return '', 401
50 |
51 | access_token = create_access_token(identity=personal_access_token)
52 | return jsonify(accessToken=access_token), 200
53 |
54 | @app.route('/api/me')
55 | @jwt_required()
56 | def user_info():
57 | github = Github(get_jwt_identity())
58 | user: NamedUser = github.get_user()
59 |
60 | return jsonify({
61 | "avatarUrl": user.avatar_url,
62 | "name": user.name,
63 | })
64 |
65 | @app.route('/')
66 | @app.route('/')
67 | def index(path=None):
68 | return app.send_static_file('index.html')
69 |
70 | return app
71 |
--------------------------------------------------------------------------------
/src/backend/config.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | ALLOW_CORS = os.getenv('ALLOW_CORS', True)
4 | JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY', '')
5 | DATABASE_URI = os.getenv(
6 | 'DATABASE_URI',
7 | 'postgres+psycopg2://postgres:password@localhost:5432/koopera')
8 |
--------------------------------------------------------------------------------
/src/backend/domain/code_comment.py:
--------------------------------------------------------------------------------
1 | from datetime import datetime, timezone
2 | from typing import Optional
3 |
4 | from github.PullRequestComment import PullRequestComment
5 |
6 |
7 | class CodeComment:
8 |
9 | def __init__(self,
10 | file_path: str,
11 | code_block_id: int,
12 | body: str,
13 | author: str,
14 | updated_at: datetime,
15 | id: Optional[int] = None):
16 | self.id = id
17 | self.filePath = file_path
18 | self.codeBlockId = code_block_id
19 | self.body = body
20 | self.author = author
21 | self.updatedAt = updated_at
22 |
23 | @classmethod
24 | def is_ipynb_file(cls, path):
25 | return path.endswith('.ipynb')
26 |
27 | def get_github_path(self) -> int:
28 | return self.filePath
29 |
30 | def get_github_position(self) -> int:
31 | if self.is_ipynb_file(self.filePath):
32 | return 1
33 |
34 | return self.codeBlockId
35 |
36 | def get_github_comment(self) -> str:
37 | if self.is_ipynb_file(self.filePath):
38 | return f'[{self.codeBlockId}] {self.body}'
39 |
40 | return self.body
41 |
42 | @classmethod
43 | def from_github_code_comment(cls, code_comment: PullRequestComment):
44 | body = code_comment.body
45 |
46 | if cls.is_ipynb_file(code_comment.path):
47 | separator_index = body.find(']')
48 | code_block_id = body[1:separator_index]
49 |
50 | if code_block_id.isdigit() is False:
51 | return None
52 |
53 | code_block_id = int(code_block_id)
54 | body = body[separator_index + 2:body.__len__()]
55 | else:
56 | code_block_id = code_comment.position if code_comment.position is not None else 1
57 |
58 | return CodeComment(id=code_comment.id,
59 | file_path=code_comment.path,
60 | code_block_id=code_block_id,
61 | body=body,
62 | author=code_comment.user.name,
63 | updated_at=code_comment.updated_at.replace(tzinfo=timezone.utc).timestamp())
64 |
--------------------------------------------------------------------------------
/src/backend/model/code_repository.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Column, Integer, String
2 | from sqlalchemy.ext.declarative import declarative_base
3 |
4 | CodeRepositoryBase = declarative_base()
5 |
6 |
7 | class CodeRepository(CodeRepositoryBase):
8 | __tablename__ = 'code_repositories'
9 |
10 | id = Column(Integer, primary_key=True)
11 | name = Column(String)
12 | owner = Column(String)
13 |
--------------------------------------------------------------------------------
/src/backend/model/notebook.py:
--------------------------------------------------------------------------------
1 | from sqlalchemy import Column, ForeignKey, Integer, String
2 | from sqlalchemy.ext.declarative import declarative_base
3 | from sqlalchemy.orm import relationship
4 |
5 | from src.backend.model.code_repository import CodeRepository
6 |
7 | NotebookBase = declarative_base()
8 |
9 |
10 | class Notebook(NotebookBase):
11 | __tablename__ = 'notebooks'
12 |
13 | id = Column(Integer, primary_key=True)
14 | code_repo_id = Column(Integer, ForeignKey(CodeRepository.id))
15 | sha = Column(String)
16 | path = Column(String)
17 | title = Column(String)
18 | summary = Column(String)
19 |
20 | code_repo = relationship(CodeRepository, lazy='joined')
21 |
--------------------------------------------------------------------------------
/src/frontend/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
27 |
28 |
78 |
--------------------------------------------------------------------------------
/src/frontend/components/Loader.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
17 |
--------------------------------------------------------------------------------
/src/frontend/components/NavBar.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
12 | Notebooks
13 |
14 |
15 |
27 |
28 |
29 |
30 |
75 |
76 |
121 |
--------------------------------------------------------------------------------
/src/frontend/components/SubNav.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ getBaseLinkTitle() }}
5 |
6 | /
7 |
9 | {{ getSecondLinkTitle() }}
10 |
11 |
12 |
13 |
14 |
49 |
50 |
68 |
--------------------------------------------------------------------------------
/src/frontend/components/codeComment.js:
--------------------------------------------------------------------------------
1 | const CODE_COMMENT_STATUS = {
2 | OPENED: 0,
3 | CLOSED: 1,
4 | };
5 |
6 | export default class CodeComment {
7 | constructor(
8 | containerElement,
9 | path,
10 | codeBlockId,
11 | status,
12 | addCommentCallback,
13 | comments = [],
14 | ) {
15 | this.containerElement = containerElement;
16 | this.path = path;
17 | this.codeBlockId = codeBlockId;
18 | this.status = status;
19 | this.addCommentCallback = addCommentCallback;
20 | this.comments = comments;
21 | }
22 |
23 | static _timePassedFormatting(timestampInSeconds) {
24 | const timeDiffInSeconds = (Date.now() / 1000) - timestampInSeconds;
25 |
26 | if (timeDiffInSeconds > 604800) {
27 | return 'more than a week ago';
28 | }
29 | if (timeDiffInSeconds > 86400) {
30 | return `${Math.round(timeDiffInSeconds / 86400)} days ago`;
31 | }
32 | if (timeDiffInSeconds > 3600) {
33 | return `${Math.round(timeDiffInSeconds / 3600)} hours ago`;
34 | }
35 | if (timeDiffInSeconds > 60) {
36 | return `${Math.round(timeDiffInSeconds / 60)} minutes ago`;
37 | }
38 |
39 | return 'less than a minute ago';
40 | }
41 |
42 | static _getCommentsAsHtmlStr(comments) {
43 | return comments.map(comment => `
44 | `).join('');
51 | }
52 |
53 | attachOnClickToSaveBtn() {
54 | document.getElementsByClassName('save-code-comment-btn')[0].onclick = () => {
55 | const comment = document.getElementsByClassName('code-comment-text')[0].value;
56 | const inReplyToCommentId = this.comments.length > 0
57 | ? this.comments[this.comments.length - 1].id
58 | : null;
59 | this.addCommentCallback(this, comment, inReplyToCommentId);
60 | };
61 | }
62 |
63 | template() {
64 | if (this.status === CODE_COMMENT_STATUS.OPENED) {
65 | return `
66 |
77 | `;
78 | }
79 |
80 | return ``;
83 | }
84 |
85 | addComment(id, newComment) {
86 | this.comments.push({
87 | id,
88 | author: 'Me',
89 | comment: newComment,
90 | updatedAt: (Date.now() / 1000),
91 | });
92 | this.status = CODE_COMMENT_STATUS.CLOSED;
93 | this.render();
94 | }
95 |
96 | open() {
97 | this.status = CODE_COMMENT_STATUS.OPENED;
98 | this.render();
99 | this.attachOnClickToSaveBtn();
100 | }
101 |
102 | close() {
103 | if (this.comments.length === 0) {
104 | // no comments in this code block -> just remove it from UI
105 | this.containerElement.parentElement.removeChild(this.containerElement);
106 | } else {
107 | this.status = CODE_COMMENT_STATUS.CLOSED;
108 | }
109 | this.render();
110 | }
111 |
112 | render() {
113 | this.containerElement.innerHTML = this.template();
114 | }
115 |
116 | static createNew(containerElement, path, codeBlockId, addCommentCallback) {
117 | const codeComment = new CodeComment(
118 | containerElement,
119 | path,
120 | codeBlockId,
121 | CODE_COMMENT_STATUS.OPENED,
122 | addCommentCallback,
123 | );
124 |
125 | codeComment.render();
126 | codeComment.attachOnClickToSaveBtn();
127 |
128 | return codeComment;
129 | }
130 |
131 | static createExisting(containerElement, path, codeBlockId, addCommentCallback, comments) {
132 | const codeComment = new CodeComment(
133 | containerElement,
134 | path,
135 | codeBlockId,
136 | CODE_COMMENT_STATUS.CLOSED,
137 | addCommentCallback,
138 | comments,
139 | );
140 |
141 | codeComment.render();
142 |
143 | return codeComment;
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/frontend/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import App from './App.vue';
3 | import router from './router';
4 |
5 | Vue.config.productionTip = false;
6 |
7 | new Vue({
8 | router,
9 | render: h => h(App),
10 | }).$mount('#app');
11 |
--------------------------------------------------------------------------------
/src/frontend/router.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import Router from 'vue-router';
3 | import Home from './views/Home.vue';
4 | import Login from './views/Login.vue';
5 |
6 | Vue.use(Router);
7 |
8 | export default new Router({
9 | mode: 'history',
10 | base: process.env.BASE_URL,
11 | routes: [
12 | {
13 | path: '/',
14 | name: 'home',
15 | component: Home,
16 | },
17 | {
18 | path: '/login',
19 | name: 'login',
20 | component: Login,
21 | },
22 | {
23 | path: '/notebooks',
24 | name: 'notebooks',
25 | component: () => import('./views/notebooks/Notebooks.vue'),
26 | },
27 | {
28 | path: '/notebooks/view/:notebookId',
29 | name: 'notebook',
30 | component: () => import('./views/notebooks/Notebook.vue'),
31 | },
32 | {
33 | path: '/notebooks/import',
34 | name: 'notebooksImport',
35 | component: () => import('./views/notebooks/ImportNotebooks.vue'),
36 | },
37 | {
38 | path: '/coderepositories/:codeRepositoryId/pullrequests',
39 | name: 'pullRequests',
40 | component: () => import('./views/PullRequests.vue'),
41 | },
42 | {
43 | path: '/coderepositories/:codeRepositoryId/pullrequests/:pullRequestNumber',
44 | name: 'pullRequest',
45 | component: () => import('./views/PullRequest.vue'),
46 | },
47 | ],
48 | });
49 |
--------------------------------------------------------------------------------
/src/frontend/shared/credentialManager.js:
--------------------------------------------------------------------------------
1 | import UserCredentials from './userCredentials';
2 |
3 | export default class CredentialManager {
4 | static store(sourceControlAccessToken, apiAccessToken) {
5 | localStorage.setItem('sourceControlAccessToken', sourceControlAccessToken);
6 | localStorage.setItem('apiAccessToken', apiAccessToken);
7 | }
8 |
9 | static load() {
10 | return new UserCredentials(
11 | localStorage.getItem('sourceControlAccessToken'),
12 | localStorage.getItem('apiAccessToken'),
13 | );
14 | }
15 |
16 | static erase() {
17 | localStorage.removeItem('sourceControlAccessToken');
18 | localStorage.removeItem('apiAccessToken');
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/src/frontend/shared/getAPIUrl.js:
--------------------------------------------------------------------------------
1 | function getAPIUrl(relativePath) {
2 | if (process.env.NODE_ENV === 'development') {
3 | if (process.env.API_BASE_URL) {
4 | return `${process.env.API_BASE_URL}/api/${relativePath}`;
5 | }
6 |
7 | return `http://localhost:5000/api/${relativePath}`;
8 | }
9 |
10 | return `/api/${relativePath}`;
11 | }
12 |
13 | export default getAPIUrl;
14 |
--------------------------------------------------------------------------------
/src/frontend/shared/userCredentials.js:
--------------------------------------------------------------------------------
1 | export default class UserCredentials {
2 | constructor(sourceControlAccessToken, apiAccessToken) {
3 | this.sourceControlAccessToken = sourceControlAccessToken;
4 | this.apiAccessToken = apiAccessToken;
5 | }
6 |
7 | isValid() {
8 | return !!this.sourceControlAccessToken && !!this.apiAccessToken;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/frontend/store.js:
--------------------------------------------------------------------------------
1 | const store = {
2 | state: {
3 | codeRepoId: null,
4 | codeRepoName: ''
5 | },
6 | setCodeRepo(id, name) {
7 | this.state.codeRepoId = id;
8 | this.state.codeRepoName = name;
9 | },
10 | getCodeRepoName(id) {
11 | if (this.state.codeRepoId !== id) {
12 | return null;
13 | }
14 | return this.state.codeRepoName;
15 | }
16 | };
17 |
18 | export default store;
19 |
--------------------------------------------------------------------------------
/src/frontend/views/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
{{ codeRepository.name }}
10 |
11 |
12 |
17 | Pull Requests ({{ codeRepository.openPullRequestCount }} open)
18 |
19 |
20 |
21 |
22 |
23 |
24 |
67 |
68 |
95 |
--------------------------------------------------------------------------------
/src/frontend/views/Login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
12 |
16 |
17 |
18 |
19 |
20 |
59 |
60 |
86 |
--------------------------------------------------------------------------------
/src/frontend/views/PullRequest.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
18 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | Description
43 |
44 |
45 |
46 |
47 |
48 |
49 |
52 |
56 |
57 |
58 |
60 | Merge
61 |
62 |
64 | Squash
65 |
66 |
68 | Rebase
69 |
70 |
71 |
72 |
73 |
89 |
95 |
96 |
97 |
98 |
99 |
390 |
391 |
683 |
--------------------------------------------------------------------------------
/src/frontend/views/PullRequests.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
10 |
{{ pullRequest.title }}
11 |
12 |
13 |
19 | Review
20 |
21 |
22 |
23 |
24 |
25 |
26 |
70 |
71 |
81 |
--------------------------------------------------------------------------------
/src/frontend/views/notebooks/ImportNotebooks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | selectAllCodeRepo()'/>
9 |
10 |
11 |
12 |
15 |
21 |
22 |
23 |
24 |
25 |
26 |
32 |
33 |
34 |
check_circle
35 |
36 | Successfully imported {{ notebooksImported }} notebooks...
37 |
38 |
41 | Back
42 |
43 |
44 |
45 |
error_outline
46 |
47 | Unfortunately, we could not found any jupyter notebook
48 | in the selected repositories...
49 |
50 |
53 | Back
54 |
55 |
56 |
57 |
58 |
59 |
60 |
143 |
144 |
166 |
--------------------------------------------------------------------------------
/src/frontend/views/notebooks/Notebook.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
47 |
--------------------------------------------------------------------------------
/src/frontend/views/notebooks/Notebooks.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
18 | Import
19 |
20 |
21 |
22 |
26 |
27 |
{{ codeRepoName }}
28 |
29 |
33 |
44 |
45 |
46 |
47 | {{ notebook.title }}
48 |
49 |
50 |
51 |
52 |
53 |
54 |
56 |
No notebooks found...
57 |
60 |
Want to import your jupyter notebooks directly from your GitHub?
61 |
62 |
65 | Import
66 |
67 |
68 |
69 |
70 |
71 |
72 |
181 |
182 |
241 |
--------------------------------------------------------------------------------