├── .gitattributes ├── .github └── workflows │ ├── run-on-windows.yml │ └── run-tests.yml ├── .gitignore ├── COMMERCIAL-LICENSE.txt ├── CONTRIBUTOR_LICENSE_AGREEMENT.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── docker-compose-https.yml ├── docker-compose.yml ├── docker ├── init-letsencrypt.sh ├── mercury │ ├── Dockerfile │ └── entrypoint.sh └── nginx │ ├── Dockerfile │ ├── default.conf │ └── pro │ └── default.conf ├── frontend ├── .gitignore ├── package.json ├── public │ ├── favicon-old.ico │ ├── favicon.ico │ ├── index.html │ ├── jupyter-additional.css │ ├── jupyter-syntax.css │ ├── jupyter-theme-light.css │ ├── manifest.json │ ├── mercury_black_logo.svg │ ├── mercury_logo.svg │ └── robots.txt ├── src │ ├── Root.tsx │ ├── Routes.tsx │ ├── components │ │ ├── AutoRefresh.tsx │ │ ├── DefaultLogo.tsx │ │ ├── FileItem.tsx │ │ ├── FilesView.tsx │ │ ├── Footer.tsx │ │ ├── HomeNavBar.tsx │ │ ├── LoginButton.tsx │ │ ├── MadeWithDiv.tsx │ │ ├── MainView.tsx │ │ ├── NavBar.tsx │ │ ├── ProFeatureAlert.tsx │ │ ├── RequireAuth.tsx │ │ ├── RestAPIView.tsx │ │ ├── RunButton.tsx │ │ ├── SelectExecutionHistory.tsx │ │ ├── ShareDialog.tsx │ │ ├── SideBar.tsx │ │ ├── StatusBar.tsx │ │ ├── UserButton.tsx │ │ ├── WaitPDFExport.tsx │ │ └── WindowDimensions.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── rootReducer.ts │ ├── setupTests.ts │ ├── slices │ │ ├── appSlice.ts │ │ ├── authSlice.ts │ │ ├── notebooksSlice.ts │ │ ├── sitesSlice.ts │ │ ├── tasksSlice.ts │ │ ├── versionSlice.ts │ │ └── wsSlice.tsx │ ├── store.ts │ ├── utils.ts │ ├── views │ │ ├── AccountView.tsx │ │ ├── App.css │ │ ├── App.tsx │ │ ├── AppView.tsx │ │ ├── HomeView.tsx │ │ ├── LoginView.tsx │ │ ├── LostConnection.tsx │ │ ├── NotebookNotFoundView.tsx │ │ ├── OpenAPIView.tsx │ │ ├── SiteAccessForbiddenView.tsx │ │ ├── SiteLoadingView.tsx │ │ ├── SiteNetworkErrorView.tsx │ │ ├── SiteNotFoundView.tsx │ │ ├── SiteNotReadyView.tsx │ │ └── SitePleaseRefreshView.tsx │ ├── websocket │ │ └── Provider.tsx │ └── widgets │ │ ├── Button.tsx │ │ ├── Checkbox.tsx │ │ ├── File.tsx │ │ ├── Markdown.tsx │ │ ├── Numeric.tsx │ │ ├── Range.tsx │ │ ├── Select.tsx │ │ ├── Slider.tsx │ │ ├── Text.tsx │ │ └── Types.tsx ├── tsconfig.json └── yarn.lock ├── mercury ├── .gitignore ├── __init__.py ├── apps │ ├── __init__.py │ ├── accounts │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── fields.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_apikey.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── templatetags │ │ │ ├── __init__.py │ │ │ └── replace.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ ├── test_accounts.py │ │ │ ├── test_apikey.py │ │ │ ├── test_invitations.py │ │ │ ├── test_secrets.py │ │ │ ├── test_sites.py │ │ │ └── test_subscription.py │ │ ├── urls.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── accounts.py │ │ │ ├── apikey.py │ │ │ ├── invitations.py │ │ │ ├── permissions.py │ │ │ ├── secrets.py │ │ │ ├── sites.py │ │ │ ├── subscription.py │ │ │ └── utils.py │ ├── nb │ │ ├── __init__.py │ │ ├── exporter.py │ │ ├── nbrun.py │ │ ├── tests.py │ │ ├── tests │ │ │ ├── __init__.py │ │ │ └── test_nbrun.py │ │ └── utils.py │ ├── nbworker │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── nb.py │ │ ├── rest.py │ │ ├── tests.py │ │ ├── utils.py │ │ └── ws.py │ ├── notebooks │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── fixtures │ │ │ ├── simple_notebook.ipynb │ │ │ └── third_notebook.ipynb │ │ ├── management │ │ │ ├── __init__.py │ │ │ └── commands │ │ │ │ ├── __init__.py │ │ │ │ ├── add.py │ │ │ │ ├── delete.py │ │ │ │ ├── list.py │ │ │ │ └── watch.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── slides_themes.py │ │ ├── tasks.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── storage │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_useruploadedfile.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── s3utils.py │ │ ├── serializers.py │ │ ├── storage.py │ │ ├── tests.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views │ │ │ ├── __init__.py │ │ │ ├── dashboardfiles.py │ │ │ ├── notebookfiles.py │ │ │ ├── stylefiles.py │ │ │ └── workerfiles.py │ ├── tasks │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── clean_service.py │ │ ├── export_pdf.py │ │ ├── export_png.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_restapitask.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── notify.py │ │ ├── serializers.py │ │ ├── tasks.py │ │ ├── tasks_export.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── workers │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── constants.py │ │ ├── migrations │ │ │ ├── 0001_initial.py │ │ │ ├── 0002_machine.py │ │ │ ├── 0003_worker_run_by_workersession.py │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── serializers.py │ │ ├── tests.py │ │ ├── urls.py │ │ ├── utils.py │ │ └── views.py │ └── ws │ │ ├── __init__.py │ │ ├── apps.py │ │ ├── client.py │ │ ├── middleware.py │ │ ├── migrations │ │ └── __init__.py │ │ ├── routing.py │ │ ├── tasks.py │ │ ├── tests.py │ │ ├── utils.py │ │ └── worker.py ├── demo.py ├── manage.py ├── mercury.py ├── requirements.txt ├── server │ ├── __init__.py │ ├── asgi.py │ ├── celery.py │ ├── settings.py │ ├── urls.py │ ├── views.py │ └── wsgi.py ├── templates │ └── account │ │ └── email │ │ ├── email_confirmation_message.txt │ │ └── password_reset_key_message.txt └── widgets │ ├── __init__.py │ ├── apiresponse.py │ ├── app.py │ ├── button.py │ ├── chat.py │ ├── checkbox.py │ ├── confetti.py │ ├── file.py │ ├── in_mercury.py │ ├── json.py │ ├── manager.py │ ├── md.py │ ├── multiselect.py │ ├── note.py │ ├── numberbox.py │ ├── numeric.py │ ├── outputdir.py │ ├── pdf.py │ ├── range.py │ ├── select.py │ ├── slider.py │ ├── stop.py │ ├── table.py │ ├── text.py │ └── user.py ├── pyproject.toml ├── scripts ├── dual_pack_mercury.sh └── pack_mercury.sh ├── setup-https.sh └── setup.py /.gitattributes: -------------------------------------------------------------------------------- 1 | *.css linguist-detectable=false 2 | -------------------------------------------------------------------------------- /.github/workflows/run-on-windows.yml: -------------------------------------------------------------------------------- 1 | name: Run on Windows 2 | 3 | on: 4 | schedule: 5 | - cron: '0 8 * * 1' 6 | # run workflow manually 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | runs-on: windows-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.8] 15 | 16 | steps: 17 | - uses: conda-incubator/setup-miniconda@v2 18 | with: 19 | activate-environment: test 20 | auto-update-conda: false 21 | python-version: ${{ matrix.python-version }} 22 | - name: Activate conda and check versions 23 | run: | 24 | conda create --name testenv python=3.8 25 | conda activate testenv 26 | conda --version 27 | python --version 28 | - name: Install Mercury 29 | run: conda install -c conda-forge -n testenv mljar-mercury 30 | - name: Run Mercury server 31 | run: mercury run 32 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.8] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | sudo apt-get update 22 | python -m pip install --upgrade pip 23 | ./scripts/pack_mercury.sh 24 | pip install --upgrade setuptools 25 | cd mercury 26 | pip install -U -r requirements.txt 27 | - name: Test backend 28 | run: | 29 | cd mercury 30 | python manage.py test apps 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info* 2 | dist/ 3 | .env -------------------------------------------------------------------------------- /CONTRIBUTOR_LICENSE_AGREEMENT.md: -------------------------------------------------------------------------------- 1 | Mercury Contributor License Agreement 2 | 3 | I disavow any rights or claims to any changes submitted to the Mercury project and assign the copyright of those changes to MLJAR Sp. z o.o. 4 | 5 | As far as the law allows, my contributions come as is, without any warranty or condition, and I will not be liable to anyone for any damages related to this software or this license, under any kind of legal claim. 6 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include mercury/frontend-dist/ * 2 | recursive-include mercury/frontend-single-site-dist/ * 3 | include mercury/requirements.txt 4 | include README.md 5 | -------------------------------------------------------------------------------- /docker-compose-https.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | nginx: 5 | restart: unless-stopped 6 | build: 7 | context: . 8 | dockerfile: ./docker/nginx/Dockerfile 9 | ports: 10 | - 80:80 11 | - 443:443 12 | volumes: 13 | - static_volume:/app/mercury/django_static 14 | - media_volume:/app/mercury/media 15 | - ./docker/nginx/pro:/etc/nginx/conf.d 16 | - ./docker/nginx/certbot/conf:/etc/letsencrypt 17 | - ./docker/nginx/certbot/www:/var/www/certbot 18 | depends_on: 19 | - mercury 20 | command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'" 21 | certbot: 22 | image: certbot/certbot 23 | restart: unless-stopped 24 | volumes: 25 | - ./docker/nginx/certbot/conf:/etc/letsencrypt 26 | - ./docker/nginx/certbot/www:/var/www/certbot 27 | entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" 28 | 29 | mercury: 30 | restart: unless-stopped 31 | build: 32 | context: . 33 | dockerfile: ./docker/mercury/Dockerfile 34 | args: 35 | GITHUB_TOKEN: ${GITHUB_TOKEN} 36 | entrypoint: /app/docker/mercury/entrypoint.sh 37 | volumes: 38 | - ${NOTEBOOKS_PATH}:/app/notebooks 39 | - static_volume:/app/mercury/django_static 40 | - media_volume:/app/mercury/media 41 | expose: 42 | - 9000 43 | environment: 44 | DEBUG: ${DEBUG} 45 | SERVE_STATIC: ${SERVE_STATIC} 46 | DJANGO_SUPERUSER_USERNAME: ${DJANGO_SUPERUSER_USERNAME} 47 | DJANGO_SUPERUSER_PASSWORD: ${DJANGO_SUPERUSER_PASSWORD} 48 | DJANGO_SUPERUSER_EMAIL: ${DJANGO_SUPERUSER_EMAIL} 49 | SECRET_KEY: ${SECRET_KEY} 50 | ALLOWED_HOSTS: ${ALLOWED_HOSTS} 51 | WELCOME: ${WELCOME} 52 | EMAIL_HOST: ${EMAIL_HOST} 53 | EMAIL_HOST_USER: ${EMAIL_HOST_USER} 54 | EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} 55 | EMAIL_PORT: ${EMAIL_PORT} 56 | DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL} 57 | DJANGO_DB: postgresql 58 | POSTGRES_HOST: db 59 | POSTGRES_NAME: postgres 60 | POSTGRES_USER: postgres 61 | POSTGRES_PASSWORD: postgres 62 | POSTGRES_PORT: 5432 63 | depends_on: 64 | - db 65 | db: 66 | image: postgres:13.0-alpine 67 | restart: unless-stopped 68 | volumes: 69 | - postgres_data:/var/lib/postgresql/data/ 70 | environment: 71 | POSTGRES_DB: postgres 72 | POSTGRES_USER: postgres 73 | POSTGRES_PASSWORD: postgres 74 | 75 | volumes: 76 | static_volume: {} 77 | media_volume: {} 78 | postgres_data: {} 79 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | nginx: 5 | restart: unless-stopped 6 | build: 7 | context: . 8 | dockerfile: ./docker/nginx/Dockerfile 9 | ports: 10 | - 80:80 11 | volumes: 12 | - static_volume:/app/mercury/django_static 13 | - media_volume:/app/mercury/media 14 | - ./docker/nginx:/etc/nginx/conf.d 15 | depends_on: 16 | - mercury 17 | command: "/bin/sh -c 'nginx -g \"daemon off;\"'" 18 | mercury: 19 | restart: unless-stopped 20 | build: 21 | context: . 22 | dockerfile: ./docker/mercury/Dockerfile 23 | entrypoint: /app/docker/mercury/entrypoint.sh 24 | volumes: 25 | - ${NOTEBOOKS_PATH}:/app/notebooks 26 | - static_volume:/app/mercury/django_static 27 | - media_volume:/app/mercury/media 28 | expose: 29 | - 9000 30 | environment: 31 | DEBUG: ${DEBUG} 32 | SERVE_STATIC: ${SERVE_STATIC} 33 | DJANGO_SUPERUSER_USERNAME: ${DJANGO_SUPERUSER_USERNAME} 34 | DJANGO_SUPERUSER_PASSWORD: ${DJANGO_SUPERUSER_PASSWORD} 35 | DJANGO_SUPERUSER_EMAIL: ${DJANGO_SUPERUSER_EMAIL} 36 | SECRET_KEY: ${SECRET_KEY} 37 | ALLOWED_HOSTS: ${ALLOWED_HOSTS} 38 | WELCOME: ${WELCOME} 39 | EMAIL_HOST: ${EMAIL_HOST} 40 | EMAIL_HOST_USER: ${EMAIL_HOST_USER} 41 | EMAIL_HOST_PASSWORD: ${EMAIL_HOST_PASSWORD} 42 | EMAIL_PORT: ${EMAIL_PORT} 43 | DEFAULT_FROM_EMAIL: ${DEFAULT_FROM_EMAIL} 44 | DJANGO_DB: postgresql 45 | POSTGRES_HOST: db 46 | POSTGRES_NAME: postgres 47 | POSTGRES_USER: postgres 48 | POSTGRES_PASSWORD: postgres 49 | POSTGRES_PORT: 5432 50 | MERCURY_VERBOSE: ${MERCURY_VERBOSE} 51 | DJANGO_LOG_LEVEL: ${DJANGO_LOG_LEVEL} 52 | depends_on: 53 | - db 54 | db: 55 | image: postgres:13.0-alpine 56 | restart: unless-stopped 57 | volumes: 58 | - postgres_data:/var/lib/postgresql/data/ 59 | environment: 60 | POSTGRES_DB: postgres 61 | POSTGRES_USER: postgres 62 | POSTGRES_PASSWORD: postgres 63 | 64 | volumes: 65 | static_volume: {} 66 | media_volume: {} 67 | postgres_data: {} 68 | -------------------------------------------------------------------------------- /docker/init-letsencrypt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if ! [ -x "$(command -v docker-compose)" ]; then 4 | echo 'Error: docker-compose is not installed.' >&2 5 | exit 1 6 | fi 7 | 8 | domains=({{your_domain}} www.{{your_domain}}) 9 | rsa_key_size=4096 10 | data_path="./docker/nginx/certbot" 11 | email="" # Adding a valid address is strongly recommended 12 | staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits 13 | 14 | if [ -d "$data_path" ]; then 15 | read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision 16 | if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then 17 | exit 18 | fi 19 | fi 20 | 21 | 22 | if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then 23 | echo "### Downloading recommended TLS parameters ..." 24 | mkdir -p "$data_path/conf" 25 | curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf" 26 | curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem" 27 | echo 28 | fi 29 | 30 | echo "### Creating dummy certificate for $domains ..." 31 | path="/etc/letsencrypt/live/$domains" 32 | mkdir -p "$data_path/conf/live/$domains" 33 | docker-compose -f docker-compose-https.yml run --rm --entrypoint "\ 34 | openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\ 35 | -keyout '$path/privkey.pem' \ 36 | -out '$path/fullchain.pem' \ 37 | -subj '/CN=localhost'" certbot 38 | echo 39 | 40 | 41 | echo "### Starting nginx ..." 42 | docker-compose -f docker-compose-https.yml up --force-recreate -d nginx 43 | echo 44 | 45 | echo "### Deleting dummy certificate for $domains ..." 46 | docker-compose -f docker-compose-https.yml run --rm --entrypoint "\ 47 | rm -Rf /etc/letsencrypt/live/$domains && \ 48 | rm -Rf /etc/letsencrypt/archive/$domains && \ 49 | rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot 50 | echo 51 | 52 | 53 | echo "### Requesting Let's Encrypt certificate for $domains ..." 54 | #Join $domains to -d args 55 | domain_args="" 56 | for domain in "${domains[@]}"; do 57 | domain_args="$domain_args -d $domain" 58 | done 59 | 60 | # Select appropriate email arg 61 | case "$email" in 62 | "") email_arg="--register-unsafely-without-email" ;; 63 | *) email_arg="--email $email" ;; 64 | esac 65 | 66 | # Enable staging mode if needed 67 | if [ $staging != "0" ]; then staging_arg="--staging"; fi 68 | 69 | docker-compose -f docker-compose-https.yml run --rm --entrypoint "\ 70 | certbot certonly --webroot -w /var/www/certbot \ 71 | $staging_arg \ 72 | $email_arg \ 73 | $domain_args \ 74 | --rsa-key-size $rsa_key_size \ 75 | --agree-tos \ 76 | --force-renewal" certbot 77 | echo 78 | 79 | echo "### Reloading nginx ..." 80 | docker-compose -f docker-compose-https.yml exec nginx nginx -s reload 81 | -------------------------------------------------------------------------------- /docker/mercury/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:20.04 2 | 3 | RUN apt-get update && \ 4 | apt-get install -y software-properties-common && \ 5 | add-apt-repository ppa:deadsnakes/ppa && \ 6 | apt-get update && \ 7 | apt-get install -y python3.10 python3.10-dev python3-pip python-is-python3 git gconf-service \ 8 | libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 \ 9 | libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 \ 10 | libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 \ 11 | libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 \ 12 | lsb-release xdg-utils wget libcairo-gobject2 libxinerama1 libgtk2.0-0 libpangoft2-1.0-0 libthai0 libpixman-1-0 \ 13 | libxcb-render0 libharfbuzz0b libdatrie1 libgraphite2-3 libgbm1 \ 14 | libpq-dev libarchive13 15 | 16 | # Install Miniconda using official installer script 17 | RUN wget -qO /tmp/miniconda.sh https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \ 18 | bash /tmp/miniconda.sh -b -p /opt/conda && \ 19 | rm /tmp/miniconda.sh 20 | 21 | ENV PATH="/opt/conda/bin:${PATH}" 22 | 23 | WORKDIR /app 24 | ADD ./mercury/requirements.txt /app/mercury/ 25 | 26 | RUN conda install --yes mamba -c conda-forge 27 | RUN mamba install --yes python=3.10 --file mercury/requirements.txt -c conda-forge 28 | RUN mamba install --yes gunicorn psycopg2 daphne -c conda-forge 29 | RUN mamba install --yes mercury -c conda-forge 30 | 31 | ADD ./mercury/templates /app/mercury/templates 32 | ADD ./mercury/server /app/mercury/server 33 | ADD ./mercury/apps /app/mercury/apps 34 | ADD ./mercury/*.py /app/mercury/ 35 | ADD ./mercury/widgets /app/mercury/widgets 36 | ADD ./docker /app/docker 37 | 38 | RUN chmod +x /app/docker/mercury/entrypoint.sh 39 | -------------------------------------------------------------------------------- /docker/mercury/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | until cd /app/mercury 4 | do 5 | echo "Waiting for server volume..." 6 | done 7 | 8 | 9 | until python manage.py migrate 10 | do 11 | echo "Waiting for db to be ready..." 12 | sleep 2 13 | done 14 | 15 | # path with jupyter 16 | export PATH="$HOME/.local/bin:$PATH" 17 | 18 | echo "Docker: Add requirements for notebooks" 19 | REQ=/app/notebooks/requirements.txt 20 | if test -f "$REQ"; then 21 | pip install -r $REQ 22 | fi 23 | 24 | echo "Docker list files" 25 | ls -al /app/notebooks/ 26 | ls -al /app/ 27 | echo "Docker: Add notebooks" 28 | for filepath in /app/notebooks/*.ipynb; do 29 | echo "Docker: Add " $filepath 30 | python manage.py add $filepath 31 | done 32 | 33 | echo "Docker: Collect statics, for Admin Panel, DRF views" 34 | python manage.py collectstatic --noinput 35 | 36 | echo "Docker: Try to create super user, if doesnt exist" 37 | python manage.py createsuperuser --noinput 38 | 39 | echo "Docker: Start worker and beat service" 40 | celery -A server worker --loglevel=info -P gevent --concurrency 4 -E -Q celery,ws & 41 | celery -A server beat --loglevel=error --max-interval 60 & 42 | 43 | echo "Docker: Start daphne server" 44 | daphne server.asgi:application --bind 0.0.0.0 --port 9000 45 | 46 | #gunicorn server.wsgi --bind 0.0.0.0:8000 --workers 4 --threads 4 47 | 48 | # for debug 49 | #python manage.py runserver 0.0.0.0:9000 -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:14.16.0-alpine as build 2 | 3 | WORKDIR /app/frontend 4 | COPY ./frontend/package.json ./ 5 | COPY ./frontend/yarn.lock ./ 6 | RUN yarn install --frozen-lockfile 7 | COPY ./frontend/src ./src 8 | COPY ./frontend/public ./public 9 | COPY ./frontend/tsconfig.json ./tsconfig.json 10 | 11 | RUN yarn build 12 | 13 | # The second stage 14 | # Copy React static files and start nginx 15 | FROM nginx:stable-alpine 16 | COPY --from=build /app/frontend/build /usr/share/nginx/html 17 | -------------------------------------------------------------------------------- /docker/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name _; 4 | server_tokens off; 5 | access_log off; 6 | client_max_body_size 20M; 7 | 8 | location / { 9 | root /usr/share/nginx/html; 10 | index index.html index.htm; 11 | try_files $uri $uri/ /index.html; 12 | } 13 | 14 | location /api { 15 | try_files $uri @proxy_api; 16 | } 17 | location /admin { 18 | try_files $uri @proxy_api; 19 | } 20 | 21 | location @proxy_api { 22 | proxy_set_header Host $http_host; 23 | proxy_redirect off; 24 | proxy_pass http://mercury:9000; 25 | } 26 | 27 | location /ws { 28 | try_files $uri @proxy_ws; 29 | } 30 | 31 | location @proxy_ws { 32 | proxy_http_version 1.1; 33 | proxy_set_header Upgrade $http_upgrade; 34 | proxy_set_header Connection "upgrade"; 35 | proxy_redirect off; 36 | proxy_pass http://mercury:9000; 37 | } 38 | 39 | location /django_static/ { 40 | autoindex on; 41 | alias /app/mercury/django_static/; 42 | } 43 | 44 | location /media/ { 45 | autoindex on; 46 | alias /app/mercury/media/; 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /docker/nginx/pro/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name {{your_domain}}; 4 | server_tokens off; 5 | 6 | 7 | location /.well-known/acme-challenge/ { 8 | root /var/www/certbot; 9 | } 10 | 11 | location / { 12 | return 301 https://$host$request_uri; 13 | } 14 | } 15 | 16 | server { 17 | listen 443 ssl; 18 | server_name {{your_domain}}; 19 | server_tokens off; 20 | 21 | ssl_certificate /etc/letsencrypt/live/{{your_domain}}/fullchain.pem; 22 | ssl_certificate_key /etc/letsencrypt/live/{{your_domain}}/privkey.pem; 23 | include /etc/letsencrypt/options-ssl-nginx.conf; 24 | ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; 25 | 26 | client_max_body_size 20M; 27 | 28 | location / { 29 | root /usr/share/nginx/html; 30 | index index.html index.htm; 31 | try_files $uri $uri/ /index.html; 32 | } 33 | 34 | location /api { 35 | try_files $uri @proxy_api; 36 | } 37 | location /admin { 38 | try_files $uri @proxy_api; 39 | } 40 | 41 | location @proxy_api { 42 | proxy_set_header X-Forwarded-Proto https; 43 | proxy_set_header X-Url-Scheme $scheme; 44 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 45 | proxy_set_header Host $http_host; 46 | proxy_redirect off; 47 | proxy_pass http://mercury:9000; 48 | } 49 | 50 | location /ws { 51 | try_files $uri @proxy_ws; 52 | } 53 | 54 | location @proxy_ws { 55 | proxy_http_version 1.1; 56 | proxy_set_header Upgrade $http_upgrade; 57 | proxy_set_header Connection "upgrade"; 58 | proxy_redirect off; 59 | proxy_pass http://mercury:9000; 60 | } 61 | 62 | location /django_static/ { 63 | autoindex on; 64 | alias /app/mercury/django_static/; 65 | } 66 | 67 | location /media/ { 68 | autoindex on; 69 | alias /app/mercury/media/; 70 | } 71 | } -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "2.4.3", 4 | "private": true, 5 | "dependencies": { 6 | "@popperjs/core": "^2.11.0", 7 | "@reduxjs/toolkit": "^1.6.2", 8 | "@testing-library/jest-dom": "^5.11.4", 9 | "@testing-library/react": "^11.1.0", 10 | "@testing-library/user-event": "^12.1.10", 11 | "@types/jest": "^26.0.15", 12 | "@types/node": "^12.0.0", 13 | "@types/react": "^17.0.0", 14 | "@types/react-dom": "^17.0.0", 15 | "@types/react-numeric-input": "^2.2.4", 16 | "@types/react-router-dom": "^5.3.2", 17 | "axios": "^1.3.4", 18 | "bootstrap": "^5.1.3", 19 | "dangerously-set-html-content": "^1.0.12", 20 | "filepond": "^4.30.3", 21 | "filepond-plugin-file-validate-size": "^2.2.5", 22 | "filepond-plugin-image-exif-orientation": "^1.0.11", 23 | "filepond-plugin-image-preview": "^4.6.10", 24 | "font-awesome": "^4.7.0", 25 | "history": "^5.1.0", 26 | "js-file-download": "^0.4.12", 27 | "popper": "^1.0.1", 28 | "react": "^17.0.2", 29 | "react-block-ui": "^1.3.5", 30 | "react-dom": "^17.0.2", 31 | "react-filepond": "^7.1.1", 32 | "react-hot-loader": "4.13.0", 33 | "react-markdown": "^8.0.0", 34 | "react-range": "^1.8.11", 35 | "react-redux": "^7.2.6", 36 | "react-router-dom": "6.8.1", 37 | "react-scripts": "4.0.3", 38 | "react-select": "^5.2.1", 39 | "react-toastify": "^8.1.1", 40 | "react-use-websocket": "^4.2.0", 41 | "redux": "^4.1.2", 42 | "redux-thunk": "^2.4.1", 43 | "rehype-highlight": "^5.0.2", 44 | "rehype-raw": "^6.1.1", 45 | "remark-emoji": "^3.0.2", 46 | "remark-gfm": "^3.0.1", 47 | "typescript": "^4.1.2", 48 | "uuidv4": "^6.2.12", 49 | "web-vitals": "^1.0.1" 50 | }, 51 | "scripts": { 52 | "start": "REACT_APP_LOCAL_URL=\"\" react-scripts start", 53 | "build": "REACT_APP_LOCAL_URL=\"\" react-scripts build", 54 | "local-build": "REACT_APP_LOCAL_URL=\"/static\" react-scripts build && mv build/ ../mercury/frontend-dist", 55 | "local-single-site-build": "REACT_APP_LOCAL_URL=\"/static\" react-scripts build && mv build/ ../mercury/frontend-single-site-dist", 56 | "test": "react-scripts test", 57 | "eject": "react-scripts eject" 58 | }, 59 | "eslintConfig": { 60 | "extends": [ 61 | "react-app", 62 | "react-app/jest" 63 | ] 64 | }, 65 | "browserslist": { 66 | "production": [ 67 | ">0.2%", 68 | "not dead", 69 | "not op_mini all" 70 | ], 71 | "development": [ 72 | "last 1 chrome version", 73 | "last 1 firefox version", 74 | "last 1 safari version" 75 | ] 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /frontend/public/favicon-old.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mljar/mercury/c4c50fc3d2545704a0da2a690e53dd6d163b9711/frontend/public/favicon-old.ico -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mljar/mercury/c4c50fc3d2545704a0da2a690e53dd6d163b9711/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 16 | 20 | 29 | 30 | 31 | 36 | 41 | 42 | 47 | Mercury - Turn Jupyter Notebook to Web App 48 | 49 | 50 | 51 | 52 | 53 | 90 | 91 | 92 | 93 |
94 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Mercury", 3 | "name": "Mercury: Easily share your Python notebooks", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/src/Root.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Provider } from "react-redux"; 3 | import { History } from "history"; 4 | import { Store } from "./store"; 5 | import Routes from "./Routes"; 6 | 7 | type Props = { 8 | store: Store; 9 | history: History; 10 | }; 11 | const Root = ({ store, history }: Props) => ( 12 | 13 | 14 | 15 | ); 16 | 17 | export default Root; 18 | -------------------------------------------------------------------------------- /frontend/src/Routes.tsx: -------------------------------------------------------------------------------- 1 | /* eslint react/jsx-props-no-spreading: off */ 2 | import React, { ReactNode, useEffect } from "react"; 3 | import { useDispatch } from "react-redux"; 4 | import { 5 | BrowserRouter as Router, 6 | Routes, 7 | Route, 8 | Outlet, 9 | } from "react-router-dom"; 10 | 11 | import { setToken, setUsername } from "./slices/authSlice"; 12 | // import { fetchVersion } from "./slices/versionSlice"; 13 | import { getSessionId } from "./utils"; 14 | import MainApp from "./views/App"; 15 | import AccountView from "./views/AccountView"; 16 | import HomeView from "./views/HomeView"; 17 | import LoginView from "./views/LoginView"; 18 | import { fetchSite } from "./slices/sitesSlice"; 19 | import RequireAuth from "./components/RequireAuth"; 20 | import WebSocketProvider from "./websocket/Provider"; 21 | import OpenAPIView from "./views/OpenAPIView"; 22 | type Props = { 23 | children: ReactNode; 24 | }; 25 | 26 | function App(props: Props) { 27 | const { children } = props; 28 | return <>{children}; 29 | } 30 | 31 | function AppLayout() { 32 | return ( 33 | 34 | <> 35 | 36 | 37 | 38 | ); 39 | } 40 | 41 | export default function AppRoutes() { 42 | const dispatch = useDispatch(); 43 | 44 | useEffect(() => { 45 | getSessionId(true); 46 | // dispatch(fetchVersion()); 47 | if (localStorage.getItem("token")) { 48 | dispatch(setToken(localStorage.getItem("token"))); 49 | } 50 | if (localStorage.getItem("username")) { 51 | dispatch(setUsername(localStorage.getItem("username"))); 52 | } 53 | 54 | dispatch(fetchSite()); 55 | // eslint-disable-next-line react-hooks/exhaustive-deps 56 | }, []); 57 | 58 | return ( 59 | 60 | 61 | 62 | }> 63 | } /> 64 | 68 | 69 | 70 | } 71 | /> 72 | } /> 73 | } /> 74 | 75 | } /> 76 | 77 | 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /frontend/src/components/AutoRefresh.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from "react"; 2 | import { useDispatch, useSelector } from "react-redux"; 3 | import { fetchCurrentTask } from "../slices/tasksSlice"; 4 | 5 | import { fetchNotebook, getSelectedNotebook } from "../slices/notebooksSlice"; 6 | 7 | type Props = { 8 | siteId: number; 9 | notebookId: number; 10 | }; 11 | 12 | export default function AutoRefresh({ siteId, notebookId }: Props) { 13 | const dispatch = useDispatch(); 14 | const notebook = useSelector(getSelectedNotebook); 15 | 16 | useEffect(() => { 17 | setTimeout(() => { 18 | dispatch(fetchNotebook(siteId, notebookId, true)); 19 | dispatch(fetchCurrentTask(notebookId)); 20 | }, 60000); // every 1 minute 21 | }, [dispatch, siteId, notebookId, notebook]); 22 | 23 | return
; 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/components/DefaultLogo.tsx: -------------------------------------------------------------------------------- 1 | export default process.env.PUBLIC_URL + 2 | process.env.REACT_APP_LOCAL_URL + 3 | "/mercury_logo.svg"; 4 | -------------------------------------------------------------------------------- /frontend/src/components/FileItem.tsx: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import fileDownload from "js-file-download"; 3 | 4 | type Props = { 5 | fname: string; 6 | downloadLink: string; 7 | firstItem: boolean; 8 | lastItem: boolean; 9 | }; 10 | 11 | export default function FileItem({ 12 | fname, 13 | downloadLink, 14 | firstItem, 15 | lastItem, 16 | }: Props) { 17 | const handleDownload = (url: string, filename: string) => { 18 | let token = axios.defaults.headers.common["Authorization"]; 19 | 20 | if (url.includes("s3.amazonaws.com")) { 21 | // we cant do requests to s3 with auth token 22 | // we need to remove auth token before request 23 | delete axios.defaults.headers.common["Authorization"]; 24 | } 25 | 26 | axios 27 | .get(url, { 28 | responseType: "blob", 29 | }) 30 | .then((res) => { 31 | fileDownload(res.data, filename); 32 | }); 33 | 34 | if (url.includes("s3.amazonaws.com")) { 35 | // after request we set token back 36 | axios.defaults.headers.common["Authorization"] = token; 37 | } 38 | }; 39 | 40 | return ( 41 |
52 | {" "} 57 | {fname} 58 |
59 | 67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /frontend/src/components/FilesView.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import React from "react"; 3 | import { useDispatch } from "react-redux"; 4 | import axios from "axios"; 5 | import BlockUi from "react-block-ui"; 6 | import { setView } from "../slices/appSlice"; 7 | import FileItem from "./FileItem"; 8 | 9 | type FilesViewProps = { 10 | files: string[]; 11 | filesState: string; 12 | waiting: boolean; 13 | }; 14 | 15 | export default function FilesView({ 16 | files, 17 | filesState, 18 | waiting, 19 | }: FilesViewProps) { 20 | const dispatch = useDispatch(); 21 | 22 | let filesLinks = []; 23 | 24 | let myFiles = [...files]; 25 | 26 | myFiles.sort(); 27 | 28 | for (let f of myFiles) { 29 | let fname = f.split("/").pop(); 30 | fname = fname?.split("?")[0]; 31 | 32 | if (f && fname) { 33 | let downloadLink = `${axios.defaults.baseURL}${f}`; 34 | if (f.includes("s3.amazonaws.com")) { 35 | downloadLink = f; 36 | } 37 | filesLinks.push( 38 | 44 | ); 45 | } 46 | } 47 | 48 | return ( 49 |
50 |
51 |

52 | Output 53 | Files 54 |

55 | 56 |
57 | {filesState === "loaded" && filesLinks} 58 | {filesState === "loaded" && filesLinks.length === 0 && ( 59 |
No files available for download
60 | )} 61 | {filesState === "unknown" && ( 62 |

Please run the notebook to produce output files ...

63 | )} 64 | {filesState === "loading" &&

Loading files please wait ...

} 65 | {filesState === "error" && ( 66 |
67 | There was an error during loading files. Please try to run the 68 | app again or contact the administrator. 69 |
70 | )} 71 |
72 |
73 |
74 | 75 | 84 |
85 | ); 86 | } 87 | -------------------------------------------------------------------------------- /frontend/src/components/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type FooterProps = { 4 | footerText: string; 5 | }; 6 | 7 | export default function Footer({ footerText }: FooterProps) { 8 | return ( 9 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/components/HomeNavBar.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import React from "react"; 3 | import { Link } from "react-router-dom"; 4 | import LoginButton from "./LoginButton"; 5 | import UserButton from "./UserButton"; 6 | 7 | type NavBarProps = { 8 | isSitePublic: boolean; 9 | username: string; 10 | logoSrc: string; 11 | navbarColor: string; 12 | }; 13 | 14 | export default function NavBar({ 15 | isSitePublic, 16 | username, 17 | logoSrc, 18 | navbarColor, 19 | }: NavBarProps) { 20 | let headerBgClass = ""; 21 | let headerStyle = {}; 22 | if (navbarColor === "") { 23 | headerBgClass = "bg-dark"; 24 | } else { 25 | headerStyle = { 26 | backgroundColor: navbarColor, 27 | }; 28 | } 29 | 30 | return ( 31 |
35 |
36 |
37 |
38 | {logoSrc !== "" && logoSrc !== "loading" && ( 39 | 40 | 41 | 42 | )} 43 | {logoSrc === "loading" && ( 44 | 45 |
46 | 47 | )} 48 |
49 |
53 | {!isSitePublic && username === "" && } 54 | {username !== "" && } 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /frontend/src/components/LoginButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | export default function LoginButton() { 5 | return ( 6 |
7 | 8 | Log in 9 | 10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/MadeWithDiv.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function MadeWithDiv() { 4 | return ( 5 | 6 |
7 |
8 | {" "} 9 | created with{" "} 10 |
11 |
12 | Mercury 21 |
22 |
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/NavBar.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-is-valid */ 2 | import React from "react"; 3 | import { Link } from "react-router-dom"; 4 | import LoginButton from "./LoginButton"; 5 | import UserButton from "./UserButton"; 6 | 7 | type NavBarProps = { 8 | isSitePublic: boolean; 9 | username: string; 10 | logoSrc: string; 11 | navbarColor: string; 12 | }; 13 | 14 | export default function NavBar({ 15 | isSitePublic, 16 | username, 17 | logoSrc, 18 | navbarColor, 19 | }: NavBarProps) { 20 | 21 | let headerBgClass = ""; 22 | let headerStyle = {}; 23 | if (navbarColor === "") { 24 | headerBgClass = "bg-dark"; 25 | } else { 26 | headerStyle = { 27 | backgroundColor: navbarColor, 28 | }; 29 | } 30 | 31 | return ( 32 |
36 | 37 | {logoSrc !== "" && logoSrc !== "loading" && ( 38 | 43 | )} 44 | 45 | 46 | {!isSitePublic && username === "" && } 47 | {username !== "" && } 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/ProFeatureAlert.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | 4 | type ProFeatureProps = { 5 | featureName: string; 6 | }; 7 | 8 | export default function ProFeatureAlert({ featureName }: ProFeatureProps) { 9 | return ( 10 |
11 |
12 |
13 | This is a Pro 14 | feature{" "} 15 |
16 | You are using an open-source version of the Mercury framework. The{' '} 17 | {featureName} is a Pro feature available only for commercial users. 18 | Please consider purchasing the Mercury commercial license. It is 19 | perpetual and comes with additional features, dedicated support, and 20 | allows white-labeling. You can learn more about available licenses on 21 | our{" "} 22 | 23 | website 24 | 25 | . 26 |
27 |
28 |
29 | 30 |