├── .dockerignore ├── .envrc ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yaml │ ├── pypy.yml │ └── website.yml ├── .gitignore ├── .gitlab-ci.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── MANIFEST.in ├── README.md ├── backend ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── backend │ ├── __init__.py │ ├── alembic.ini │ ├── alembic │ │ ├── README │ │ ├── env.py │ │ ├── script.py.mako │ │ └── versions │ │ │ ├── 10dea94d2dc1_add_an_output_dir_override_folder.py │ │ │ ├── 156752e2d05e_added_ids_for_git_commit_parents.py │ │ │ ├── 44c55bb36f57_add_batch_batch_dir_override.py │ │ │ ├── 5720713911df_add_an_output_type_field.py │ │ │ ├── 6f8f309611c5_rename_slam_output_to_output.py │ │ │ ├── 7bb944065bfd_added_data_json_field_for_batch.py │ │ │ ├── 80a0b3ed9dd1_allow_sub_projects.py │ │ │ ├── 847475604161_added_data_json_field_for_ci_commit.py │ │ │ ├── 8d684ac2793b_remove_hardcoded_test_input_data_columns.py │ │ │ ├── 8d84dfaf350d_convert_configuration_to_jsonb_as_.py │ │ │ ├── 93a369d8ac64_changing_recording_to_input_and_adding_database.py │ │ │ ├── b0785fa8ab5a_baseline.py │ │ │ ├── c10df60dd41c_add_deleted_fields.py │ │ │ ├── c44a0b869765_convert_output_data_to_jsonb.py │ │ │ ├── c872ccb5ecf2_added_fields_useful_for_the_cis_.py │ │ │ ├── e37bfe94d6e0_added_misc_json_fields_for_outputs_and_.py │ │ │ └── f6a4bc0b55f8_turning_the_many_metrics_columns_into_a_.py │ ├── api │ │ ├── __init__.py │ │ ├── api.py │ │ ├── auth.py │ │ ├── auto_rois.py │ │ ├── batch.py │ │ ├── commit.py │ │ ├── export_to_folder.py │ │ ├── integrations.py │ │ ├── milestones.py │ │ ├── outputs.py │ │ ├── tuning.py │ │ └── webhooks.py │ ├── backend.py │ ├── clean.py │ ├── clean_big_files.py │ ├── config.py │ ├── database.py │ ├── fs_utils.py │ ├── git_utils.py │ ├── models │ │ ├── Batch.py │ │ ├── CiCommit.py │ │ ├── Output.py │ │ ├── Project.py │ │ ├── TestInput.py │ │ ├── User.py │ │ └── __init__.py │ ├── scripts │ │ ├── README.md │ │ ├── __init__.py │ │ ├── add_output_data_storage.py │ │ ├── add_output_data_user.py │ │ ├── debug_missing_storage.py │ │ ├── delete_orphan_output_dir.py │ │ ├── delete_remaining_data_from_deleted_outputs.py │ │ ├── fix-nobody-manifest.py │ │ ├── fix_bad_output_dir_name.py │ │ ├── gen_parallel_migration.py │ │ ├── init_database.py │ │ ├── migrate.py │ │ ├── migration_utils.py │ │ ├── queries.sql │ │ ├── quota.py │ │ ├── remove_duplicates.py │ │ ├── remove_duplicates_commits.py │ │ ├── save_all_directories.py │ │ └── user_storage_server.py │ └── utils.py ├── init.sh ├── poetry.lock ├── pyproject.toml ├── restore_artifacts.py ├── uwsgi.ini └── wait-for-it.sh ├── development.yml ├── docker-compose.yml ├── fastentrypoints.py ├── production.yml ├── qaboard ├── README.md ├── __init__.py ├── __main__.py ├── api.py ├── bit_accuracy.py ├── check_for_updates.py ├── ci_helpers.py ├── compat.py ├── config.py ├── conventions.py ├── git.py ├── gitlab.py ├── init.py ├── iterators.py ├── optimize.py ├── qa.py ├── qa_aggregate.py ├── run.py ├── runners │ ├── __init__.py │ ├── base.py │ ├── celery.py │ ├── celery_app.py │ ├── jenkins_windows.py │ ├── job.py │ ├── local.py │ └── lsf.py ├── sample_project │ ├── .gitignore │ ├── README.md │ ├── cli_tests │ │ ├── a.txt │ │ ├── b.txt │ │ └── dir │ │ │ └── c.txt │ ├── qa │ │ ├── batches.yaml │ │ ├── main.py │ │ └── metrics.yaml │ ├── qaboard.yaml │ └── subproject │ │ └── qaboard.yaml ├── scripts │ └── get_batch_storage.py └── utils.py ├── qatools └── __init__.py ├── services ├── cantaloupe │ ├── .gitignore │ ├── Dockerfile │ ├── README.md │ └── cantaloupe.properties ├── db │ ├── backup │ ├── init.sh │ ├── postgres.conf │ └── restore ├── nginx │ ├── Dockerfile │ ├── conf.d │ │ └── qaboard.conf │ ├── cors │ ├── fastcgi.conf │ ├── fastcgi_params │ ├── koi-utf │ ├── koi-win │ ├── mime.types │ ├── nginx.conf.template │ ├── proxy_params │ ├── scgi_params │ ├── snippets │ │ ├── fastcgi-php.conf │ │ └── snakeoil.conf │ ├── uwsgi_params │ └── win-utf └── pgadmin │ ├── passfile │ └── servers.json ├── setup.py ├── tests ├── __init__.py ├── test_cli.py ├── test_cli_subproject.py ├── test_conventions.py └── test_iterators.py ├── webapp ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── config-overrides.js ├── notes.md ├── package-lock.json ├── package.json ├── public │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── index.html │ ├── manifest.json │ └── openseadragon │ │ ├── Thumbs.db │ │ ├── _resized │ │ ├── Thumbs.db │ │ ├── button_grouphover.png │ │ ├── button_hover.png │ │ ├── button_pressed.png │ │ ├── button_rest.png │ │ ├── flip_grouphover.png │ │ ├── flip_hover.png │ │ ├── flip_pressed.png │ │ ├── flip_rest.png │ │ ├── fullpage_grouphover.png │ │ ├── fullpage_hover.png │ │ ├── fullpage_pressed.png │ │ ├── fullpage_rest.png │ │ ├── home_grouphover.png │ │ ├── home_hover.png │ │ ├── home_pressed.png │ │ ├── home_rest.png │ │ ├── next_grouphover.png │ │ ├── next_hover.png │ │ ├── next_pressed.png │ │ ├── next_rest.png │ │ ├── previous_grouphover.png │ │ ├── previous_hover.png │ │ ├── previous_pressed.png │ │ ├── previous_rest.png │ │ ├── rotateleft_grouphover.png │ │ ├── rotateleft_hover.png │ │ ├── rotateleft_pressed.png │ │ ├── rotateleft_rest.png │ │ ├── rotateright_grouphover.png │ │ ├── rotateright_hover.png │ │ ├── rotateright_pressed.png │ │ ├── rotateright_rest.png │ │ ├── zoomin_grouphover.png │ │ ├── zoomin_hover.png │ │ ├── zoomin_pressed.png │ │ ├── zoomin_rest.png │ │ ├── zoomout_grouphover.png │ │ ├── zoomout_hover.png │ │ ├── zoomout_pressed.png │ │ └── zoomout_rest.png │ │ ├── button_grouphover.png │ │ ├── button_hover.png │ │ ├── button_pressed.png │ │ ├── button_rest.png │ │ ├── flip_grouphover.png │ │ ├── flip_hover.png │ │ ├── flip_pressed.png │ │ ├── flip_rest.png │ │ ├── fullpage_grouphover.png │ │ ├── fullpage_hover.png │ │ ├── fullpage_pressed.png │ │ ├── fullpage_rest.png │ │ ├── home_grouphover.png │ │ ├── home_hover.png │ │ ├── home_pressed.png │ │ ├── home_rest.png │ │ ├── imagetools_grouphover.png │ │ ├── imagetools_hover.png │ │ ├── imagetools_pressed.png │ │ ├── imagetools_rest.png │ │ ├── next_grouphover.png │ │ ├── next_hover.png │ │ ├── next_pressed.png │ │ ├── next_rest.png │ │ ├── previous_grouphover.png │ │ ├── previous_hover.png │ │ ├── previous_pressed.png │ │ ├── previous_rest.png │ │ ├── rotateleft_grouphover.png │ │ ├── rotateleft_hover.png │ │ ├── rotateleft_pressed.png │ │ ├── rotateleft_rest.png │ │ ├── rotateright_grouphover.png │ │ ├── rotateright_hover.png │ │ ├── rotateright_pressed.png │ │ ├── rotateright_rest.png │ │ ├── selection │ │ ├── Thumbs.db │ │ ├── selection_grouphover.png │ │ ├── selection_hover.png │ │ ├── selection_pressed.png │ │ └── selection_rest.png │ │ ├── selection_cancel_grouphover.png │ │ ├── selection_cancel_hover.png │ │ ├── selection_cancel_pressed.png │ │ ├── selection_cancel_rest.png │ │ ├── selection_confirm_grouphover.png │ │ ├── selection_confirm_hover.png │ │ ├── selection_confirm_pressed.png │ │ ├── selection_confirm_rest.png │ │ ├── selection_grouphover.png │ │ ├── selection_hover.png │ │ ├── selection_pressed.png │ │ ├── selection_rest.png │ │ ├── zoomin_grouphover.png │ │ ├── zoomin_hover.png │ │ ├── zoomin_pressed.png │ │ ├── zoomin_rest.png │ │ ├── zoomout_grouphover.png │ │ ├── zoomout_hover.png │ │ ├── zoomout_pressed.png │ │ └── zoomout_rest.png ├── src │ ├── App.css │ ├── App.js │ ├── App.test.js │ ├── AppNavbar.js │ ├── AppSider.js │ ├── CiCommitList.js │ ├── CiCommitResults.js │ ├── CommitsEvolution.js │ ├── Dashboard.js │ ├── ProjectsList.js │ ├── actions │ │ ├── commit.js │ │ ├── constants.js │ │ ├── index.js │ │ ├── projects.js │ │ ├── selected.js │ │ ├── tuning.js │ │ └── users.js │ ├── components │ │ ├── BatchLogs.js │ │ ├── CommitNavbar.js │ │ ├── CommitRow.js │ │ ├── DoneAtTag.js │ │ ├── EmptyLoading.js │ │ ├── ErrorPage.js │ │ ├── IeDeprecationWarning.js │ │ ├── Parameters.js │ │ ├── authentication │ │ │ ├── Auth.js │ │ │ └── RequireAuth.js │ │ ├── avatars.js │ │ ├── integrations.js │ │ ├── layout.js │ │ ├── messages.js │ │ ├── metricSelect.js │ │ ├── metrics.js │ │ ├── milestones.js │ │ ├── tables.js │ │ ├── tags.js │ │ └── tuning │ │ │ ├── SelectBatches.js │ │ │ ├── TuningExploration.js │ │ │ ├── form_groups.js │ │ │ ├── forms.js │ │ │ └── templates.js │ ├── configureStore.js │ ├── defaults.js │ ├── enhancers │ │ └── monitorReducers.js │ ├── history.js │ ├── index.css │ ├── index.js │ ├── levenshtein.js │ ├── logo.svg │ ├── middleware │ │ └── logger.js │ ├── plugins │ │ └── ExportPlugin.js │ ├── polyfills.js │ ├── react-app-env.d.ts │ ├── reducers │ │ ├── index.js │ │ ├── projects.js │ │ └── users.js │ ├── routes.js │ ├── selectors │ │ ├── batches.js │ │ ├── index.js │ │ └── projects.js │ ├── serviceWorker.js │ ├── setupProxy.js │ ├── todo.md │ ├── utils.js │ └── viewers │ │ ├── OutputCard.js │ │ ├── OutputCardsList.js │ │ ├── OutputViewer.js │ │ ├── bit_accuracy │ │ ├── bitAccuracyViewer.js │ │ └── utils.js │ │ ├── controls.js │ │ ├── flame-graph.js │ │ ├── html.js │ │ ├── images │ │ ├── AutoCrops.js │ │ ├── MultiselectCrops.js │ │ ├── crops.js │ │ ├── filtering.js │ │ ├── filters.js │ │ ├── histogram.js │ │ ├── image-canvas.css │ │ ├── images.js │ │ ├── pixelmatch.js │ │ ├── rgb.js │ │ ├── selection.js │ │ ├── tooltip.js │ │ └── utils.js │ │ ├── plotly.js │ │ ├── slam │ │ └── SlamOutputCard.js │ │ ├── text.js │ │ ├── textViewer.js │ │ ├── todo-image-viewers.md │ │ ├── tof │ │ ├── OrbitControls.js │ │ ├── PCDLoader.js │ │ ├── PointerLockControls.js │ │ ├── Sys_Tools.js │ │ ├── TofOutputCard.js │ │ └── metrics.js │ │ └── videos.js └── tsconfig.json └── website ├── .dockerignore ├── .gitignore ├── Dockerfile ├── README.md ├── babel.config.js ├── blog ├── 2020-06-24-flame-graphs.md ├── 2020-08-10-introducing-QA-Board.md └── authors.yml ├── docs ├── alternatives-and-missing-features.md ├── auto-optimization.md ├── backend-admin │ ├── host-upgrades.md │ └── troubleshooting.md ├── batches-running-on-multiple-inputs.md ├── bit-accuracy.md ├── celery-integration.md ├── ci-integration.md ├── computing-quantitative-metrics.md ├── creating-and-viewing-outputs-files.md ├── dag-pipelines.md ├── debugging-runs-with-an-IDE.md ├── deep-learning.md ├── drafts-contributing.txt ├── faq.md ├── inputs.md ├── installation.md ├── integrating-qa-in-scripts.md ├── introduction.md ├── jenkins-integration.md ├── local-multiprocessing.md ├── lsf-cluster-integration.md ├── metadata-integration-external-databases.md ├── monorepo-support.md ├── project-init.md ├── references-and-milestones.md ├── running-your-code.md ├── specifying-configurations.md ├── start-server.md ├── storage │ ├── artifacts.md │ ├── deleting-old-data.md │ └── where-is-the-data-saved.md ├── triggering-external-ci-tools.md ├── tuning-from-the-webapp.md ├── tuning-workflows.md ├── using-the-qa-cli.md └── visualizations.md ├── docusaurus.config.js ├── makefile ├── package.json ├── sidebars.js ├── src ├── components │ ├── HomepageFeatures.js │ ├── HomepageFeatures.module.css │ ├── feature.js │ └── feature.module.css ├── css │ └── custom.css └── pages │ ├── index.js │ └── index.module.css ├── static ├── .nojekyll └── img │ ├── artifacts-tab.jpg │ ├── bit-accuracy-viewer.jpg │ ├── commit-select-menu.png │ ├── commits-index.jpg │ ├── comparing-new-and-reference.png │ ├── configure-jenkins-build-triggers.png │ ├── copy-windows-output-dir.png │ ├── deep-learning │ ├── qa-wand-infoa.png │ ├── wandb-experiments.png │ ├── wandb-files.png │ ├── wandb-metrics.png │ └── wandb-reports.png │ ├── dynamic-outputs-select.gif │ ├── dynamic-outputs.gif │ ├── dynamic-visualizations-frames.jpg │ ├── export-commit-folder.png │ ├── export-files-commit.jpg │ ├── export-files-output.jpg │ ├── export-files-viz.jpg │ ├── favicon │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── mstile-70x70.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest │ ├── first-outputs.png │ ├── github.png │ ├── gitlab-jenkins.gif │ ├── hidden_by_default_switches.png │ ├── image-perceptural-diff.png │ ├── image-viewer-autoroi.png │ ├── image-viewer-selection.png │ ├── image-viewer.gif │ ├── jenkins-gitlab.png │ ├── local-runs-warning.jpg │ ├── logo-name.png │ ├── logo-text-white.png │ ├── milestone-details.png │ ├── output-files.png │ ├── output-windows-dir.jpg │ ├── plotly-1.png │ ├── plotly-2.png │ ├── plotly-3d-example.png │ ├── projects-index.jpg │ ├── pycharm-debugging-setup.png │ ├── quantitative-metrics-on-viz.png │ ├── quantitative-metrics.png │ ├── rich-metrics-in-card.png │ ├── run-badges.png │ ├── samsung-logo-black.jpg │ ├── samsung-logo-black.png │ ├── samsung-logo.png │ ├── save-as-milestone.png │ ├── select-batch.png │ ├── selecting-local-runs.jpg │ ├── share-github.png │ ├── share.jpg │ ├── slides │ ├── aggregate.jpg │ ├── always-compare.jpg │ ├── commit-list.jpg │ ├── flame-graphs.jpg │ ├── image-viewer.jpg │ ├── plotly-maps.png │ ├── regressions.jpg │ ├── show-files.jpg │ ├── triggers.jpg │ ├── triggers.png │ └── tuning.jpg │ ├── summary-metrics.png │ ├── text-viewer.jpg │ ├── tuning-from-the-ui.jpg │ ├── twemoji_poodle.svg │ ├── twemoji_poodle_greyscale.svg │ ├── ui-triggers.png │ ├── undraw │ ├── undraw_factory_dy0a.svg │ ├── undraw_forming_ideas_0pav.svg │ └── undraw_ideation_2a64.svg │ └── winows-explorer-output-dir.jpg └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | services/ 2 | build/ 3 | dist/ 4 | website/ 5 | 6 | data* 7 | data/ 8 | *.sock 9 | *.egg-info/ 10 | */__pycache__/ 11 | .git/ 12 | */node_modules/ 13 | */build/ 14 | 15 | .dockerignore 16 | Dockerfile 17 | __pycache__ 18 | *.pyc 19 | *.pyo 20 | *.pyd 21 | .cache 22 | *.log 23 | .git 24 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | export DOCKER_BUILDKIT=1 3 | export COMPOSE_DOCKER_CLI_BUILD=1 4 | export BUILDKIT_INLINE_CACHE=1 5 | 6 | # Makes it easier to run the unit tests 7 | export PYTHONPATH=$(pwd) 8 | 9 | # We assume `npm config set strict-ssl false` 10 | export NODE_TLS_REJECT_UNAUTHORIZED=0 11 | export REACT_APP_QABOARD_DOCS_ROOT="/" 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Linux] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Additional context** 32 | Add any other context about the problem here. 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/workflows/pypy.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 2 | name: Upload Python Package 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Set up Python 14 | uses: actions/setup-python@v2 15 | with: 16 | python-version: '3.7' 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install setuptools wheel twine 21 | - name: Build and publish 22 | env: 23 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 24 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 25 | run: | 26 | python setup.py sdist bdist_wheel 27 | twine upload dist/* 28 | -------------------------------------------------------------------------------- /.github/workflows/website.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to GitHub Pages 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | paths: [website/**] 7 | 8 | jobs: 9 | deploy: 10 | name: Deploy to GitHub Pages 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - uses: actions/setup-node@v2 15 | with: 16 | node-version: 14.x 17 | cache: yarn 18 | cache-dependency-path: website/yarn.lock 19 | - name: Build website 20 | working-directory: website 21 | run: | 22 | yarn install --frozen-lockfile 23 | yarn build 24 | 25 | # Popular action to deploy to GitHub Pages: 26 | # Docs: https://github.com/peaceiris/actions-gh-pages#%EF%B8%8F-docusaurus 27 | - name: Deploy to GitHub Pages 28 | uses: peaceiris/actions-gh-pages@v3 29 | with: 30 | github_token: ${{ secrets.GITHUB_TOKEN }} 31 | # Build output to publish to the `gh-pages` branch: 32 | publish_dir: ./website/build 33 | # Assign commit authorship to the official GH-Actions bot for deploys to `gh-pages` branch: 34 | # https://github.com/actions/checkout/issues/13#issuecomment-724415212 35 | # The GH actions bot is used by default if you didn't specify the two fields. 36 | # You can swap them out with your own user credentials. 37 | user_name: github-actions[bot] 38 | user_email: 41898282+github-actions[bot]@users.noreply.github.com -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | stages: 2 | - test 3 | - deploy 4 | 5 | before_script: 6 | - source .envrc 7 | 8 | 9 | unit-tests: 10 | stage: test 11 | retry: 2 # solves frequent filesystem sync issues 12 | script: 13 | - green -vvv --quiet-stdout --run-coverage 14 | 15 | type-check: 16 | stage: test 17 | script: 18 | - mypy -p qaboard --ignore-missing-imports 19 | 20 | lint: 21 | stage: test 22 | script: 23 | - flake8 backend qaboard --count --select=E9,F63,F7,F82 --show-source --statistics 24 | 25 | 26 | deploy:qa: 27 | stage: deploy 28 | only: 29 | - master 30 | script: 31 | - pip install . 32 | 33 | variables: 34 | DOCKER_IMAGE: qaboard 35 | # https://docs.gitlab.com/ee/ci/yaml/#git-clean-flags 36 | GIT_CLEAN_FLAGS: -ffdx --exclude=webapp/node_modules/ 37 | 38 | # CI for the web application and the backend 39 | # TODO: enable it, make it work... 40 | # backend:tests: 41 | # stage: test 42 | # script: 43 | # - cd backend 44 | # # we only check that the syntax is correct 45 | # - pip install . 46 | # webapp:tests: 47 | # stage: test 48 | # script: 49 | # - cd webapp 50 | # - npm ci 51 | # # - npm test 52 | 53 | 54 | # we deploy to the "qa" host. 55 | # Internally we need to deal with proxy issues, tcsh as default shell, etc... 56 | .deploy: &deploy 57 | stage: deploy 58 | script: 59 | - ssh $DOCKER_HOST "bash -c \"cd $(pwd) && source .envrc && ./at-sirc-before-up.py && docker-compose -f docker-compose.yml -f production.yml -f sirc.yml up -d --build\"" 60 | 61 | deploy:production: 62 | <<: *deploy 63 | environment: 64 | name: production 65 | url: https://qa 66 | only: 67 | - master 68 | when: manual 69 | 70 | deploy:staging: 71 | <<: *deploy 72 | variables: 73 | DOCKER_HOST: qa 74 | CI_ENVIRONMENT_SLUG: "" 75 | environment: 76 | name: staging 77 | url: http://qa:9000 78 | only: 79 | - master 80 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | Community participants must adhere to simple rules of conduct: common sense, civility and good neighborliness. Frank discussion is welcomed and encouraged with the goal of arriving at the best technical solution possible. 3 | 4 | Examples of behavior that contributes to creating a positive environment include: 5 | - Using welcoming and inclusive language 6 | - Being respectful of differing viewpoints and experiences 7 | - Respect and acknowledge all contributions, suggestions and comments from the community 8 | - Gracefully accepting constructive criticism. 9 | - Assume people mean well 10 | - Focusing on what is best for the community 11 | - Showing empathy towards other community members, help each other. 12 | 13 | Examples of unacceptable behavior by participants include: 14 | - Discussion about the people participating in development is not welcomed. 15 | - Publishing others' private information, such as a physical or electronic address, without explicit permission. 16 | - Trolling, insulting/derogatory comments, and personal or political attacks. 17 | - Public or private harassment. 18 | - Ad hominem attacks. 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances. 20 | - Other conduct which could reasonably be considered inappropriate in a professional setting. 21 | 22 | ## Corporate sponsorship 23 | QA-Board is currently an open-source project sponsored by Samsung. Maintainers will keep the project open-source friendly by using public tools as the main communication channels. They will keep these open and friendly. 24 | 25 | ## Credits 26 | *This Code of Conduct is adapted from the Contributor Covenant, version 1.4 [[link](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html)]* 27 | 28 | 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | recursive-include qaboard/sample_project/qa * 2 | include qaboard/sample_project/qaboard.yaml 3 | 4 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | Dockerfile 2 | -------------------------------------------------------------------------------- /backend/backend/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | from .database import db_session, Session 3 | 4 | # Configure the flask application 5 | from flask import Flask 6 | from flask_cors import CORS 7 | app = Flask(__name__) 8 | 9 | # This key will be used to sign session cookies 10 | # To generate a key: python -c 'import os; print(os.urandom(16))' 11 | app.secret_key = os.environ.get('SECRET_KEY', 'please-generate-your-own-secret-key') 12 | 13 | # Provide easy access to our git repositories 14 | from .git_utils import Repos 15 | from .config import git_server, qaboard_data_git_dir 16 | repos = Repos(git_server, qaboard_data_git_dir) 17 | 18 | 19 | # Some magic to use sqlalchemy safely with Flask 20 | # http://flask.pocoo.org/docs/0.12/patterns/sqlalchemy/ 21 | from backend.database import db_session, engine, Base 22 | @app.teardown_appcontext 23 | def shutdown_session(exception=None): 24 | db_session.remove() 25 | 26 | import backend.api.api 27 | import backend.api.commit 28 | import backend.api.batch 29 | import backend.api.outputs 30 | import backend.api.webhooks 31 | import backend.api.integrations 32 | import backend.api.tuning 33 | import backend.api.export_to_folder 34 | import backend.api.auto_rois 35 | import backend.api.milestones 36 | import backend.api.auth 37 | 38 | # Enable cross-origin requests to avoid development headcaches 39 | # cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) 40 | CORS(app) 41 | 42 | Base.metadata.create_all(engine) 43 | 44 | # Avoids errors 45 | # > sqlalchemy.exc.OperationalError: (psycopg2.OperationalError) lost synchronization with server: got message type " " 46 | # https://docs.sqlalchemy.org/en/13/core/pooling.html#pooling-multiprocessing 47 | # https://stackoverflow.com/questions/43648075/uwsgi-flask-sqlalchemy-intermittent-postgresql-errors-with-warning-there-is-al 48 | # https://uwsgi-docs.readthedocs.io/en/latest/articles/TheArtOfGracefulReloading.html#preforking-vs-lazy-apps-vs-lazy 49 | # https://stackoverflow.com/questions/41279157/connection-problems-with-sqlalchemy-and-multiple-processes 50 | engine.dispose() 51 | -------------------------------------------------------------------------------- /backend/backend/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 | # is set in backend/env.py 39 | # sqlalchemy.url = postgresql://qaboard:password@localhost:5432/qaboard 40 | 41 | 42 | # Logging configuration 43 | [loggers] 44 | keys = root,sqlalchemy,alembic 45 | 46 | [handlers] 47 | keys = console 48 | 49 | [formatters] 50 | keys = generic 51 | 52 | [logger_root] 53 | level = WARN 54 | handlers = console 55 | qualname = 56 | 57 | [logger_sqlalchemy] 58 | level = WARN 59 | handlers = 60 | qualname = sqlalchemy.engine 61 | 62 | [logger_alembic] 63 | level = INFO 64 | handlers = 65 | qualname = alembic 66 | 67 | [handler_console] 68 | class = StreamHandler 69 | args = (sys.stderr,) 70 | level = NOTSET 71 | formatter = generic 72 | 73 | [formatter_generic] 74 | format = %(levelname)-5.5s [%(name)s] %(message)s 75 | datefmt = %H:%M:%S 76 | -------------------------------------------------------------------------------- /backend/backend/alembic/README: -------------------------------------------------------------------------------- 1 | Generic single-database configuration. -------------------------------------------------------------------------------- /backend/backend/alembic/env.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from alembic import context 3 | from sqlalchemy import create_engine 4 | from logging.config import fileConfig 5 | 6 | from backend.database import engine_url, engine 7 | 8 | # this could be used for automatic migrations 9 | # context.configure(compare_type = True) 10 | 11 | # this is the Alembic Config object, which provides 12 | # access to the values within the .ini file in use. 13 | config = context.config 14 | 15 | # Interpret the config file for Python logging. 16 | # This line sets up loggers basically. 17 | fileConfig(config.config_file_name) 18 | 19 | # add your model's MetaData object here 20 | # for 'autogenerate' support 21 | # from myapp import mymodel 22 | # target_metadata = mymodel.Base.metadata 23 | from backend.models import Base 24 | target_metadata = Base.metadata 25 | 26 | # other values from the config, defined by the needs of env.py, 27 | # can be acquired: 28 | # my_important_option = config.get_main_option("my_important_option") 29 | # ... etc. 30 | 31 | 32 | def run_migrations_offline(): 33 | """Run migrations in 'offline' mode. 34 | 35 | This configures the context with just a URL 36 | and not an Engine, though an Engine is acceptable 37 | here as well. By skipping the Engine creation 38 | we don't even need a DBAPI to be available. 39 | 40 | Calls to context.execute() here emit the given string to the 41 | script output. 42 | 43 | """ 44 | url = config.get_main_option("sqlalchemy.url") 45 | context.configure( 46 | url=engine_url, target_metadata=target_metadata, literal_binds=True) 47 | 48 | with context.begin_transaction(): 49 | context.run_migrations() 50 | 51 | 52 | def run_migrations_online(): 53 | """Run migrations in 'online' mode. 54 | 55 | In this scenario we need to create an Engine 56 | and associate a connection with the context. 57 | 58 | """ 59 | with engine.connect() as connection: 60 | context.configure( 61 | connection=connection, 62 | target_metadata=target_metadata 63 | ) 64 | 65 | with context.begin_transaction(): 66 | context.run_migrations() 67 | 68 | if context.is_offline_mode(): 69 | run_migrations_offline() 70 | else: 71 | run_migrations_online() 72 | -------------------------------------------------------------------------------- /backend/backend/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 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/10dea94d2dc1_add_an_output_dir_override_folder.py: -------------------------------------------------------------------------------- 1 | """add an output_dir_override folder 2 | 3 | Revision ID: 10dea94d2dc1 4 | Revises: 6f8f309611c5 5 | Create Date: 2018-05-23 11:43:43.030517 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '10dea94d2dc1' 14 | down_revision = '6f8f309611c5' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('outputs', sa.Column('output_dir_override', sa.String)) 21 | 22 | def downgrade(): 23 | op.drop_column('outputs', 'output_dir_override') 24 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/156752e2d05e_added_ids_for_git_commit_parents.py: -------------------------------------------------------------------------------- 1 | """Added ids for git commit parents 2 | 3 | Revision ID: 156752e2d05e 4 | Revises: e37bfe94d6e0 5 | Create Date: 2018-07-02 14:53:53.800334 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | from sqlalchemy.orm import sessionmaker, Session as BaseSession 12 | from sqlalchemy.ext.declarative import declarative_base 13 | 14 | Base = declarative_base() 15 | Session = sessionmaker() 16 | 17 | from backend import repos 18 | 19 | # revision identifiers, used by Alembic. 20 | revision = '156752e2d05e' 21 | down_revision = 'e37bfe94d6e0' 22 | branch_labels = None 23 | depends_on = None 24 | 25 | 26 | 27 | 28 | class CiCommit(Base): 29 | __tablename__ = 'ci_commits' 30 | id = sa.Column(sa.String, primary_key=True) 31 | commit_type = sa.Column(sa.String) 32 | project_id = sa.Column(sa.String) 33 | parents = sa.Column(sa.JSON) 34 | 35 | 36 | def upgrade(): 37 | op.add_column('ci_commits', sa.Column('parents', sa.JSON)) 38 | bind = op.get_bind() 39 | session = Session(bind=bind) 40 | ci_commits = session.query(CiCommit).filter(CiCommit.commit_type=='git') 41 | for ci_commit in ci_commits: 42 | print(ci_commit) 43 | try: 44 | git_commit = repos[ci_commit.project_id].commit(ci_commit.id) 45 | parent_ids = [p.hexsha for p in git_commit.parents] 46 | print(parent_ids) 47 | setattr(ci_commit, 'parents', parent_ids) 48 | except Exception as e: 49 | print(e) 50 | setattr(ci_commit, 'parents', []) 51 | # raise e 52 | session.commit() 53 | 54 | 55 | def downgrade(): 56 | # op.drop_column('ci_commits', 'parents') 57 | pass -------------------------------------------------------------------------------- /backend/backend/alembic/versions/44c55bb36f57_add_batch_batch_dir_override.py: -------------------------------------------------------------------------------- 1 | """Add Batch.batch_dir_override 2 | 3 | Revision ID: 44c55bb36f57 4 | Revises: c44a0b869765 5 | Create Date: 2020-08-02 05:39:48.915317 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '44c55bb36f57' 14 | down_revision = 'c44a0b869765' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('batches', sa.Column('batch_dir_override', sa.String)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column('batches', 'batch_dir_override') 25 | # 13:27.40 26 | # 13: -------------------------------------------------------------------------------- /backend/backend/alembic/versions/5720713911df_add_an_output_type_field.py: -------------------------------------------------------------------------------- 1 | """add an output_type field 2 | 3 | Revision ID: 5720713911df 4 | Revises: 10dea94d2dc1 5 | Create Date: 2018-05-24 12:05:10.226540 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '5720713911df' 14 | down_revision = '10dea94d2dc1' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | from sqlalchemy.orm import sessionmaker, Session as BaseSession 20 | from sqlalchemy.ext.declarative import declarative_base 21 | 22 | Base = declarative_base() 23 | Session = sessionmaker() 24 | 25 | class Output(Base): 26 | __tablename__ = 'outputs' 27 | id = sa.Column(sa.Integer, primary_key=True) 28 | 29 | 30 | def upgrade(): 31 | op.add_column('outputs', sa.Column('output_type', sa.String)) 32 | bind = op.get_bind() 33 | session = Session(bind=bind) 34 | # let's home it's all SLAM! 35 | for o in session.query(Output): 36 | setattr(o, 'output_type', 'slam/6dof') 37 | session.commit() 38 | 39 | def downgrade(): 40 | op.drop_column('outputs', 'output_type') 41 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/6f8f309611c5_rename_slam_output_to_output.py: -------------------------------------------------------------------------------- 1 | """rename slam_output to output 2 | 3 | Revision ID: 6f8f309611c5 4 | Revises: f6a4bc0b55f8 5 | Create Date: 2018-05-22 17:22:36.926716 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '6f8f309611c5' 14 | down_revision = 'f6a4bc0b55f8' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.rename_table('slam_outputs', 'outputs') 21 | 22 | def downgrade(): 23 | op.rename_table('outputs', 'slam_outputs') 24 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/7bb944065bfd_added_data_json_field_for_batch.py: -------------------------------------------------------------------------------- 1 | """Added data JSON field for batch 2 | 3 | Revision ID: 7bb944065bfd 4 | Revises: 847475604161 5 | Create Date: 2018-09-26 13:57:11.786313 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '7bb944065bfd' 14 | down_revision = '847475604161' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('batches', sa.Column('data', sa.JSON)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column("batches", "data") 25 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/847475604161_added_data_json_field_for_ci_commit.py: -------------------------------------------------------------------------------- 1 | """Added data JSON field for ci_commit 2 | 3 | Revision ID: 847475604161 4 | Revises: 156752e2d05e 5 | Create Date: 2018-07-22 10:25:08.212386 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '847475604161' 14 | down_revision = '156752e2d05e' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('ci_commits', sa.Column('data', sa.JSON)) 21 | 22 | 23 | def downgrade(): 24 | op.drop_column("ci_commits", "data") 25 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/8d684ac2793b_remove_hardcoded_test_input_data_columns.py: -------------------------------------------------------------------------------- 1 | """Remove hardcoded test-input data columns 2 | 3 | Revision ID: 8d684ac2793b 4 | Revises: 7bb944065bfd 5 | Create Date: 2018-10-09 11:27:09.950444 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = '8d684ac2793b' 14 | down_revision = '7bb944065bfd' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.drop_column('test_inputs', 'stereo_baseline') 21 | op.drop_column('test_inputs', 'duration') 22 | op.drop_column('test_inputs', 'is_wide_angle') 23 | op.drop_column('test_inputs', 'is_dynamic') 24 | op.drop_column('test_inputs', 'is_static') 25 | op.drop_column('test_inputs', 'is_calibration') 26 | op.drop_column('test_inputs', 'is_flickering') 27 | op.drop_column('test_inputs', 'is_low_light') 28 | op.drop_column('test_inputs', 'is_hdr') 29 | op.drop_column('test_inputs', 'motion_axis') 30 | op.drop_column('test_inputs', 'motion_speed') 31 | op.drop_column('test_inputs', 'motion_is_translation') 32 | op.drop_column('test_inputs', 'motion_is_rotation') 33 | 34 | 35 | def downgrade(): 36 | pass 37 | # syntax: 38 | # op.('ci_commits', sa.Column('message', sa.String)) 39 | # 40 | # op.add_column('test_inputs', 'stereo_baseline') 41 | # op.add_column('test_inputs', 'duration') 42 | # op.add_column('test_inputs', 'is_wide_angle') 43 | # op.add_column('test_inputs', 'is_dynamic') 44 | # op.add_column('test_inputs', 'is_static') 45 | # op.add_column('test_inputs', 'is_calibration') 46 | # op.add_column('test_inputs', 'is_flickering') 47 | # op.add_column('test_inputs', 'is_low_light') 48 | # op.add_column('test_inputs', 'is_hdr') 49 | # op.add_column('test_inputs', 'motion_axis') 50 | # op.add_column('test_inputs', 'motion_speed') 51 | # op.add_column('test_inputs', 'motion_is_translation') 52 | # op.add_column('test_inputs', 'motion_is_rotation') 53 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/b0785fa8ab5a_baseline.py: -------------------------------------------------------------------------------- 1 | """baseline 2 | 3 | Revision ID: b0785fa8ab5a 4 | Revises: 5 | Create Date: 2018-04-12 13:43:26.794896 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'b0785fa8ab5a' 14 | down_revision = None 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | pass 21 | 22 | 23 | def downgrade(): 24 | pass 25 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/c44a0b869765_convert_output_data_to_jsonb.py: -------------------------------------------------------------------------------- 1 | """Convert output.data to JSONB 2 | 3 | Revision ID: c44a0b869765 4 | Revises: 8d84dfaf350d 5 | Create Date: 2020-03-25 17:09:15.810682 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | import sqlalchemy.dialects.postgresql as postgresql 11 | 12 | 13 | # revision identifiers, used by Alembic. 14 | revision = 'c44a0b869765' 15 | down_revision = '8d84dfaf350d' 16 | branch_labels = None 17 | depends_on = None 18 | 19 | 20 | def upgrade(): 21 | op.alter_column('outputs', 'data', type_=postgresql.JSONB, postgresql_using='data::text::jsonb') 22 | 23 | 24 | def downgrade(): 25 | op.alter_column('outputs', 'data', type_=sa.dialects.postgresql.JSON, postgresql_using='data::jsonb::text') 26 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/c872ccb5ecf2_added_fields_useful_for_the_cis_.py: -------------------------------------------------------------------------------- 1 | """added fields useful for the CIS integration: commit messages, commit output dir override.. 2 | 3 | Revision ID: c872ccb5ecf2 4 | Revises: 5720713911df 5 | Create Date: 2018-05-24 16:30:06.940926 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'c872ccb5ecf2' 14 | down_revision = '5720713911df' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | from sqlalchemy.orm import sessionmaker, Session as BaseSession 19 | from sqlalchemy.ext.declarative import declarative_base 20 | 21 | Base = declarative_base() 22 | Session = sessionmaker() 23 | 24 | from backend import repos 25 | 26 | class CiCommit(Base): 27 | __tablename__ = 'ci_commits' 28 | id = sa.Column(sa.String, primary_key=True) 29 | message = sa.Column(sa.String) 30 | commit_dir_override = sa.Column(sa.String) 31 | commit_type = sa.Column(sa.String) 32 | 33 | 34 | def upgrade(): 35 | op.add_column('ci_commits', sa.Column('message', sa.String)) 36 | op.add_column('ci_commits', sa.Column('commit_dir_override', sa.String)) 37 | op.add_column('ci_commits', sa.Column('commit_type', sa.String)) 38 | 39 | repo = repos['dvs/psp_swip'] 40 | bind = op.get_bind() 41 | session = Session(bind=bind) 42 | for o in session.query(CiCommit): 43 | setattr(o, 'commit_type', 'git') 44 | try: 45 | commit = repo.commit(o.id) 46 | setattr(o, 'message', commit.message) 47 | except: 48 | setattr(o, 'message', '') 49 | session.commit() 50 | 51 | def downgrade(): 52 | op.drop_column('ci_commits', 'message') 53 | op.drop_column('ci_commits', 'commit_dir_override') 54 | op.drop_column('ci_commits', 'commit_type') 55 | -------------------------------------------------------------------------------- /backend/backend/alembic/versions/e37bfe94d6e0_added_misc_json_fields_for_outputs_and_.py: -------------------------------------------------------------------------------- 1 | """Added misc JSON fields for outputs and inputs 2 | 3 | Revision ID: e37bfe94d6e0 4 | Revises: 93a369d8ac64 5 | Create Date: 2018-05-31 12:26:11.914743 6 | 7 | """ 8 | from alembic import op 9 | import sqlalchemy as sa 10 | 11 | 12 | # revision identifiers, used by Alembic. 13 | revision = 'e37bfe94d6e0' 14 | down_revision = '93a369d8ac64' 15 | branch_labels = None 16 | depends_on = None 17 | 18 | 19 | def upgrade(): 20 | op.add_column('test_inputs', sa.Column('data', sa.JSON)) 21 | op.add_column('outputs', sa.Column('data', sa.JSON)) 22 | 23 | 24 | def downgrade(): 25 | op.drop_column("test_inputs", "data") 26 | op.drop_column("outputs", "data") 27 | -------------------------------------------------------------------------------- /backend/backend/api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/backend/backend/api/__init__.py -------------------------------------------------------------------------------- /backend/backend/api/milestones.py: -------------------------------------------------------------------------------- 1 | """ 2 | CRUD operations for project milestones. 3 | """ 4 | from flask import request, jsonify 5 | from backend import app, db_session 6 | from sqlalchemy.orm.attributes import flag_modified 7 | 8 | from ..models import Project 9 | 10 | 11 | @app.route("/api/v1/project/milestones", methods=['GET', 'POST', 'DELETE']) 12 | @app.route("/api/v1/project/milestones/", methods=['GET', 'POST', 'DELETE']) 13 | def crud_milestones(): 14 | data = request.get_json() 15 | project_id = data['project'] 16 | try: 17 | project = Project.query.filter(Project.id == project_id).one() 18 | except: 19 | return f'ERROR: Project not found', 404 20 | milestones = project.data.get('milestones', {}) 21 | 22 | if request.method == 'GET': 23 | return jsonify(milestones) 24 | 25 | # The body for HTTP DELETE requests can be dropped by proxies (eg uwsgi, nginx...) 26 | # so it's simpler to reuse the POST method... 27 | if request.method=='DELETE' or data.get('delete') in ['true', True]: 28 | del milestones[data['key']] 29 | else: 30 | milestones[data['key']] = data['milestone'] 31 | 32 | project.data['milestones'] = milestones 33 | flag_modified(project, "data") 34 | db_session.add(project) 35 | db_session.commit() 36 | print(f"UPDATE: Milestones {project_id}: {project.data['milestones']}") 37 | return jsonify(project.data['milestones']) 38 | -------------------------------------------------------------------------------- /backend/backend/api/webhooks.py: -------------------------------------------------------------------------------- 1 | """ 2 | Here is the "write" part of the API, to signal more data is ready. 3 | It includes the actual webhooks sent e.g. by Gitlab, as well as 4 | API calls to update batches and outputs. 5 | """ 6 | import sys 7 | import json 8 | 9 | from flask import request, jsonify 10 | from sqlalchemy.orm.attributes import flag_modified 11 | 12 | from backend import app, db_session 13 | from ..models import CiCommit, Output 14 | from ..models.Project import update_project 15 | 16 | 17 | @app.route('/api/v1/commit///batches', methods=['DELETE']) 18 | @app.route('/api/v1/commit///batches/', methods=['DELETE']) 19 | @app.route('/api/v1/commit//batches', methods=['DELETE']) 20 | @app.route('/api/v1/commit//batches/', methods=['DELETE']) 21 | def delete_commit(commit_id, project_id=None): 22 | try: 23 | ci_commits = CiCommit.query.filter(CiCommit.hexsha == commit_id) 24 | if project_id: 25 | ci_commits = ci_commits.filter(CiCommit.project_id == project_id) 26 | ci_commits = ci_commits.all() 27 | except Exception as e: 28 | return f"404 ERROR {e}: {commit_id} in {project_id}", 404 29 | for ci_commit in ci_commits: 30 | print("DELETING", ci_commit) 31 | if ci_commit.hexsha in ci_commit.project.milestone_commits: 32 | return f"403 ERROR: Cannot delete milestones", 403 33 | for batch in ci_commit.batches: 34 | print(f" > {batch}") 35 | stop_status = batch.stop(db_session) 36 | if "error" in stop_status: 37 | return jsonify(stop_status), 500 38 | batch.delete(session=db_session) 39 | return {"status": "OK"} 40 | return f"404 ERROR: Cannot find commit", 404 41 | 42 | 43 | 44 | 45 | @app.route('/webhook/gitlab', methods=['GET', 'POST']) 46 | def gitlab_webhook(): 47 | """If Gitlab calls this endpoint every push, we get avatars and update our local copy of the repo.""" 48 | # https://docs.gitlab.com/ce/user/project/integrations/webhooks.html 49 | data = json.loads(request.data) 50 | print(data, file=sys.stderr) 51 | update_project(data, db_session) 52 | return "{status:'OK'}" 53 | 54 | -------------------------------------------------------------------------------- /backend/backend/backend.py: -------------------------------------------------------------------------------- 1 | from .backend import app 2 | -------------------------------------------------------------------------------- /backend/backend/clean_big_files.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import sys 3 | import datetime 4 | import subprocess 5 | from pathlib import Path 6 | 7 | from sqlalchemy import func, and_, asc, or_, not_ 8 | 9 | from .database import db_session, Session 10 | from .models import Output 11 | 12 | 13 | 14 | now = datetime.datetime.utcnow() 15 | from .clean import parse_time 16 | query_start = now - parse_time('6months') 17 | # query_start = now - parse_time('1day') 18 | 19 | 20 | def main(): 21 | outputs = (db_session 22 | .query(Output) 23 | .filter(Output.created_date > query_start) 24 | .order_by(Output.created_date.desc()) 25 | ) 26 | print(outputs.count()) 27 | nb_bad_files = 0 28 | total_size = 0 29 | errors = [] 30 | for output in outputs.all(): 31 | if not output.output_dir.exists(): 32 | continue 33 | if not (output.output_dir / 'manifest.outputs.json').exists(): 34 | output.update_manifest() 35 | for path in output.output_dir.rglob('*'): 36 | size = path.stat().st_size 37 | if size > 1_000_000_000: 38 | nb_bad_files += 1 39 | total_size += size 40 | print(nb_bad_files, path, size) 41 | try: 42 | path.unlink() 43 | except: 44 | errors.append(path) 45 | # exit(0) 46 | # exit(0) 47 | print(f"{nb_bad_files} files for {total_size}") 48 | if errors: 49 | print(f"ERRORS!") 50 | for e in errors: 51 | print(e) 52 | # 237 53 | with Path('/home/arthurf/errors.txt').open('w') as f: 54 | for e in errors: 55 | print(e, file=f) 56 | 57 | main() -------------------------------------------------------------------------------- /backend/backend/config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from pathlib import Path 3 | 4 | qaboard_url = os.getenv('QABOARD_URL', 'http://qaboard') 5 | 6 | # we clone our repositories locally here to access commit metadata 7 | git_server = os.getenv('GITLAB_HOST', 'https://gitlab.com') 8 | 9 | # Where we save "non-metadata" qaboard data 10 | qaboard_data_dir = Path(os.getenv('QABOARD_DATA_DIR', '/var/qaboard')).resolve() 11 | # Where we save custom per-project groups (currently used only for extra-runs and tuning in api/tuning.py) 12 | qaboard_data_shared_dir = Path(os.environ.get("QABOARD_DATA_SHARED_DIR", qaboard_data_dir / 'shared')) 13 | # Where we clone git repositories 14 | qaboard_data_git_dir = Path(os.environ.get("QABOARD_DATA_GIT_DIR", qaboard_data_dir / 'git')) 15 | 16 | default_storage_root = Path('/mnt/qaboard') 17 | default_outputs_root = default_storage_root 18 | default_artifacts_root = default_storage_root 19 | -------------------------------------------------------------------------------- /backend/backend/database.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from sqlalchemy import create_engine 4 | from sqlalchemy.orm import sessionmaker, scoped_session 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy_utils import database_exists, create_database 7 | 8 | 9 | # For other databases read http://docs.sqlalchemy.org/en/latest/core/engines.html 10 | # http://docs.sqlalchemy.org/en/latest/dialects/mysql.html 11 | # http://docs.sqlalchemy.org/en/latest/core/engines.html 12 | # https://github.com/PyMySQL/mysqlclient-python 13 | db_type = os.getenv('QABOARD_DB_TYPE', 'postgresql') 14 | 15 | db_user = os.getenv('QABOARD_DB_USER', 'qaboard') 16 | db_password = os.getenv('QABOARD_DB_PASSWORD', 'password') 17 | db_host = os.getenv('QABOARD_DB_HOST', 'db') 18 | db_port = os.getenv('QABOARD_DB_PORT', 5432) 19 | db_name = os.getenv('QABOARD_DB_NAME', 'qaboard') 20 | db_echo = bool(os.getenv('QABOARD_DB_ECHO', False)) 21 | 22 | 23 | import ujson 24 | import psycopg2.extras 25 | psycopg2.extras.register_default_json(loads=lambda x: ujson.loads) 26 | psycopg2.extras.register_default_jsonb(loads=lambda x: ujson.loads) 27 | 28 | engine_url = f'{db_type}://{db_user}:{db_password}@{db_host}:{db_port}/{db_name}' 29 | engine = create_engine( 30 | engine_url, 31 | echo=db_echo, 32 | pool_size=100, 33 | max_overflow=10, 34 | json_deserializer=ujson.loads, 35 | json_serializer=ujson.dumps, 36 | ) 37 | 38 | try: 39 | if not database_exists(engine.url): 40 | create_database(engine.url) 41 | except: 42 | print(f'[WARNING] Could not connect to {engine_url}') 43 | pass 44 | 45 | 46 | # This is the recommended integration with Flask 47 | # It scopes session within HTTP requests 48 | from flask import _app_ctx_stack 49 | Session = sessionmaker(bind=engine) 50 | db_session = scoped_session( 51 | sessionmaker( 52 | autocommit=False, 53 | autoflush=False, 54 | bind=engine), 55 | scopefunc=_app_ctx_stack.__ident_func__ 56 | ) 57 | Base = declarative_base() # prints (no name) 58 | Base.query = db_session.query_property() 59 | -------------------------------------------------------------------------------- /backend/backend/models/User.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from flask_login import UserMixin 4 | from sqlalchemy import Column, Integer, String, DateTime, Boolean 5 | 6 | from backend.models import Base 7 | 8 | 9 | class User(Base, UserMixin): 10 | __tablename__ = 'users' 11 | 12 | id = Column(Integer, primary_key=True) 13 | created_date = Column(DateTime, default=datetime.datetime.utcnow, nullable=False) 14 | 15 | user_name = Column(String(), unique=True) 16 | full_name = Column(String(), unique=True) 17 | email = Column(String(), unique=True) 18 | 19 | password = Column(String()) 20 | is_ldap = Column(Boolean(), default=False) 21 | 22 | def __repr__(self): 23 | return (f" omera.txt 64 | 65 | # ? data folder / version -------------------------------------------------------------------------------- /backend/backend/scripts/fix-nobody-manifest.py: -------------------------------------------------------------------------------- 1 | """ 2 | Usage: 3 | - docker-compose -f docker-compose.yml -f development.yml -f sirc.yml run --no-deps backend bash 4 | - [arthurf] python backend/scripts/fix-nobody-manifest.py 5 | """ 6 | import os 7 | import json 8 | import time 9 | import datetime 10 | from pathlib import Path 11 | 12 | from sqlalchemy.orm.attributes import flag_modified 13 | from sqlalchemy import text 14 | from qaboard.utils import total_storage, save_outputs_manifest, outputs_manifest 15 | 16 | from backend.models import Output 17 | from backend.database import db_session, Session 18 | 19 | db_session.autoflush = False 20 | 21 | os.umask(0) 22 | 23 | 24 | 25 | def get_storage(output): 26 | manifest_path = output.output_dir / 'manifest.outputs.json' 27 | if not output.output_dir.exists(): 28 | return 0 29 | try: 30 | with manifest_path.open() as f: 31 | manifest = json.load(f) 32 | except: # e.g. if the manifest does not exist, if it is corrupted... 33 | manifest = outputs_manifest(output.output_dir) 34 | try: 35 | with manifest_path.open('w') as f: 36 | json.dump(manifest, f, indent=2) 37 | except Exception as e: 38 | print(f"[WARNING] Could not write the manifest: {e}") 39 | finally: 40 | return total_storage(manifest) 41 | 42 | 43 | def migrate(): 44 | outputs = (db_session.query(Output) 45 | .filter(text("outputs.created_date > now() - '2 days'::interval")) 46 | .order_by(Output.created_date.desc()) 47 | .enable_eagerloads(False) 48 | ) 49 | updated = 0 50 | now = time.time() 51 | for idx, o in enumerate(outputs): 52 | # try: 53 | manifest_path = o.output_dir / 'manifest.outputs.json' 54 | if 'C:\\' in str(manifest_path): 55 | continue 56 | if not manifest_path.exists() or manifest_path.owner() == 'nobody' or manifest_path.owner() == 'arthurf': 57 | print(o.id, o.output_dir) 58 | if manifest_path.exists(): 59 | manifest_path.unlink() 60 | storage = get_storage(o) 61 | o.data['storage'] = storage 62 | db_session.add(o) 63 | flag_modified(o, "data") 64 | db_session.commit() 65 | # except Exception as e: 66 | # print("[Error-Skipping]", o, e) 67 | # continue 68 | 69 | 70 | return updated 71 | 72 | 73 | if __name__ == '__main__': 74 | while migrate(): 75 | print('Still results to update...') 76 | 77 | 78 | -------------------------------------------------------------------------------- /backend/backend/scripts/init_database.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Initializes or updates the database using information from the filesystem. 4 | """ 5 | import time 6 | from pathlib import Path 7 | 8 | import click 9 | from alembic.config import Config 10 | from alembic import command 11 | 12 | import backend 13 | from backend.database import engine, Session, Base 14 | 15 | 16 | 17 | @click.command() 18 | @click.option('--drop-all', is_flag=True) 19 | @click.option('--verbose', is_flag=True) 20 | def init_database(drop_all, verbose): 21 | if drop_all: 22 | # for tbl in reversed(Base.metadata.sorted_tables): 23 | # engine.execute(tbl.delete()) 24 | if verbose: print('dropping all data') 25 | Base.metadata.drop_all(engine) 26 | if verbose: print('creating schema...') 27 | Base.metadata.create_all(engine) 28 | 29 | stamp_schema_version() 30 | 31 | 32 | def stamp_schema_version(): 33 | """Write the schema version stamp to the database, in case it is missing.""" 34 | with engine.begin() as connection: 35 | alembic_cfg = Config() 36 | alembic_cfg.set_main_option("script_location", "backend:alembic") 37 | path = Path(backend.__file__).parent / 'alembic.ini' 38 | alembic_cfg.config_file_name = str(path) 39 | alembic_cfg.attributes['connection'] = connection 40 | command.stamp(alembic_cfg, "head") 41 | -------------------------------------------------------------------------------- /backend/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -ex 3 | 4 | # at first startup, solves issues when running uwsgi as another user 5 | # chown $UWSGI_UID:$UWSGI_GID /var/qaboard 6 | chmod 777 /var/qaboard 7 | 8 | # Apply migrations if needed 9 | cd /qaboard/backend/backend 10 | alembic upgrade head || alembic downgrade head || alembic stamp head 11 | 12 | 13 | # At SIRC we need to be able to turn into any user to delete their output files 14 | if [ -z ${UWSGI_UID_CAN_SUDO+x} ]; then 15 | if [ -z ${UWSGI_UID+x} ]; then 16 | echo "not-needed" 17 | else 18 | echo "$UWSGI_UID ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers 19 | fi 20 | fi 21 | 22 | 23 | # Start the server 24 | cd /qaboard/backend 25 | uwsgi --listen $UWSGI_LISTEN_QUEUE_SIZE --ini /qaboard/backend/uwsgi.ini 26 | -------------------------------------------------------------------------------- /backend/pyproject.toml: -------------------------------------------------------------------------------- 1 | # https://python-poetry.org/docs/basic-usage/ 2 | # At SIRC, to deal with SSL issues: 3 | 4 | [tool.poetry] 5 | name = "backend" 6 | # name = "qaboard" # used to be backend, poetry issues.. 7 | version = "1.0.1" 8 | description = "Backend for QA-Board" 9 | authors = ["Arthur Flam "] 10 | license="Apache-2.0" 11 | 12 | [tool.poetry.urls] 13 | repository="https://github.com/Samsung/qaboard" 14 | homepage="https://samsung.github.io/qaboard" 15 | 16 | [tool.poetry.dependencies] 17 | python = "3.8.*" 18 | gitpython = "*" # manipulate git repositories 19 | click = "*" # build CLI tools easily 20 | flask = "*" # HTTP server 21 | flask_cors = "*" # (not used?) 22 | flask_login = "*" # Authentication Flows 23 | python-ldap = "*" # Authentication with LDAP 24 | sqlalchemy = "*" # ORM 25 | sqlalchemy_utils = "*" 26 | alembic = "*" # Schema migrations 27 | psycopg2 = "*" # postgresql driver 28 | ujson = "*" # fast json lib 29 | pandas = "*" # stats 30 | numpy = "=>1.17.3" # (needed by pandas - just we ensure python3.8 compat) 31 | scikit-image = "*" # image processing for auto-regions of interest 32 | uwsgi = "*" # server 33 | uwsgitop = "*" # top for uwsgi 34 | qaboard = { path = "..", optional = true } #, develop = true } 35 | scikit-optimize = "*" # needed for auto-tuning, it's specified in qaboard, normally it should be enough but.. 36 | 37 | # Makes incremental docker builds easier 38 | [tool.poetry.extras] 39 | qaboard = ["qaboard"] 40 | 41 | [tool.poetry.dev-dependencies] 42 | pytest = "^3.4" 43 | 44 | [tool.poetry.scripts] 45 | qaboard_clean = 'backend.clean:clean' 46 | qaboard_clean_untracked_hwalg_artifacts = 'backend.clean:clean_untracked_hwalg_artifacts' 47 | qaboard_init_database = 'backend.scripts.init_database:init_database' 48 | -------------------------------------------------------------------------------- /backend/restore_artifacts.py: -------------------------------------------------------------------------------- 1 | import os 2 | import requests 3 | 4 | 5 | r = requests.get('http://qa:5002/api/v1/projects') 6 | projects = r.json() 7 | for project, data in projects.items(): 8 | if 'product' not in project: 9 | continue 10 | if 'HM1' in project: 11 | continue 12 | print(project) 13 | milestones = data.get('data').get('milestones', {}) 14 | for m in milestones.values(): 15 | commit = m['commit'] 16 | print('>', commit) 17 | # continue 18 | os.chdir(f'/home/arthurf/{project}') 19 | os.system(f"git checkout {commit}") 20 | # exit(0) 21 | # if not workppace 22 | os.system("git checkout develop qatools.yaml") 23 | os.system("qa save-artifacts") 24 | os.system("git reset --hard") 25 | os.system("git clean -fd") 26 | 27 | -------------------------------------------------------------------------------- /development.yml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | 3 | services: 4 | # don't show logs from those services 5 | pgadmin: 6 | logging: 7 | driver: none 8 | db: 9 | logging: 10 | driver: none 11 | cantaloupe: 12 | logging: 13 | driver: none 14 | metabase: 15 | logging: 16 | driver: none 17 | proxy: 18 | environment: 19 | - NGINX_BIN=nginx-debug 20 | 21 | backend: 22 | volumes: 23 | - ./qaboard:/qaboard/qaboard 24 | - ./qaboard:/usr/local/lib/python3.8/site-packages/qaboard 25 | - ./backend:/qaboard/backend 26 | - ./setup.py:/qaboard/setup.py 27 | environment: 28 | - QABOARD_DB_ECHO=true 29 | - UWSGI_STATS=true 30 | - FLASK_APP=backend 31 | - FLASK_ENV=development 32 | - FLASK_DEBUG=1 33 | # Start a flask debug server instead of the full uwsgi 34 | # To use it, comment out, as well as frontent:environment below 35 | working_dir: /qaboard/backend 36 | command: flask run --host 0.0.0.0 --with-threads --port 5152 37 | # in same cases (NFS+squash-root), it can be useful to be a usual sudoer user... 38 | # user: "11611:10" 39 | # command: > 40 | # bash -c " 41 | # echo 'user ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers 42 | # && su user -c 'flask run --host 0.0.0.0 --with-threads --port 5152' 43 | # " 44 | ports: 45 | - "${QABOARD_DEV_BACKEND_PORT:-5152}:5152" 46 | # logging: 47 | # driver: none 48 | 49 | 50 | frontend: 51 | volumes: 52 | - ./webapp:/webapp 53 | environment: 54 | # Should we comment it out by default? 55 | - HOST=0.0.0.0 56 | - DANGEROUSLY_DISABLE_HOST_CHECK=true 57 | - CHOKIDAR_USEPOLLING=true 58 | # - REACT_EDITOR=code 59 | # Relay API requests to your development server 60 | - REACT_APP_QABOARD_API_HOST=http://backend:5152 61 | - REACT_APP_QABOARD_HOST=http://proxy:5151 62 | command: npm start 63 | user: "11611:10" 64 | ports: 65 | - "${QABOARD_DEV_FRONTEND_PORT:-3000}:3000" 66 | depends_on: 67 | - proxy 68 | - backend 69 | -------------------------------------------------------------------------------- /qaboard/README.md: -------------------------------------------------------------------------------- 1 | # `qaboard` python package 2 | 3 | ## How does this work? 4 | - The `qa` CLI executable is implemented by _qa.py_. We notably use the [`click`](https://click.palletsprojects.com/en/7.x/) package. 5 | - _config.py_ is responsible for finding all the details about the current run: project configuration, git information, where to store results, whether we're it's running as part of some Continuous Integration... 6 | - The different files each are responsible for specific parts of the application: 7 | * _api.py_ talks to the QA-Board server: creating new runs, etc. 8 | * _git.py_ makes calls to `git` 9 | * _iterators.py_ implements the logic to find/filter batch inputs, or iterate over tuning parameters... 10 | * _conventions.py_ decides on e.g. where to store results... 11 | * Some of the more complicated `qa` tasks have their own file: _optimize.py_ for the parameter optimizer, _check_bit_accuracy.py_... 12 | - In the repository's top-level, `setup.py` provides: 13 | * the required glue to make the project installable 14 | * the list of third-party package requirements 15 | * the magic to make `qa` an available CLI executable 16 | 17 | ## Development 18 | To make debugging easy, you'll want `qa` to use the code from your working directory: 19 | 20 | ```bash 21 | # from the top-level of the repository 22 | pip install --editable . 23 | ``` 24 | -------------------------------------------------------------------------------- /qaboard/__init__.py: -------------------------------------------------------------------------------- 1 | # need to be update setup.py as well 2 | __version__ = '1.0.1' 3 | from .check_for_updates import check_for_updates 4 | check_for_updates() 5 | 6 | from .config import on_windows, on_linux, on_lsf, on_vdi, is_ci, config 7 | from .utils import merge 8 | from .conventions import slugify 9 | from .qa import qa 10 | -------------------------------------------------------------------------------- /qaboard/__main__.py: -------------------------------------------------------------------------------- 1 | from .qa import main 2 | main() 3 | -------------------------------------------------------------------------------- /qaboard/runners/__init__.py: -------------------------------------------------------------------------------- 1 | from typing import Dict, Type 2 | 3 | from .job import Job, JobGroup 4 | from .base import BaseRunner 5 | from .lsf import LsfRunner 6 | from .local import LocalRunner 7 | from .celery import CeleryRunner 8 | from .jenkins_windows import JenkinsWindowsRunner 9 | 10 | runners: Dict[str, Type[BaseRunner]] = { 11 | 'local': LocalRunner, 12 | 'lsf': LsfRunner, 13 | 'celery': CeleryRunner, 14 | 'windows': JenkinsWindowsRunner, 15 | } 16 | 17 | 18 | ## We may not want to promote this, better have people contribute more runners to the project, 19 | ## rather than encourage everyone to write their own wrapper. Also it lets us keep the API internal. 20 | ## Besides, when we call stop_command server-side, having custom runners makes things even more complex 21 | # def register_runner(runnerType): 22 | # assert hasattr(runnerType, 'type') 23 | # assert hasattr(runnerType, 'start_jobs') 24 | # assert hasattr(runnerType, 'stop_jobs') 25 | # runners[runnerType.type] = runnerType 26 | -------------------------------------------------------------------------------- /qaboard/runners/base.py: -------------------------------------------------------------------------------- 1 | from typing import List, Dict, Any 2 | 3 | from .job import Job 4 | from ..run import RunContext 5 | 6 | 7 | class BaseRunner(): 8 | type: str 9 | 10 | def __init__(self, run_context : RunContext): 11 | self.run_context = run_context 12 | 13 | # Right now we don't call start directy, only start_jobs, so feel free to add parameters 14 | # if need be, we'll refactor all runners later. 15 | def start(self, blocking=True): 16 | raise NotImplementedError 17 | 18 | @staticmethod 19 | def start_jobs(jobs: List[Job], job_options: Dict[str, Any], blocking=True): 20 | raise NotImplementedError 21 | 22 | @staticmethod 23 | def stop_jobs(jobs: List[Job], job_options: Dict[str, Any]): 24 | raise NotImplementedError 25 | -------------------------------------------------------------------------------- /qaboard/runners/celery_app.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | 4 | from celery import Celery 5 | 6 | app = Celery('celery_app') 7 | app.conf.update( 8 | broker_url=os.environ.get('CELERY_BROKER_URL', 'pyamqp://guest:guest@qaboard:5672//'), 9 | result_backend=os.environ.get('CELERY_RESULT_BACKEND', 'rpc://'), 10 | task_serializer='pickle', 11 | accept_content=['pickle', 'json'], 12 | result_serializer='pickle', 13 | enable_utc=True, 14 | ) 15 | 16 | from qaboard.config import config 17 | celery_config = config.get('runners', {}).get('celery', {}) 18 | app.conf.update(**celery_config) 19 | 20 | 21 | 22 | @app.task(bind=True, name=celery_config.get('qaboard_task_name', "qaboard")) 23 | def start(self, job, cwd=None, env=None): 24 | # https://docs.celeryproject.org/en/stable/userguide/tasks.html#task-request-info 25 | print('Executing task id {0.id}, groupID: {0.group}'.format(self.request)) 26 | 27 | pipe = subprocess.PIPE 28 | # print("job.run_context.command", job.run_context.command) 29 | # print("env", env) 30 | with subprocess.Popen(job.run_context.command, shell=True, 31 | encoding='utf-8', 32 | # Avoid issues with code outputing malformed unicode 33 | # https://docs.python.org/3/library/codecs.html#error-handlers 34 | errors='surrogateescape', 35 | cwd=cwd if cwd else job.run_context.job_options['cwd'], 36 | env=env, 37 | stdout=pipe, stderr=pipe) as process: 38 | for line in process.stdout: 39 | print(line, end='') 40 | process.wait() 41 | return process.returncode 42 | 43 | -------------------------------------------------------------------------------- /qaboard/runners/local.py: -------------------------------------------------------------------------------- 1 | import os 2 | import subprocess 3 | from pathlib import Path 4 | from typing import List, Dict, Any 5 | 6 | import click 7 | 8 | from .base import BaseRunner 9 | from .job import Job 10 | from ..run import RunContext 11 | 12 | 13 | class LocalRunner(BaseRunner): 14 | type = "local" 15 | 16 | def __init__(self, run_context : RunContext): 17 | self.run_context = run_context 18 | 19 | 20 | def start(self, blocking=True, cwd=None): 21 | process = subprocess.run( 22 | self.run_context.command, shell=True, 23 | encoding='utf-8', 24 | # Avoid issues with code outputing malformed unicode 25 | # https://docs.python.org/3/library/codecs.html#error-handlers 26 | errors='surrogateescape', 27 | cwd=cwd if cwd else self.run_context.job_options['cwd'], 28 | ) 29 | return process.returncode 30 | 31 | @staticmethod 32 | def start_jobs(jobs: List[Job], job_options: Dict[str, Any], blocking=True): 33 | if not blocking: 34 | click.secho(f'WARNING: We currently dont support non-blocking local runs.', fg='yellow', err=True) 35 | 36 | from joblib import Parallel, delayed 37 | # multiprocessing will try to reimport qaboard, which relies on the CWD 38 | cwd = os.getcwd() 39 | if 'cwd' in job_options: 40 | os.chdir(job_options['cwd']) 41 | Parallel( 42 | n_jobs=job_options.get('concurrency'), 43 | verbose=int(os.environ.get('QA_BATCH_VERBOSE', 0)), 44 | )(delayed(lambda j: j.start(cwd=cwd))(j) for j in jobs) 45 | os.chdir(cwd) 46 | 47 | 48 | @staticmethod 49 | def stop_jobs(jobs: List[Job], job_options: Dict[str, Any]): 50 | return NotImplementedError -------------------------------------------------------------------------------- /qaboard/sample_project/.gitignore: -------------------------------------------------------------------------------- 1 | output/ 2 | 3 | -------------------------------------------------------------------------------- /qaboard/sample_project/README.md: -------------------------------------------------------------------------------- 1 | # Sample project using QA-Board 2 | -------------------------------------------------------------------------------- /qaboard/sample_project/cli_tests/a.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/qaboard/sample_project/cli_tests/a.txt -------------------------------------------------------------------------------- /qaboard/sample_project/cli_tests/b.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/qaboard/sample_project/cli_tests/b.txt -------------------------------------------------------------------------------- /qaboard/sample_project/cli_tests/dir/c.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/qaboard/sample_project/cli_tests/dir/c.txt -------------------------------------------------------------------------------- /qaboard/sample_project/qa/batches.yaml: -------------------------------------------------------------------------------- 1 | # Read about how you can define batches of runs here: 2 | # https://samsung.github.io/qaboard/docs/batches-running-on-multiple-inputs/ 3 | # It can be as simple as 4 | my-batch: 5 | inputs: 6 | - A.jpg 7 | - B.jpg 8 | 9 | 10 | # There is a lot of flexibility to specify different choices of databases, configurations, 11 | # create "test matrices", or use standard YAML features to avoid repetition 12 | -------------------------------------------------------------------------------- /qaboard/sample_project/qa/metrics.yaml: -------------------------------------------------------------------------------- 1 | # you must define metadata about all your objective figures of merits 2 | available_metrics: 3 | # loss: 4 | # label: My Loss Function # defaults to the key 5 | # short_label: Loss # defaults to the label 6 | # smaller_is_better: true # default 7 | # target: 0.01 # optionnal 8 | # # used for display 9 | # scale: 1.0 # default 10 | # suffix: % # default: '' 11 | 12 | is_failed: 13 | label: Crashed 14 | short_label: Crashed 15 | scale: 100 16 | suffix: "%" 17 | target: 0 18 | smaller_is_better: true 19 | plot_scale: linear # | default: log 20 | 21 | 22 | # Below we define which metrics the GUI should show 23 | default_metric: is_failed 24 | 25 | # will be shown in the summary histogramms 26 | summary_metrics: 27 | - is_failed 28 | 29 | # will be shown in the results table and in the output cards 30 | main_metrics: 31 | - is_failed 32 | 33 | # In the history view, only those metrics will be available in the plot showing results over time 34 | # [OPTIONNAL]: defaults to the summary_metrics 35 | # dashboard_evolution_metrics: 36 | # - is_failed 37 | # In the history view, only those metrics will be shown in the summary histograms 38 | # [OPTIONNAL]: defaults to the summary_metrics 39 | # dashboard_metrics: 40 | # - is_failed 41 | -------------------------------------------------------------------------------- /qaboard/sample_project/subproject/qaboard.yaml: -------------------------------------------------------------------------------- 1 | # Each qaboard.yaml in your repository will be a separate QA-Board project 2 | # 3 | # 4 | # Configuration is inherited from the parent projects, merged deeply 5 | # 6 | # All the paths you use here are meant relative to the root of the repository 7 | # When using qa commands, the working directory will change to the repository root 8 | # 9 | # We support a tiny bit of magic for common use-cases 10 | # Let's say you want to append a visualization to ones defined in parent projects, 11 | # You can do this 12 | # 13 | # outputs: 14 | # visualizations 15 | # - super 16 | # - path: output.jpg 17 | # name: My new output 18 | -------------------------------------------------------------------------------- /qaboard/scripts/get_batch_storage.py: -------------------------------------------------------------------------------- 1 | """ 2 | Used this script to get a batch's total storage. 3 | 4 | ``` 5 | export QABOARD_HOST=http://localhost:5152 6 | python get_batch_storage.py some/project commit_id [batch_label] 7 | #=> batch_label: 240MB for 30 outputs 8 | ``` 9 | """ 10 | import os 11 | import sys 12 | import json 13 | 14 | import click 15 | import requests 16 | from qaboard.api import url_to_dir 17 | 18 | qaboard_host = os.environ.get("QABOARD_HOST", "http://qa") 19 | 20 | def fetch_batch(batch_id): 21 | r = requests.get(f"{qaboard_host}/api/v1/batch/{batch_id}/") 22 | print(r.text) 23 | return r.json() 24 | 25 | def fetch_commit(hexsha, project): 26 | r = requests.get( 27 | f"{qaboard_host}/api/v1/commit/", 28 | params={ 29 | "commit": hexsha, 30 | "project": project, 31 | "with_outputs": True, 32 | } 33 | ) 34 | return r.json() 35 | 36 | 37 | 38 | def used_storage(batch): 39 | total_size = 0 40 | for output in batch['outputs'].values(): 41 | if output['is_pending']: 42 | continue 43 | output_storage = output.get('data', {}).get('storage', {}) 44 | if output_storage: 45 | total_size += output_storage 46 | continue 47 | 48 | output_dir = url_to_dir(output['output_dir_url']) 49 | manifest_path = output_dir / 'manifest.outputs.json' 50 | print(output) 51 | with manifest_path.open() as f: 52 | manifest = json.load(f) 53 | sizes = [f['st_size'] for f in manifest.values()] 54 | total_size += sum(sizes) 55 | return total_size 56 | 57 | 58 | if __name__ == "__main__": 59 | project = sys.argv[1] 60 | hexsha = sys.argv[2] 61 | batch_label = sys.argv[3] if len(sys.argv) > 3 else None 62 | # print(project, hexsha, batch_label) 63 | commit = fetch_commit(hexsha, project) 64 | # print(commit) 65 | 66 | for label, batch in commit['batches'].items(): 67 | if batch_label and batch_label != label: 68 | continue 69 | storage = used_storage(batch) 70 | print(f"{label}: {storage / 1e6:.01f}MB for {len(batch['outputs'])} outputs") -------------------------------------------------------------------------------- /qatools/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | For backward-compatibility: once qaboard was imported as "import qatools" 3 | Inspired by https://stackoverflow.com/a/24324577/5993501 4 | """ 5 | # FIXME: for backward compatibility, maybe suffice to re-install the latest qatools @ispq, and in the docs/installation list both 6 | # pip install 'git+ssh://git@gitlab-srv/common-infrastructure/qatools@f2f993ea8' 7 | import sys 8 | import qaboard 9 | 10 | modules = [ 11 | # user only ever used config... 12 | 'qaboard', 13 | 'qaboard.config', 14 | # 'qaboard.check_for_updates', 15 | # 'qaboard.utils', 16 | # 'qaboard.conventions', 17 | # 'qaboard.iterators', 18 | # 'qaboard.qa', 19 | # 'qaboard.lsf', 20 | # 'qaboard.api' 21 | ] 22 | for m in modules: 23 | sys.modules[m.replace('qaboard', 'qatools')] = sys.modules[m] 24 | 25 | -------------------------------------------------------------------------------- /services/cantaloupe/.gitignore: -------------------------------------------------------------------------------- 1 | config.json 2 | -------------------------------------------------------------------------------- /services/cantaloupe/README.md: -------------------------------------------------------------------------------- 1 | # Adapted from [cihm-cantaloupe](https://github.com/c7a/cihm-cantaloupe) 2 | `cihm-cantaloupe` is Canadiana's [Cantaloupe](https://cantaloupe-project.github.io/) configuration. 3 | -------------------------------------------------------------------------------- /services/db/backup: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Creates a backup 3 | set -ex 4 | 5 | if [ "$1" = "" ]; then 6 | backup=$(date -I).dump 7 | else 8 | backup=$1 9 | fi 10 | 11 | if [ -z ${AS_USER_NAME+x} ]; then 12 | pg_dump -Fc > "/backups/$backup" 13 | else 14 | # we should use gosu but it's nice to use the official image as-is 15 | addgroup -S $AS_USER_GROUP -g $AS_USER_GID group || true 16 | adduser --disabled-password $AS_USER_NAME -u $AS_USER_UID -G $AS_USER_GROUP || true 17 | su $AS_USER_NAME -c "pg_dump -Fc > '/backups/$backup'" 18 | fi 19 | 20 | # TODO: 21 | # - Remove backups older than X 22 | # - upload the backup to e.g. S3 23 | -------------------------------------------------------------------------------- /services/db/init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | cp /postgres.host.conf /var/lib/postgresql/data/postgres.conf 3 | chown postgres /var/lib/postgresql/data/postgres.conf 4 | 5 | if [[ $# -eq 0 ]] ; then 6 | exec docker-entrypoint.sh postgres 7 | fi 8 | exec docker-entrypoint.sh "$@" 9 | -------------------------------------------------------------------------------- /services/db/restore: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Restore from a backup 3 | set -ex 4 | 5 | export PGPASSWORD=$POSTGRES_PASSWORD 6 | export PGUSER=$POSTGRES_USER 7 | export PGDATABASE=$POSTGRES_DB 8 | 9 | 10 | dropdb $PGDATABASE 11 | createdb -T template0 qaboard 12 | pg_restore --verbose -d $PGDATABASE < $1 13 | # --clean --create -l 14 | # If you migrate from a DB with a non-default owner, it can be useful to add 15 | # --no-acl --no-owner -------------------------------------------------------------------------------- /services/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | # We don't use this Dockerfile, instead rely on the official nginx image 2 | # But we lost full support for webdav, so 3 | # TODO: Create an image to restore webdav. 4 | # Likely exactly like below but need to test it! 5 | FROM ubuntu:bionic 6 | RUN apt-get update && apt-get install -y nginx nginx-extras apache2-utils 7 | 8 | RUN echo 'deb http://nginx.org/packages/ubuntu/ bionic nginx' > /etc/apt/sources.list.d/nginx.list && \ 9 | echo 'deb-src http://nginx.org/packages/ubuntu/ bionic nginx' >> /etc/apt/sources.list.d/nginx.list && \ 10 | apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --keyserver-options http-proxy=$PROXY --recv-keys ABF5BD827BD9BF62 && \ 11 | # nginx-extra instead of just -full or smaller for WebDav and DAV Ext 12 | apt-get update -qq && apt-get install -y --no-install-recommends nginx-extras && \ 13 | rm /etc/nginx/sites-enabled/default 14 | -------------------------------------------------------------------------------- /services/nginx/cors: -------------------------------------------------------------------------------- 1 | # http://nginx.org/en/docs/http/ngx_http_headers_module.html#add_header 2 | if ($request_method ~* "(GET|POST|PUT|DELETE)") { 3 | add_header "Access-Control-Allow-Origin" * always; 4 | } 5 | # Preflighted requests 6 | if ($request_method = OPTIONS ) { 7 | add_header "Access-Control-Allow-Origin" *; 8 | add_header "Access-Control-Allow-Methods" "GET, POST, OPTIONS, HEAD"; 9 | add_header "Access-Control-Allow-Headers" "Authorization, Origin, X-Requested-With, Content-Type, Accept"; 10 | return 200; 11 | } 12 | -------------------------------------------------------------------------------- /services/nginx/fastcgi.conf: -------------------------------------------------------------------------------- 1 | 2 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 3 | fastcgi_param QUERY_STRING $query_string; 4 | fastcgi_param REQUEST_METHOD $request_method; 5 | fastcgi_param CONTENT_TYPE $content_type; 6 | fastcgi_param CONTENT_LENGTH $content_length; 7 | 8 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 9 | fastcgi_param REQUEST_URI $request_uri; 10 | fastcgi_param DOCUMENT_URI $document_uri; 11 | fastcgi_param DOCUMENT_ROOT $document_root; 12 | fastcgi_param SERVER_PROTOCOL $server_protocol; 13 | fastcgi_param REQUEST_SCHEME $scheme; 14 | fastcgi_param HTTPS $https if_not_empty; 15 | 16 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 17 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 18 | 19 | fastcgi_param REMOTE_ADDR $remote_addr; 20 | fastcgi_param REMOTE_PORT $remote_port; 21 | fastcgi_param SERVER_ADDR $server_addr; 22 | fastcgi_param SERVER_PORT $server_port; 23 | fastcgi_param SERVER_NAME $server_name; 24 | 25 | # PHP only, required if PHP was built with --enable-force-cgi-redirect 26 | fastcgi_param REDIRECT_STATUS 200; 27 | -------------------------------------------------------------------------------- /services/nginx/fastcgi_params: -------------------------------------------------------------------------------- 1 | 2 | fastcgi_param QUERY_STRING $query_string; 3 | fastcgi_param REQUEST_METHOD $request_method; 4 | fastcgi_param CONTENT_TYPE $content_type; 5 | fastcgi_param CONTENT_LENGTH $content_length; 6 | 7 | fastcgi_param SCRIPT_NAME $fastcgi_script_name; 8 | fastcgi_param REQUEST_URI $request_uri; 9 | fastcgi_param DOCUMENT_URI $document_uri; 10 | fastcgi_param DOCUMENT_ROOT $document_root; 11 | fastcgi_param SERVER_PROTOCOL $server_protocol; 12 | fastcgi_param REQUEST_SCHEME $scheme; 13 | fastcgi_param HTTPS $https if_not_empty; 14 | 15 | fastcgi_param GATEWAY_INTERFACE CGI/1.1; 16 | fastcgi_param SERVER_SOFTWARE nginx/$nginx_version; 17 | 18 | fastcgi_param REMOTE_ADDR $remote_addr; 19 | fastcgi_param REMOTE_PORT $remote_port; 20 | fastcgi_param SERVER_ADDR $server_addr; 21 | fastcgi_param SERVER_PORT $server_port; 22 | fastcgi_param SERVER_NAME $server_name; 23 | 24 | # PHP only, required if PHP was built with --enable-force-cgi-redirect 25 | fastcgi_param REDIRECT_STATUS 200; 26 | -------------------------------------------------------------------------------- /services/nginx/nginx.conf.template: -------------------------------------------------------------------------------- 1 | # https://www.nginx.com/resources/wiki/start/topics/examples/full/ 2 | user ${NGINX_USER} uucp; 3 | worker_processes auto; 4 | 5 | # https://github.com/docker-library/docs/tree/master/nginx#running-nginx-as-a-non-root-user 6 | pid /tmp/nginx.pid; 7 | 8 | include /etc/nginx/modules-enabled/*.conf; 9 | 10 | events { 11 | worker_connections 4096; 12 | } 13 | 14 | http { 15 | # Basic Settings 16 | sendfile on; 17 | tcp_nopush on; 18 | tcp_nodelay on; 19 | keepalive_timeout 65; 20 | types_hash_max_size 2048; 21 | # server_tokens off; 22 | 23 | # server_names_hash_bucket_size 64; 24 | # server_name_in_redirect off; 25 | 26 | include /etc/nginx/mime.types; 27 | default_type application/octet-stream; 28 | 29 | # SSL Settings 30 | ssl_session_cache shared:SSL:10m; 31 | ssl_session_timeout 1h; 32 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2; # Dropping SSLv3, ref: POODLE 33 | ssl_prefer_server_ciphers on; 34 | ssl_ciphers EECDH+CHACHA20:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5; 35 | 36 | 37 | # https://github.com/docker-library/docs/tree/master/nginx#running-nginx-as-a-non-root-user 38 | client_body_temp_path /tmp/client_temp; 39 | proxy_temp_path /tmp/proxy_temp_path; 40 | fastcgi_temp_path /tmp/fastcgi_temp; 41 | uwsgi_temp_path /tmp/uwsgi_temp; 42 | scgi_temp_path /tmp/scgi_temp; 43 | 44 | # Logging Settings 45 | access_log /tmp/access.log; 46 | error_log /tmp/error.log; 47 | 48 | # Gzip Settings 49 | gzip on; 50 | gzip_disable "msie6"; 51 | gzip_buffers 16 8k; 52 | gzip_vary on; 53 | gzip_min_length 1000; 54 | gzip_proxied expired no-cache no-store private auth; 55 | gzip_comp_level 6; 56 | # gzip_proxied any; 57 | # gzip_http_version 1.1; 58 | gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript text/csv; 59 | 60 | # Virtual Host Configs 61 | include /etc/nginx/conf.d/*.conf; 62 | } 63 | -------------------------------------------------------------------------------- /services/nginx/proxy_params: -------------------------------------------------------------------------------- 1 | proxy_set_header Host $http_host; 2 | proxy_set_header X-Real-IP $remote_addr; 3 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 4 | proxy_set_header X-Forwarded-Proto $scheme; 5 | -------------------------------------------------------------------------------- /services/nginx/scgi_params: -------------------------------------------------------------------------------- 1 | 2 | scgi_param REQUEST_METHOD $request_method; 3 | scgi_param REQUEST_URI $request_uri; 4 | scgi_param QUERY_STRING $query_string; 5 | scgi_param CONTENT_TYPE $content_type; 6 | 7 | scgi_param DOCUMENT_URI $document_uri; 8 | scgi_param DOCUMENT_ROOT $document_root; 9 | scgi_param SCGI 1; 10 | scgi_param SERVER_PROTOCOL $server_protocol; 11 | scgi_param REQUEST_SCHEME $scheme; 12 | scgi_param HTTPS $https if_not_empty; 13 | 14 | scgi_param REMOTE_ADDR $remote_addr; 15 | scgi_param REMOTE_PORT $remote_port; 16 | scgi_param SERVER_PORT $server_port; 17 | scgi_param SERVER_NAME $server_name; 18 | -------------------------------------------------------------------------------- /services/nginx/snippets/fastcgi-php.conf: -------------------------------------------------------------------------------- 1 | # regex to split $uri to $fastcgi_script_name and $fastcgi_path 2 | fastcgi_split_path_info ^(.+\.php)(/.+)$; 3 | 4 | # Check that the PHP script exists before passing it 5 | try_files $fastcgi_script_name =404; 6 | 7 | # Bypass the fact that try_files resets $fastcgi_path_info 8 | # see: http://trac.nginx.org/nginx/ticket/321 9 | set $path_info $fastcgi_path_info; 10 | fastcgi_param PATH_INFO $path_info; 11 | 12 | fastcgi_index index.php; 13 | include fastcgi.conf; 14 | -------------------------------------------------------------------------------- /services/nginx/snippets/snakeoil.conf: -------------------------------------------------------------------------------- 1 | # Self signed certificates generated by the ssl-cert package 2 | # Don't use them in a production server! 3 | 4 | ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem; 5 | ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; 6 | -------------------------------------------------------------------------------- /services/nginx/uwsgi_params: -------------------------------------------------------------------------------- 1 | 2 | uwsgi_param QUERY_STRING $query_string; 3 | uwsgi_param REQUEST_METHOD $request_method; 4 | uwsgi_param CONTENT_TYPE $content_type; 5 | uwsgi_param CONTENT_LENGTH $content_length; 6 | 7 | uwsgi_param REQUEST_URI $request_uri; 8 | uwsgi_param PATH_INFO $document_uri; 9 | uwsgi_param DOCUMENT_ROOT $document_root; 10 | uwsgi_param SERVER_PROTOCOL $server_protocol; 11 | uwsgi_param REQUEST_SCHEME $scheme; 12 | uwsgi_param HTTPS $https if_not_empty; 13 | 14 | uwsgi_param REMOTE_ADDR $remote_addr; 15 | uwsgi_param REMOTE_PORT $remote_port; 16 | uwsgi_param SERVER_PORT $server_port; 17 | uwsgi_param SERVER_NAME $server_name; 18 | -------------------------------------------------------------------------------- /services/pgadmin/passfile: -------------------------------------------------------------------------------- 1 | db:5432:qaboard:qaboard:password -------------------------------------------------------------------------------- /services/pgadmin/servers.json: -------------------------------------------------------------------------------- 1 | { 2 | "Servers": { 3 | "1": { 4 | "Name": "QA-Board", 5 | "Group": "QA-Board", 6 | "Comment": "Default QA-Board server", 7 | "Port": 5432, 8 | "Username": "qaboard", 9 | "Host": "db", 10 | "SSLMode": "prefer", 11 | "MaintenanceDB": "postgres", 12 | "PassFile": "/pgadmin4/pgpassfile" 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_conventions.py: -------------------------------------------------------------------------------- 1 | """ 2 | TODO: Write more tests. 3 | https://docs.python.org/3/library/unittest.html 4 | """ 5 | import unittest 6 | 7 | import os 8 | from pathlib import Path 9 | 10 | class TestConventions(unittest.TestCase): 11 | def test_location_spec(self): 12 | from qaboard.conventions import location_from_spec 13 | self.assertEqual(location_from_spec({"linux": "/mnt/qaboard"}), Path("/mnt/qaboard")) 14 | self.assertEqual(location_from_spec("/mnt/qaboard"), Path("/mnt/qaboard")) 15 | os.environ['XXXXX'] = "user" 16 | self.assertEqual(location_from_spec("/mnt/${XXXXX}"), Path("/mnt/user")) 17 | self.assertEqual(location_from_spec("/mnt/{subproject.parts[0]}", {"subproject": Path("x/y")}), Path("/mnt/x")) 18 | self.assertEqual(location_from_spec("/mnt/{subproject_parts[0]}", {"subproject_parts": ["x","y"]}), Path("/mnt/x")) 19 | self.assertEqual(location_from_spec("/mnt/{subproject.name}", {"subproject": Path("x/y")}), Path("/mnt/y")) 20 | # https://docs.python.org/3/library/string.html#formatspec 21 | # https://www.python.org/dev/peps/pep-3101 22 | # self.assertEqual(location_from_spec("/mnt/{subproject_parts[-1]}", {"subproject_parts": ["x","y"]}), Path("/mnt/y")) 23 | 24 | 25 | def test_serialize_config(self): 26 | from qaboard.conventions import serialize_config 27 | self.assertEqual(serialize_config(['a', 'b', 'c']), 'a:b:c') 28 | self.assertEqual(serialize_config(['a', {'b': 1}]), 'a:{"b": 1}') 29 | self.assertEqual(serialize_config(['a', '{"b":1']), 'a:{"b":1') 30 | 31 | def test_deserialize_config(self): 32 | from qaboard.conventions import deserialize_config 33 | self.assertEqual(deserialize_config('a:b:c'), ['a', 'b', 'c']) 34 | self.assertEqual(deserialize_config('a:{"b":1}'), ['a', {'b': 1}]) 35 | self.assertEqual(deserialize_config('a:{"b":1'), ['a', '{"b":1']) 36 | # we do those adjustments only on windows... 37 | # self.assertEqual(deserialize_config('a:C://path:b'), ['a', 'C://path', 'b']) 38 | # self.assertEqual(deserialize_config('C://path:a'), ['C://path', 'a']) 39 | 40 | 41 | if __name__ == '__main__': 42 | unittest.main() 43 | -------------------------------------------------------------------------------- /webapp/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | 4 | .dockerignore 5 | Dockerfile 6 | Dockerfile.prod 7 | -------------------------------------------------------------------------------- /webapp/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | .vscode/ 4 | 5 | # dependencies 6 | /node_modules 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 | -------------------------------------------------------------------------------- /webapp/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts 2 | WORKDIR /webapp 3 | 4 | RUN apt update -qq && apt-get install -y --no-install-recommends rsync 5 | 6 | ENV PATH /app/node_modules/.bin:$PATH 7 | 8 | COPY package.json package-lock.json ./ 9 | 10 | # In the past we had ulimit issues and "ulimit -n 2000 &&" 11 | RUN npm ci 12 | COPY . ./ 13 | ARG REACT_APP_QABOARD_DOCS_ROOT="https://samsung.github.io/qaboard/" 14 | ENV REACT_APP_QABOARD_DOCS_ROOT=$REACT_APP_QABOARD_DOCS_ROOT 15 | RUN npm run-script build 16 | 17 | # When upgrading, we want to enable clients to continue using a previous bundle 18 | # without the application crashing and asking for a refresh 19 | # The strategy is to server the application from a named volume at 20 | VOLUME /builds 21 | # When we start the app, we copy the bundle over there and ensure new clients 22 | # get the new version. 23 | CMD ["rsync", "-r", "build/", "/builds"] 24 | -------------------------------------------------------------------------------- /webapp/config-overrides.js: -------------------------------------------------------------------------------- 1 | /* config-overrides.js */ 2 | const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); 3 | 4 | module.exports = function override(config, env) { 5 | if (!config.plugins) { 6 | config.plugins = []; 7 | } 8 | 9 | config.plugins.push( 10 | new MonacoWebpackPlugin() 11 | ); 12 | 13 | // console.log(config.plugins) 14 | // the names of the plugins could change in the future.. 15 | // you debug the changes easily with console.log statements 16 | config.plugins.forEach(plugin => { 17 | if (plugin.constructor.name === "GenerateSW") { 18 | plugin.config.navigateFallbackBlacklist = [ 19 | /^\/s\/.*/, 20 | /^\/api\/.*/, 21 | /^\/admin\/.*/, 22 | /^\/docs\/.*/, 23 | /^\/blog\/.*/, 24 | /^\/piwik\.js/, 25 | /^\/piwik\.php/, 26 | ]; 27 | // console.log(plugin.config) 28 | } 29 | }); 30 | // console.log(config.plugins.filter(plugin => plugin.constructor.name === "GenerateSW")) 31 | return config; 32 | } 33 | -------------------------------------------------------------------------------- /webapp/notes.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | ``` 3 | # https://nodejs.org/en/download/ 4 | export PATH=/home/arthurf/source/node-v8.9.4-linux-x64/bin:$PATH 5 | export NPM_CONFIG_PREFIX=/opt/.npm-global 6 | export PATH=$NPM_CONFIG_PREFIX/bin:$PATH 7 | # you need more in .npmrc for SSL certificates 8 | # https://docs.npmjs.com/misc/config 9 | ``` 10 | 11 | 12 | ``` 13 | npm start 14 | ``` 15 | 16 | ## UI frameworks 17 | https://github.com/mui-org/material-ui 18 | -------------------------------------------------------------------------------- /webapp/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/favicon-16x16.png -------------------------------------------------------------------------------- /webapp/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/favicon-32x32.png -------------------------------------------------------------------------------- /webapp/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/favicon.ico -------------------------------------------------------------------------------- /webapp/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "QA-Board", 3 | "name": "QA-Board", 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 | -------------------------------------------------------------------------------- /webapp/public/openseadragon/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/Thumbs.db -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/Thumbs.db -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/button_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/button_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/button_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/button_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/button_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/button_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/button_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/button_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/flip_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/flip_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/flip_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/flip_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/flip_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/flip_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/flip_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/flip_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/fullpage_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/fullpage_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/fullpage_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/fullpage_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/fullpage_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/fullpage_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/fullpage_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/fullpage_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/home_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/home_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/home_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/home_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/home_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/home_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/home_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/home_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/next_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/next_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/next_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/next_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/next_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/next_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/next_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/next_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/previous_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/previous_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/previous_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/previous_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/previous_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/previous_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/previous_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/previous_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/rotateleft_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/rotateleft_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/rotateleft_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/rotateleft_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/rotateleft_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/rotateleft_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/rotateleft_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/rotateleft_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/rotateright_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/rotateright_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/rotateright_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/rotateright_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/rotateright_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/rotateright_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/rotateright_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/rotateright_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/zoomin_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/zoomin_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/zoomin_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/zoomin_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/zoomin_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/zoomin_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/zoomin_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/zoomin_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/zoomout_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/zoomout_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/zoomout_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/zoomout_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/zoomout_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/zoomout_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/_resized/zoomout_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/_resized/zoomout_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/button_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/button_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/button_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/button_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/button_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/button_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/button_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/button_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/flip_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/flip_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/flip_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/flip_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/flip_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/flip_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/flip_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/flip_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/fullpage_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/fullpage_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/fullpage_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/fullpage_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/fullpage_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/fullpage_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/fullpage_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/fullpage_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/home_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/home_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/home_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/home_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/home_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/home_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/home_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/home_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/imagetools_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/imagetools_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/imagetools_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/imagetools_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/imagetools_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/imagetools_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/imagetools_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/imagetools_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/next_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/next_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/next_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/next_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/next_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/next_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/next_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/next_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/previous_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/previous_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/previous_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/previous_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/previous_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/previous_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/previous_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/previous_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/rotateleft_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/rotateleft_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/rotateleft_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/rotateleft_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/rotateleft_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/rotateleft_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/rotateleft_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/rotateleft_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/rotateright_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/rotateright_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/rotateright_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/rotateright_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/rotateright_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/rotateright_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/rotateright_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/rotateright_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection/Thumbs.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection/Thumbs.db -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection/selection_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection/selection_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection/selection_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection/selection_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection/selection_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection/selection_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection/selection_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection/selection_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_cancel_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_cancel_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_cancel_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_cancel_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_cancel_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_cancel_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_cancel_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_cancel_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_confirm_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_confirm_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_confirm_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_confirm_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_confirm_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_confirm_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_confirm_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_confirm_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/selection_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/selection_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/zoomin_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/zoomin_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/zoomin_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/zoomin_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/zoomin_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/zoomin_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/zoomin_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/zoomin_rest.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/zoomout_grouphover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/zoomout_grouphover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/zoomout_hover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/zoomout_hover.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/zoomout_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/zoomout_pressed.png -------------------------------------------------------------------------------- /webapp/public/openseadragon/zoomout_rest.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/public/openseadragon/zoomout_rest.png -------------------------------------------------------------------------------- /webapp/src/App.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 3 | font-size: 14px; 4 | line-height: 1.42857143; 5 | color: rgba(0,0,0,0.85); 6 | 7 | margin: 0; 8 | padding: 0; 9 | /*font-family: sans-serif;*/ 10 | } 11 | 12 | span.bp3-popover-wrapper { 13 | vertical-align: baseline !important; 14 | } 15 | 16 | .limit-overflow .bp3-transition-container { 17 | max-height: 500px; 18 | overflow-y: scroll; 19 | } 20 | 21 | .fullscreen-enabled { 22 | background: white; 23 | } 24 | 25 | @media only screen and (max-width: 1100px) { 26 | .hide-small-screen { 27 | display: none; 28 | } 29 | } -------------------------------------------------------------------------------- /webapp/src/App.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /webapp/src/actions/commit.js: -------------------------------------------------------------------------------- 1 | import { get } from "axios"; 2 | 3 | import { 4 | UPDATE_COMMIT, 5 | FETCH_COMMIT, 6 | } from "./constants"; 7 | import { updateSelected } from './selected'; 8 | 9 | 10 | const refresh_interval = 15 * 1000 // seconds 11 | 12 | 13 | export const updateCommit = (project, commit, error) => ({ 14 | type: UPDATE_COMMIT, 15 | project, 16 | id: commit.id, 17 | data: commit, 18 | error, 19 | }) 20 | 21 | 22 | export const fetchCommit = ({project, id, branch, update_with_id, batch}) => { 23 | return dispatch => { 24 | dispatch({ 25 | type: FETCH_COMMIT, 26 | project, 27 | id, 28 | }) 29 | // the API defaults to the latest commit on the reference branch, it is useful 30 | get(`/api/v1/commit${!!!id ? "/" : `/${id}`}`, { params: { project, branch, batch } }) 31 | .then(response => { 32 | dispatch(updateCommit(project, response.data)) 33 | 34 | // when page load and look for, say, the latest commit on master, we don't know it's commit hash until we fetch it 35 | // hence we have to expose a way to updated the "selected" commit to the now-known commit 36 | let id_ = response.data.id 37 | if (update_with_id) 38 | dispatch(updateSelected(update_with_id.project, { [update_with_id.commit]: id_}) ) 39 | 40 | // we want to keep updated 41 | const batches = response.data.batches || {} 42 | if (Object.values(batches).some(b => b.pending_outputs > 0)) 43 | setTimeout(x => dispatch(fetchCommit({project, id: id_, branch, batch})), refresh_interval); 44 | }) 45 | .catch(error => { 46 | if (error.response) 47 | dispatch(updateCommit(project, {id}, error.response.data.error)) 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /webapp/src/actions/constants.js: -------------------------------------------------------------------------------- 1 | export const FETCH_PROJECT = 'FETCH_PROJECT' 2 | export const FETCH_PROJECTS = 'FETCH_PROJECTS' 3 | export const UPDATE_PROJECTS = 'UPDATE_PROJECTS' 4 | 5 | export const FETCH_BRANCHES = 'FETCH_BRANCHES' 6 | export const UPDATE_BRANCHES = 'UPDATE_BRANCHES' 7 | 8 | export const FETCH_COMMITS = 'FETCH_COMMITS' 9 | export const UPDATE_COMMITS = 'UPDATE_COMMITS' 10 | 11 | export const FETCH_COMMIT = 'FETCH_COMMIT' 12 | export const UPDATE_COMMIT = 'UPDATE_COMMIT' 13 | 14 | export const UPDATE_SELECTED = 'UPDATE_SELECTED' 15 | export const UPDATE_TUNING_FORM = 'UPDATE_TUNING_FORM' 16 | 17 | export const UPDATE_FAVORITE = 'UPDATE_FAVORITE' 18 | export const UPDATE_MILESTONES = 'UPDATE_MILESTONES' 19 | 20 | export const LOG_IN = 'LOG_IN' 21 | export const LOG_OUT = 'LOG_OUT' 22 | -------------------------------------------------------------------------------- /webapp/src/actions/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/src/actions/index.js -------------------------------------------------------------------------------- /webapp/src/actions/tuning.js: -------------------------------------------------------------------------------- 1 | import { 2 | UPDATE_TUNING_FORM, 3 | } from './constants' 4 | 5 | export const updateTuningForm = (project, tuning_form) => ({ 6 | type: UPDATE_TUNING_FORM, 7 | project, 8 | tuning_form, 9 | }) 10 | 11 | -------------------------------------------------------------------------------- /webapp/src/actions/users.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOG_IN, 3 | LOG_OUT, 4 | } from '../actions/constants' 5 | 6 | 7 | export const login = (user) => { 8 | return { 9 | type: LOG_IN, 10 | user, 11 | }; 12 | }; 13 | 14 | export const logout = () => { 15 | return {type: LOG_OUT}; 16 | }; -------------------------------------------------------------------------------- /webapp/src/components/DoneAtTag.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Link } from "react-router-dom"; 3 | import styled from "styled-components"; 4 | 5 | import { DateTime } from 'luxon'; 6 | import { Classes } from "@blueprintjs/core"; 7 | 8 | import { updateSelected } from "../actions/selected"; 9 | 10 | 11 | const defaults = { 12 | committer_name: 'Place Holder', 13 | date: '2018-08-08T06:00:00Z', 14 | } 15 | 16 | class DoneAtTagUnstyled extends React.Component { 17 | render() { 18 | const { project, commit, className, style, dispatch } = this.props; 19 | const has_data = !!commit?.authored_datetime 20 | const maybe_skeletton = has_data ? null : Classes.SKELETON; 21 | return ( 22 | 23 | 24 | {DateTime.fromISO(commit?.authored_datetime ?? defaults.date, { zone: 'utc' }).toRelative()} 25 | 26 | {" "} 27 | dispatch(updateSelected(project, {branch: null, committer: commit.committer_name}))} 31 | > 32 | by {commit?.committer_name ?? defaults.committer_name} 33 | 34 | 35 | ); 36 | } 37 | } 38 | 39 | const DoneAtTag = styled(DoneAtTagUnstyled)` 40 | color: rgba(0, 0, 0, 0.55); 41 | white-space: nowrap; 42 | box-sizing: border-box; 43 | margin-left: 5px; 44 | `; 45 | 46 | export { DoneAtTag }; 47 | -------------------------------------------------------------------------------- /webapp/src/components/EmptyLoading.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | const EmptyLoading = props => { 4 | if (props.error) { 5 | return
Error!
; 6 | } else { 7 | return
; 8 | } 9 | }; 10 | 11 | export default EmptyLoading; -------------------------------------------------------------------------------- /webapp/src/components/ErrorPage.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Callout, Intent, Classes, Button } from "@blueprintjs/core"; 3 | import { Container } from "./layout"; 4 | 5 | 6 | class ErrorPage extends React.Component { 7 | render() { 8 | let subject = encodeURIComponent("[qa] bug report"); 9 | let error = this.props.error.toString() 10 | 11 | let componentStack = (this.props.info || {}).componentStack 12 | let body = encodeURIComponent(`URL: ${document.URL}\nerror: ${error}\ncomponentStack: ${componentStack}`) 13 | return 14 | 15 |

Try refreshing the page. If you're lucky, try the staging version

16 |

Point of contact: Arthur Flam (+972-(0)58-706-2016) WhatsApp/Phone

17 |

18 |

19 |

20 |
21 |
22 | } 23 | } 24 | 25 | export default ErrorPage; -------------------------------------------------------------------------------- /webapp/src/components/IeDeprecationWarning.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import { 4 | Intent, 5 | Tag, 6 | Tooltip, 7 | } from "@blueprintjs/core"; 8 | 9 | const IeDeprecationWarning = () => { 10 | const ua = window.navigator.userAgent; 11 | const is_ie10_or_less = ua.indexOf('MSIE ') > 0; 12 | const is_ie11 = ua.indexOf('Trident/') > 0; 13 | const is_ie = is_ie10_or_less || is_ie11; 14 | if (!is_ie) return 15 | return
21 | 22 | 28 | This application does not fully support Internet Explorer. 29 | 30 | 31 | Please use a modern browser (eg Chrome/Firefox/Edge/Safari/...) 32 | 33 | 34 |
35 | } 36 | 37 | export default IeDeprecationWarning; -------------------------------------------------------------------------------- /webapp/src/components/authentication/RequireAuth.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { connect } from 'react-redux' 3 | import { 4 | Classes, 5 | Intent, 6 | Callout, 7 | } from "@blueprintjs/core"; 8 | import AuthButton from "./Auth" 9 | 10 | 11 | 12 | class PrivateContent extends React.Component { 13 | constructor(props) { 14 | super(props); 15 | } 16 | 17 | render() { 18 | const is_logged = this.props.user ? this.props.user.is_logged : undefined; 19 | return <> 20 | {is_logged ? 21 | this.props.children : 22 | 23 |

The content is available for logged-in users only.

24 | 25 |
26 | } 27 | 28 | } 29 | } 30 | 31 | 32 | // export const require_authentication_button = ( on_click_func, user_state ) => { 33 | // // const user_state = store ? store.getState().user : undefined 34 | // const is_auth = user_state ? user_state.is_logged : undefined 35 | // return is_auth ? on_click_func : undefined 36 | // } 37 | 38 | 39 | const mapStateToProps = state => { 40 | return { 41 | user: state.user || null, 42 | } 43 | } 44 | 45 | export default connect(mapStateToProps)(PrivateContent); -------------------------------------------------------------------------------- /webapp/src/components/layout.js: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | 4 | const Container = styled.div` 5 | padding-left: 0; 6 | list-style: none; 7 | margin-top: 20px; 8 | margin-bottom: 10px; 9 | box-sizing: border-box; 10 | 11 | padding-right: 15px; 12 | padding-left: 15px; 13 | margin-right: auto; 14 | margin-left: auto; 15 | @media (min-width: 768px) { 16 | width: 750px; 17 | } 18 | @media (min-width: 992px) { 19 | width: 970px; 20 | } 21 | @media (min-width: 1300px) { 22 | width: 1270px; 23 | } 24 | `; 25 | 26 | const Section = styled.div` 27 | margin-bottom: 40px; 28 | margin-top: 30px; 29 | width: fit-content; 30 | `; 31 | 32 | 33 | 34 | const Layout = styled.div` 35 | display: flex; 36 | flex: auto; 37 | flex-direction: row; 38 | box-sizing: border-box; 39 | // style 40 | background: #f0f2f5; 41 | min-height: 750px; 42 | ` 43 | 44 | export { Container, Section, Layout }; 45 | -------------------------------------------------------------------------------- /webapp/src/components/metricSelect.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { MenuItem } from "@blueprintjs/core"; 3 | 4 | const noMetrics = ; 5 | 6 | // const renderMetric = (metric, {handleClick, modifiers, query} ) => { 7 | // if (!modifiers.matchesPredicate) { 8 | // return null; 9 | // } 10 | // return ( 11 | // 20 | // ); 21 | // }; 22 | 23 | export { noMetrics }; 24 | -------------------------------------------------------------------------------- /webapp/src/components/tuning/SelectBatches.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | Colors, 4 | HTMLSelect 5 | } from "@blueprintjs/core"; 6 | 7 | import { pretty_label } from '../../utils' 8 | 9 | const SelectBatchesNav = ({ commit, onChange, batch, hide_counts }) => { 10 | if (!commit || !commit.batches) 11 | return 12 | 13 | const batches_to_options = batches => 14 | Object.entries(batches) 15 | .sort( ([label1, _1], [label2, _2]) => { 16 | if (label1 === 'default') 17 | return -1; 18 | if (label2 === 'default') 19 | return 1; 20 | return label1.localeCompare(label2); 21 | }) 22 | .map(([label, batch]) => { 23 | let outputs = Object.values(batch.outputs || {}) 24 | let iters = outputs.filter(o => o.output_type === "optim_iteration").length 25 | outputs = outputs.filter(o => o.output_type !== "optim_iteration") 26 | const title = pretty_label(batch) 27 | let nb_success = outputs.filter(o => !o.is_pending && !o.is_failed).length; 28 | let status = `${nb_success}/${outputs.length} ✅`; 29 | let nb_failed = outputs.filter(o => o.is_failed).length; 30 | let nb_running = outputs.filter(o => o.is_running).length; 31 | let failures = nb_failed > 0 ? `${nb_failed}❌` : ""; 32 | let running = nb_running > 0 ? `${nb_running}🏃` : ""; 33 | return 36 | }); 37 | 38 | let has_tuning_batches = Object.values(commit.batches).length >= 1; 39 | let selected_batch_missing = !Object.keys(commit.batches).includes(batch.label) 40 | let style = selected_batch_missing ? {color: Colors.RED2} : {} 41 | return ( 42 | 51 | {selected_batch_missing && } 52 | {batches_to_options(commit.batches)} 53 | 54 | ); 55 | }; 56 | 57 | export { SelectBatchesNav }; 58 | -------------------------------------------------------------------------------- /webapp/src/configureStore.js: -------------------------------------------------------------------------------- 1 | // https://redux.js.org/recipes/configuringyourstore 2 | import { createStore, applyMiddleware } from 'redux' 3 | import { compose } from 'redux' 4 | import thunkMiddleware from 'redux-thunk' 5 | 6 | // https://github.com/rt2zz/redux-persist 7 | import { persistStore, persistReducer } from 'redux-persist' 8 | import localForage from "localforage"; 9 | // import storage from 'redux-persist/lib/storage' // defaults to localStorage for web and AsyncStorage for react-native 10 | // import autoMergeLevel2 from 'redux-persist/lib/stateReconciler/autoMergeLevel2'; 11 | 12 | import { composeWithDevTools } from 'redux-devtools-extension' 13 | import loggerMiddleware from './middleware/logger' 14 | import monitorReducersEnhancer from './enhancers/monitorReducers' 15 | 16 | import { rootReducer } from './reducers' 17 | 18 | 19 | // https://github.com/rt2zz/redux-persist/blob/master/src/types.js#L13-L27 20 | const persistConfig = { 21 | key: 'root', 22 | transforms: [ 23 | ], 24 | storage: localForage, 25 | whitelist: [ 26 | 'projects', 27 | 'tuning', 28 | 'user', 29 | // we may not want to store any of the commit.$id.batches.outputs. 30 | // TODO: look into 31 | // https://github.com/rt2zz/redux-persist 32 | // https://github.com/edy/redux-persist-transform-filter 33 | // 'commits', 34 | ], 35 | blacklist: ['selected'], 36 | // stateReconciler: autoMergeLevel2, 37 | } 38 | 39 | 40 | export default function configureStore(preloadedState) { 41 | let is_production = process.env.NODE_ENV === 'production' 42 | // let is_production = false 43 | 44 | let middlewares = is_production ? [thunkMiddleware] : [loggerMiddleware, thunkMiddleware] 45 | let middlewareEnhancer = applyMiddleware(...middlewares) 46 | let enhancers = is_production ? [middlewareEnhancer] : [middlewareEnhancer, monitorReducersEnhancer] 47 | let composedEnhancers = is_production ? compose(...enhancers) : composeWithDevTools(...enhancers) 48 | 49 | const persistedReducer = persistReducer(persistConfig, rootReducer) 50 | const store = createStore(persistedReducer, preloadedState, composedEnhancers) 51 | 52 | if (process.env.NODE_ENV !== 'production' && module.hot) { 53 | module.hot.accept('./reducers', () => 54 | store.replaceReducer(persistedReducer) 55 | ) 56 | } 57 | 58 | let persistor = persistStore(store) 59 | 60 | return {store, persistor} 61 | } 62 | -------------------------------------------------------------------------------- /webapp/src/enhancers/monitorReducers.js: -------------------------------------------------------------------------------- 1 | const round = number => Math.round(number * 100) / 100 2 | 3 | const monitorReducerEnhancer = createStore => ( 4 | reducer, 5 | initialState, 6 | enhancer 7 | ) => { 8 | const monitoredReducer = (state, action) => { 9 | const start = performance.now() 10 | const newState = reducer(state, action) 11 | const end = performance.now() 12 | const diff = round(end - start) 13 | 14 | if (false) console.debug('reducer process time:', diff) 15 | 16 | return newState 17 | } 18 | 19 | return createStore(monitoredReducer, initialState, enhancer) 20 | } 21 | 22 | export default monitorReducerEnhancer 23 | -------------------------------------------------------------------------------- /webapp/src/history.js: -------------------------------------------------------------------------------- 1 | import { createBrowserHistory } from "history"; 2 | export default createBrowserHistory(); -------------------------------------------------------------------------------- /webapp/src/index.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/src/index.css -------------------------------------------------------------------------------- /webapp/src/index.js: -------------------------------------------------------------------------------- 1 | import 'react-app-polyfill/ie11'; 2 | import 'react-app-polyfill/stable'; 3 | import './polyfills'; // other polyfills 4 | 5 | import React from "react"; 6 | import { render } from "react-dom"; 7 | 8 | import App from "./App"; 9 | import * as serviceWorker from './serviceWorker'; 10 | 11 | import configureStore from './configureStore'; 12 | import { default_store } from './reducers'; 13 | 14 | 15 | const { store, persistor } = configureStore(default_store) 16 | 17 | const renderApp = () => render( 18 | , 19 | document.getElementById("root") 20 | ); 21 | 22 | 23 | if (process.env.NODE_ENV !== 'production' && module.hot) { 24 | // https://www.npmjs.com/package/why-did-you-update 25 | // const { whyDidYouUpdate } = require('why-did-you-update'); 26 | // whyDidYouUpdate(React); 27 | 28 | // https://redux.js.org/recipes/configuringyourstore 29 | module.hot.accept('./App', () => { 30 | renderApp() 31 | }) 32 | } else { 33 | serviceWorker.unregister(); 34 | } 35 | 36 | renderApp() 37 | -------------------------------------------------------------------------------- /webapp/src/middleware/logger.js: -------------------------------------------------------------------------------- 1 | const logger = store => next => action => { 2 | // console.group(action.type) 3 | // console.info('dispatching', action) 4 | let result = next(action) 5 | // console.log('next state', store.getState()) 6 | // console.groupEnd() 7 | return result 8 | } 9 | 10 | export default logger 11 | -------------------------------------------------------------------------------- /webapp/src/polyfills.js: -------------------------------------------------------------------------------- 1 | // At the moment, we support Internet Explorer 11+. It needs a little help. 2 | // When everybody moves to modern browers, remove all of this! 3 | // 4 | // Known dependencies that require polyfills 5 | // - https://reactjs.org/docs/javascript-environment-requirements.html 6 | // - https://blueprintjs.com/docs/#blueprint/getting-started.js-environment 7 | // 8 | // Possible polyfill providers 9 | // - https://www.npmjs.com/package/core-js 10 | // - es6-shim 11 | // - babel-polyfill 12 | // 13 | // Note: ie11 on win7 has fetch but not 308 redirects 14 | // it can cause issues if you call POST API endpoints without a trailing slash 15 | 16 | import "dom4"; 17 | import 'intersection-observer'; 18 | 19 | // We follow the recommendations from 20 | // https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove 21 | // Alternatively, if more DOM polyfills are to be added, consider 22 | // > npm install --save dom4 23 | // ~ 24 | // ~ require("dom4") 25 | // ~ 26 | // 27 | // from:https://github.com/jserz/js_piece/blob/master/DOM/ChildNode/remove()/remove().md 28 | // eslint-disable-next-line 29 | (function (arr) { 30 | arr.forEach(function (item) { 31 | if (item.hasOwnProperty('remove')) { 32 | return; 33 | } 34 | Object.defineProperty(item, 'remove', { 35 | configurable: true, 36 | enumerable: true, 37 | writable: true, 38 | value: function remove() { 39 | if (this.parentNode === null) { 40 | return; 41 | } 42 | this.parentNode.removeChild(this); 43 | } 44 | }); 45 | }); 46 | })([Element.prototype, CharacterData.prototype, DocumentType.prototype]); 47 | 48 | 49 | 50 | // https://developer.mozilla.org/en-US/docs/Web/API/Element/closest 51 | if (!Element.prototype.matches) 52 | Element.prototype.matches = Element.prototype.msMatchesSelector || 53 | Element.prototype.webkitMatchesSelector; 54 | if (!Element.prototype.closest) 55 | Element.prototype.closest = function(s) { 56 | var el = this; 57 | if (!document.documentElement.contains(el)) return null; 58 | do { 59 | if (el.matches(s)) return el; 60 | el = el.parentElement || el.parentNode; 61 | // eslint-disable-next-line 62 | } while (el !== null && el.nodeType == 1); 63 | return null; 64 | }; 65 | -------------------------------------------------------------------------------- /webapp/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /webapp/src/reducers/users.js: -------------------------------------------------------------------------------- 1 | import { 2 | LOG_IN, 3 | LOG_OUT, 4 | } from '../actions/constants' 5 | 6 | 7 | const user_logged_out_state = { 8 | is_logged: false, 9 | user_id: null, 10 | user_name: null, 11 | full_name: null, 12 | email: null, 13 | } 14 | 15 | export const loggedReducer = (state = {...user_logged_out_state}, action) => { 16 | switch (action.type) { 17 | case LOG_IN: 18 | return { 19 | ...state, 20 | is_logged: true, 21 | ...action.user, 22 | } 23 | case LOG_OUT: 24 | return {...user_logged_out_state} 25 | default: 26 | return state 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /webapp/src/routes.js: -------------------------------------------------------------------------------- 1 | import AppNavbar from "./AppNavbar"; 2 | import AppSider from "./AppSider"; 3 | import CiCommitList from "./CiCommitList"; 4 | import CiCommitResults from "./CiCommitResults"; 5 | import Dashboard from "./Dashboard"; 6 | 7 | 8 | export const routes = [ 9 | { 10 | path: "/:project_id+/committer/:committer+", 11 | main: CiCommitList, 12 | sider: AppSider, 13 | navbar: AppNavbar, 14 | }, 15 | { 16 | path: "/:project_id+/commits/:name+", 17 | main: CiCommitList, 18 | sider: AppSider, 19 | navbar: AppNavbar, 20 | }, 21 | { 22 | path: "/:project_id+/commits", 23 | main: CiCommitList, 24 | sider: AppSider, 25 | navbar: AppNavbar, 26 | }, 27 | { 28 | path: "/:project_id+/commit/:name+", 29 | main: CiCommitResults, 30 | sider: AppSider, 31 | navbar: AppNavbar, 32 | }, 33 | { 34 | path: "/:project_id+/commit", 35 | main: CiCommitResults, 36 | sider: AppSider, 37 | navbar: AppNavbar, 38 | }, 39 | { 40 | path: "/:project_id+/dashboard/:name+", 41 | main: Dashboard, 42 | sider: AppSider, 43 | navbar: AppNavbar, 44 | }, 45 | { 46 | path: "/:project_id+/dashboard", 47 | main: Dashboard, 48 | sider: AppSider, 49 | navbar: AppNavbar, 50 | }, 51 | { 52 | path: "/:project_id+/history/:name+", 53 | main: Dashboard, 54 | sider: AppSider, 55 | navbar: AppNavbar, 56 | }, 57 | { 58 | path: "/:project_id+/history", 59 | main: Dashboard, 60 | sider: AppSider, 61 | navbar: AppNavbar, 62 | }, 63 | { 64 | path: "/:project_id+", 65 | main: CiCommitList, 66 | sider: AppSider, 67 | navbar: AppNavbar, 68 | }, 69 | ]; 70 | -------------------------------------------------------------------------------- /webapp/src/selectors/batches.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/src/selectors/batches.js -------------------------------------------------------------------------------- /webapp/src/selectors/index.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/webapp/src/selectors/index.js -------------------------------------------------------------------------------- /webapp/src/setupProxy.js: -------------------------------------------------------------------------------- 1 | // https://create-react-app.dev/docs/proxying-api-requests-in-development/ 2 | // https://github.com/chimurai/http-proxy-middleware 3 | const { createProxyMiddleware } = require('http-proxy-middleware'); 4 | 5 | // by default we assume you run QA-Board on localhost 6 | let QABOARD_SERVER_URL= process.env.REACT_APP_QABOARD_HOST || "http://localhost:5151"; 7 | // the api server doesn't serve the static content 8 | let REACT_APP_QABOARD_API_HOST= process.env.REACT_APP_QABOARD_API_HOST || QABOARD_SERVER_URL; 9 | 10 | 11 | module.exports = function(app) { 12 | app.use( 13 | '/api', 14 | createProxyMiddleware({ 15 | target: REACT_APP_QABOARD_API_HOST, 16 | changeOrigin: true, 17 | }) 18 | ); 19 | app.use( 20 | '/s', 21 | createProxyMiddleware({ 22 | target: QABOARD_SERVER_URL, 23 | changeOrigin: true, 24 | }) 25 | ); 26 | app.use( 27 | '/iiif', 28 | createProxyMiddleware({ 29 | target: QABOARD_SERVER_URL, 30 | changeOrigin: true, 31 | }) 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /webapp/src/todo.md: -------------------------------------------------------------------------------- 1 | 2 | ## Tuning 3 | pareto efficient frontier + noise 4 | https://blog.sigopt.com/posts/balancing-multiple-metrics-with-uncertainty 5 | https://blog.sigopt.com/posts/bayesian-optimization-with-uncertainty 6 | 7 | initial guesses, clustering 8 | https://blog.sigopt.com/posts/tensorflow-convnets-on-a-budget-with-bayesian-optimization 9 | 10 | https://blog.sigopt.com/posts/tensorflow-convnets-on-a-budget-with-bayesian-optimization 11 | https://blog.sigopt.com/posts/clustering-applied-to-acquisition-functions// 12 | -------------------------------------------------------------------------------- /webapp/src/viewers/controls.js: -------------------------------------------------------------------------------- 1 | import qs from "qs"; 2 | 3 | 4 | 5 | const controls_defaults = qatools_config => { 6 | let state_controls = { 7 | show: {}, 8 | }; 9 | if (!!qatools_config && !!qatools_config.outputs) { 10 | let controls = qatools_config.outputs.controls || []; 11 | controls.forEach(control => { 12 | state_controls[control.name] = control.default; 13 | }) 14 | const outputs = qatools_config.outputs; 15 | let visualizations = outputs.visualizations || outputs.detailed_views || [] 16 | visualizations.forEach( (view, idx) => { 17 | if (view.default_hidden) 18 | state_controls.show[view.name] = false; 19 | }) 20 | } 21 | 22 | let query = qs.parse(window.location.search.substring(1)); 23 | if (!!query.controls) { 24 | try { 25 | var query_controls = JSON.parse(query.controls) 26 | } catch { 27 | query_controls = {} 28 | } 29 | Object.entries(query_controls).forEach( ([key, value]) => { 30 | state_controls[key] = value; 31 | }) 32 | } 33 | return state_controls; 34 | } 35 | 36 | 37 | 38 | const updateQueryUrl = (history, controls) => { 39 | if (history === undefined || controls === undefined) 40 | return 41 | let query = qs.parse(window.location.search.substring(1)); 42 | history.push({ 43 | pathname: window.location.pathname, 44 | search: qs.stringify({ 45 | ...query, 46 | controls: JSON.stringify(controls), 47 | }) 48 | }); 49 | } 50 | 51 | export { controls_defaults, updateQueryUrl } -------------------------------------------------------------------------------- /webapp/src/viewers/images/histogram.js: -------------------------------------------------------------------------------- 1 | var empty = Array(256); 2 | for (var i = 0; i < 256; i++) { 3 | empty[i] = 0; 4 | } 5 | 6 | const rgb = { 7 | 'new': [ 8 | {name: 'Red', color: 'red', 'legendgroup': 'Red'}, 9 | {name: 'Green', color: 'green', 'legendgroup': 'Green'}, 10 | {name: 'Blue', color: 'blue', 'legendgroup': 'Blue'}, 11 | {name: 'Y', color: 'black', 'legendgroup': 'Y'}, 12 | ], 13 | 'ref': [ 14 | {name: 'Red', color: 'pink', 'legendgroup': 'Red'}, 15 | {name: 'Green', color: 'yellowgreen', 'legendgroup': 'Green'}, 16 | {name: 'Blue', color: 'aqua', 'legendgroup': 'Blue'}, 17 | {name: 'Y', color: 'darkgrey', 'legendgroup': 'Y'}, 18 | ] 19 | } 20 | 21 | 22 | const histogram_traces = (viewer, rect, label) => { 23 | if (rect !== null && rect !== undefined) { 24 | var { x, y, width, height } = rect; 25 | } else { 26 | x = 0 27 | y = 0 28 | height = viewer.drawer.canvas.height; 29 | width = viewer.drawer.canvas.width; 30 | } 31 | var context = viewer.drawer.context 32 | var data = [0, 1, 2, 3].map(() => empty.slice()); 33 | let total_pixels = 0 34 | try { 35 | var p = context.getImageData(x, y, width, height).data 36 | for(let i=0; i < p.length; i++ /*alpha*/) { 37 | // https://en.wikipedia.org/wiki/YUV#Converting_between_Y%E2%80%B2UV_and_RGB 38 | let Y = 0.299 * p[i] + 0.587 * p[i+1] + 0.114 * p[i+2]; 39 | data[3][Math.round(Y)]++ 40 | 41 | data[0][p[i++]]++; // r 42 | data[1][p[i++]]++; // g 43 | data[2][p[i++]]++; // b 44 | total_pixels += 1 45 | } 46 | } catch(e) { 47 | console.log(e) 48 | } 49 | 50 | return data.map( (y, idx) => ({ 51 | type: 'scatter', 52 | fill: 'tozeroy', 53 | x: Object.keys(y), 54 | y: y.map(e => e / total_pixels), 55 | name: label, 56 | legendgroup: rgb[label][idx].legendgroup, 57 | // fillcolor: '#ab63fa', 58 | // line: { 59 | // color: '#ab63fa' 60 | // } 61 | marker: { 62 | ...rgb[label][idx], 63 | opacity: 0.5, 64 | }, 65 | })) 66 | } 67 | 68 | export { histogram_traces }; 69 | -------------------------------------------------------------------------------- /webapp/src/viewers/images/image-canvas.css: -------------------------------------------------------------------------------- 1 | canvas { 2 | image-rendering: optimizeSpeed; /* Older versions of FF */ 3 | image-rendering: -moz-crisp-edges; /* FF 6.0+ */ 4 | image-rendering: -webkit-optimize-contrast; /* Safari */ 5 | image-rendering: -o-crisp-edges; /* OS X & Windows Opera (12.02+) */ 6 | image-rendering: pixelated; /* Awesome future-browsers */ 7 | -ms-interpolation-mode: nearest-neighbor; /* IE */ 8 | } 9 | -------------------------------------------------------------------------------- /webapp/src/viewers/images/utils.js: -------------------------------------------------------------------------------- 1 | export const iiif_url = (output_dir_url, path) => { 2 | // remove the URL's leading "/s" 3 | let identifier = output_dir_url.replace(/\/*?s\//, "") 4 | // IIIF specs require encoding the slashes inside the identifier 5 | identifier = `${identifier}/${encodeURI(decodeURIComponent(path))}`.replace(/\//g, '%2F'); 6 | let url = `/iiif/2/${identifier}` 7 | return url 8 | } 9 | 10 | 11 | 12 | export const is_image = visualization => { 13 | const { type='', path } = visualization; 14 | if (type.startsWith('image')) 15 | return true; 16 | if (path === undefined) 17 | return false; 18 | return path.endsWith('png') || 19 | path.endsWith('jpg') || 20 | path.endsWith('jpeg')|| 21 | path.endsWith('bmp') || 22 | path.endsWith('pdf') || 23 | path.endsWith('tif') || 24 | path.endsWith('tiff'); 25 | } -------------------------------------------------------------------------------- /webapp/src/viewers/textViewer.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import GenericTextViewer from "./text"; 3 | 4 | 5 | class TextViewer extends React.PureComponent { 6 | render() { 7 | const { output_new, output_ref, path, ...props } = this.props; 8 | // const { style } = this.props; 9 | // const width_px = (!!style && style.width) || '400px'; 10 | // const width = parseFloat(width_px.substring(0, width_px.length-2)) - 10; 11 | 12 | return 18 | // width={width} 19 | } 20 | } 21 | 22 | 23 | export default TextViewer; 24 | -------------------------------------------------------------------------------- /webapp/src/viewers/todo-image-viewers.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Features 4 | - ROI 5 | 6 | ## Architecture 7 | - IIIF client/server 8 | - run as a docker container on planet31 9 | - available as qa:7000 (until qa-iif:443) 10 | - viewers will translate URLS from /s/ into the host. 11 | 12 | 13 | ## Setup 14 | https://github.com/c7a/cihm-cantaloupe 15 | https://medusa-project.github.io/cantaloupe/manual/4.0/getting-started.html 16 | https://github.com/c7a/cihm-public-cos/blob/master/docker-compose.yml 17 | 18 | ## Configuration 19 | https://medusa-project.github.io/cantaloupe/manual/4.0/caching.html 20 | https://medusa-project.github.io/cantaloupe/manual/4.0/deployment.html 21 | 22 | ## Viewer 23 | http://openseadragon.github.io/ 24 | https://github.com/mejackreed/Leaflet-IIIF 25 | side-by-side 26 | https://bl.ocks.org/mejackreed/80c4248278517475a30190b427cb5c9c 27 | 28 | 29 | https://www.npmjs.com/package/@medmain/react-openseadragon 30 | https://github.com/UMNLibraries/react-openseadragon 31 | https://github.com/PaulLeCam/react-leaflet 32 | https://github.com/openseadragon/openseadragon/issues/942 33 | 34 | ## Reference 35 | https://github.com/medusa-project/cantaloupe/ 36 | https://iiif.io/apps-demos/#image-viewing-clients 37 | -------------------------------------------------------------------------------- /webapp/src/viewers/tof/Sys_Tools.js: -------------------------------------------------------------------------------- 1 | const parse_hex = (text, convert_nan ) => { 2 | if (!!!text) return {z: null} 3 | // console.log(text) 4 | let width = parseFloat(text.match(/width=(\d+)/)[1]) 5 | let height = parseFloat(text.match(/height=(\d+)/)[1]) 6 | let data_array = text 7 | .split('\n') 8 | .slice(0, width * height) 9 | .map(parseFloat) 10 | if (convert_nan){ 11 | data_array = data_array.map(x => x <= 0 ? NaN : x) 12 | } 13 | let newArr = []; 14 | while(data_array.length) 15 | newArr.push(data_array.splice(0, width)); 16 | // var hoover_text = newArr.map((row, i) => row.map((item, j) => { return `i: ${i}
j: ${j}
${label}: ${item.toFixed(3)}`})) 17 | 18 | return { 19 | z: newArr, 20 | // text: hover_text, 21 | // hoverinfo: 'text', 22 | }; 23 | }; 24 | export { parse_hex }; 25 | 26 | -------------------------------------------------------------------------------- /webapp/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "noEmit": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /website/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | build/ 3 | 4 | .dockerignore 5 | Dockerfile 6 | Dockerfile.prod 7 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /website/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine 2 | WORKDIR /website 3 | 4 | RUN apk update && apk add --no-cache \ 5 | # needed for node-gyp.. some do it with npm install in one layer 6 | # https://github.com/mhart/alpine-node/issues/27 7 | # https://github.com/nodejs/docker-node/issues/384 8 | # https://github.com/nodejs/docker-node/issues/282 9 | g++ make python3 \ 10 | rsync \ 11 | && rm -rf /var/cache/apk/* 12 | 13 | ENV PATH /app/node_modules/.bin:$PATH 14 | # RUN yarn config set strict-ssl false 15 | 16 | # RUN npm install -g yarn 17 | 18 | # In the past we had ulimit issues and "ulimit -n 2000 &&" 19 | COPY package.json yarn.lock ./ 20 | # we try multiple times to work around network issues... 21 | RUN yarn install --network-timeout 100000 || yarn install --network-timeout 100000 || yarn install --network-timeout 100000 || yarn install --network-timeout 100000 22 | COPY . ./ 23 | 24 | # On the website we have algolia for the search, but the baseURL (/qaboard) is different than 25 | # when running from the application (/docs). So we don't use algolia for the app... 26 | # RUN yarn run swizzle docusaurus-lunr-search SearchBar 27 | ENV QABOARD_DOCS_FOR_WEBAPP true 28 | ARG QABOARD_URL 29 | ENV QABOARD_URL=$QABOARD_URL 30 | RUN yarn build 31 | 32 | # When upgrading, we want to enable clients to continue using a previous bundle 33 | # without the application crashing and asking for a refresh 34 | # The strategy is to server the application from a named volume at 35 | VOLUME /builds 36 | # When we start the app, we copy the bundle over there and ensure new clients 37 | # get the new version. 38 | CMD ["rsync", "-r", "build/", "/builds"] 39 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /website/blog/authors.yml: -------------------------------------------------------------------------------- 1 | arthurf: 2 | name: Arthur Flam 3 | title: Algo engineering at Samsung 4 | url: https://shapescience.xyz/ 5 | image_url: https://avatars.githubusercontent.com/u/2649055 -------------------------------------------------------------------------------- /website/docs/alternatives-and-missing-features.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: alternatives 3 | sidebar_label: Alternatives 4 | title: Alternatives 5 | --- 6 | 7 | 8 | ## Non-features 9 | QA-Board works with other tools. It won't replace or provide: 10 | - **Automation:** You should call QA-Board's client, `qa`, in your CI. If you're looking for a CI plaform, consider [GitlabCI](https://docs.gitlab.com/ee/ci/), [Github Actions](https://github.com/features/actions), [DroneCI](https://github.com/drone/drone), etc. 11 | - **Monitoring, Deployement & Ops**: in this space solutions tend to be custom, industry specific, and have a *short* life. Get in touch if you see low-hanging fruits! 12 | - **Data Versionning** 13 | 14 | ## Why not use X instead? 15 | - Most comparable tools focus on **training for machine learning** (`sacred`, `nni`, `mlflow`, `tensorboard`, `polyaxon`...). Our use cases revolve around qualitative outputs for a wide range of algorithms. It means we *need* powerful visualizations. This said, those tools are great too! Many of the commercial solutions (`cometML`, `convrg.io`, `netpune.ai`...) can provide a lot of value too depending on your use-case and the size/maturity of your organization. 16 | - **Notebooks** are amazing for experimentation and r&d reporting, but are not easy to compare. 17 | - [`tensorboard`](https://www.tensorflow.org/tensorboard) has a lot of qualities, but it doesn't scale to many experiments, doesn't know about `git`, and is not persistent. We may integrate an "Open in Tensorboard" button, ask about it and stay tuned. As for `visdom`, it's great for experimenting, less to store historical information. 18 | -------------------------------------------------------------------------------- /website/docs/auto-optimization.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: auto-optimization 3 | sidebar_label: Auto-Opt 4 | title: Auto-Optimization 5 | --- 6 | 7 | 8 | ### `qa optimize` 9 | > **EXPERIMENTAL**: This feature is experimental and the API is subject to change at any time. 10 | > 11 | > Specifically, we may change the solver backend to `nevergrad`, offer multiple choices, etc. 12 | 13 | You need the `scikit-opt` package, which you can install with 14 | ```bash 15 | pip install qaboard[opt] 16 | ``` 17 | 18 | ```bash 19 | qa optimize --help 20 | ``` 21 | 22 | > **WIP**: We're working on section about "auto-optimization". *Stay tuned!* 23 | 24 | -------------------------------------------------------------------------------- /website/docs/backend-admin/host-upgrades.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: host-upgrades 3 | sidebar_label: Host Upgrades 4 | title: Upgrading the QA-Board host 5 | --- 6 | 7 | First connect to the QA-Board host: 8 | ```bash 9 | ssh qa 10 | ``` 11 | 12 | ## Make sure backups are enabled 13 | In *production.yml* you should uncomment the `cron-backup-db` service to enable daily backups, and replace `/WHERE/TO/SAVE/BACKUPS` with a (backup'ed!)location on the host. 14 | 15 | ## Check the latest daily backup 16 | ```bash 17 | # check the latest backup worked 18 | ls -lht /WHERE/BACKUPS/ARE/SAVED/ | head 19 | # copy the latest somewhere to make sure nothing can go wrong 20 | cp /WHERE/BACKUPS/ARE/SAVED/latest.dump . 21 | 22 | ``` 23 | 24 | ## Stop the server and create a backup 25 | ```bash 26 | # disconnect clients to avoid anyone writing 27 | docker-compose -f docker-compose.yml -f production.yml stop 28 | # we need the database to create a backup 29 | docker-compose -f docker-compose.yml -f production.yml up -d db 30 | 31 | # manually start a backup 32 | docker-compose -f docker-compose.yml -f production.yml run cron-backup-db /etc/periodic/daily/backup before-upgrade.dump 33 | ``` 34 | 35 | ## Maintenance Period 36 | > CPU/Memory/Storage upgrade... 37 | 38 | ## Restart 39 | After the boot, make sure everything is up: 40 | ```bash 41 | docker-compose -f docker-compose.yml -f production.yml up -d 42 | ``` 43 | 44 | **Checks**: 45 | - [ ] you can access the web application 46 | - [ ] the logs are all right 47 | - [ ] the logs are all right 48 | 49 | 50 | ## Restoring from a backup 51 | In case of issues, recover from a backup: 52 | 53 | ```bash 54 | # disconnect clients 55 | docker-compose -f docker-compose.yml -f production.yml stop 56 | # we need the database to restore a backup 57 | docker-compose -f docker-compose.yml -f production.yml up -d db 58 | 59 | # now restore 60 | docker-compose -f docker-compose.yml -f production.yml exec db /opt/restore /backups/before-upgrade.dump 61 | 62 | # and restart 63 | docker-compose -f docker-compose.yml -f production.yml up -d 64 | ``` -------------------------------------------------------------------------------- /website/docs/backend-admin/troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: troubleshooting 3 | sidebar_label: Troubleshooting 4 | title: Troubleshooting common issues 5 | --- 6 | Assuming you work on the QA-Board server: 7 | ```bash 8 | ssh qaboard-server 9 | ``` 10 | 11 | ## Talking to the different services 12 | You can interact with the individual services with e.g. 13 | 14 | ```bash 15 | # read logs from a specific service 16 | docker-compose logs -f backend 17 | 18 | # you can get a shell on the various services: 19 | docker-compose exec backend bash 20 | docker-compose run proxy /bin/ash 21 | # or with the docker-compose conventions, if the service is up: 22 | docker exec -it qaboard_proxy_1 bash 23 | ``` 24 | 25 | > Refer to the examples in *[docker-compose.yml](docker-compose.yml)* or to the `docker-compose` docs. 26 | 27 | ## Questions to ask if things don't work 28 | - Is the container even running ? Is it restarting all the time? 29 | ```bash 30 | docker ps 31 | ``` 32 | - Is the disk full? 33 | ```bash 34 | df -h 35 | ``` 36 | 37 | ## How to restart the docker containers 38 | Symptom: 39 | - Cannot load the web application 40 | - 500 errors 41 | - Often necessary if the disk got full.. 42 | 43 | ```bash 44 | docker-compose -f docker-compose.yml -f production.yml restart 45 | # if you make changes to the docker-compose files... 46 | docker-compose -f docker-compose.yml -f production.yml up -d 47 | ``` 48 | 49 | ### How to start from scratch the docker container 50 | ```bash 51 | docker-compose -f docker-compose.yml -f production.yml down 52 | docker-compose -f docker-compose.yml -f production.yml up -d 53 | ``` 54 | 55 | ### Quick wins when the disk is full 56 | Symptom: 57 | - 500 errors 58 | - database unreachable in the logs 59 | - `no space left on device` in the logs 60 | 61 | Remove the IIIF image cache: 62 | ```bash 63 | # stop 64 | docker-compose -f docker-compose.yml -f production.yml down cantaloupe 65 | # remove with the volumes 66 | docker-compose -f docker-compose.yml -f production.yml rm -v cantaloupe 67 | docker-compose -f docker-compose.yml -f production.yml up -d cantaloupe 68 | ``` 69 | 70 | Remove unused docker images 71 | ```bash 72 | docker image prune # -a 73 | ``` 74 | 75 | ### Re-build and start the docker container 76 | ```bash 77 | docker-compose -f docker-compose.yml -f production.yml up -d --build 78 | # you can rebuild a subset of the services: backend, frontend... 79 | ``` 80 | -------------------------------------------------------------------------------- /website/docs/creating-and-viewing-outputs-files.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: creating-and-viewing-outputs-files 3 | sidebar_label: Outputs 4 | title: Creating and viewing outputs files 5 | --- 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | 8 | 1. Write anything in `context.output_dir`: text, images, logs, pointclouds... 9 | 2. View in the web interface a list of all those files in the "Output Files" tab! 10 | 3. Click on a file to open it: 11 | 12 | https://qa/tof/swip_tof/commit/42778afb1fea31e19c00291a2a52bf490e3acc2c?
13 | reference=a451dda9cfdd586702ead95f436e41c5b074ebfa&selected_views=bit_accuracy 14 | 15 | QA-Board will try to guess the right file viewer depending on the extension. Many are available, read the [Read the visualizations guide](visualizations) to learn more. 16 | 17 | > **"Visualizations"** can help you declare pre-sets of relevant files. [Read the docs](visualizations) to learn more! 18 | 19 | ## Accessing output files 20 | All the outputs are saved as files. To get them out and QA-Board provides multiple ways to get them out. 21 | 22 | 1. **Next to each output**, there is always a button to copy-to-clipboard the path to the files it created. 23 | 24 | Export batch outputs 25 | 26 | Output directory from Windows 27 | 28 | 2. **From the Navigation bar**, you can copy-to-clipboard the windows-ish path where each commit saves its results: 29 | Export batch outputs 30 | 31 | > If you want to reproduce output files, the logs always show you the exact CLI commands that were used, so most of the time reproducing results is only a `git checkout` away. -------------------------------------------------------------------------------- /website/docs/debugging-runs-with-an-IDE.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: debugging-runs-with-an-IDE 3 | title: Debugging QA-Board' runs in an IDE 4 | sidebar_label: Debugging with IDEs 5 | --- 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | 8 | ## Debugging with PyCharm 9 | Edit your "debug configurations" like this: 10 | 11 | - **Module name:** `qaboard` *(make sure you select "module" not "script" in the dropdown menu).* 12 | - **Parameters:** CLI parameters for `qa`: **`run -i images/A.jpg`**. 13 | - **Working directory:** Check it’s defined as the directory with *qaboard.yaml*. If this directory happens to have a subfolder named "qaboard", use it. 14 | 15 | pyCharm setup 16 | 17 | > In some cases you'll also need to define as environment variables `LC_ALL=en_US.utf8 LANG=en_US.utf8` 18 | 19 | ## Debugging with VSCode 20 | To configure debugging, the editor opens a file called *launch.json*. You want to add configurations that look like those: 21 | 22 | ```json 23 | { 24 | "name": "qaboard", 25 | "type": "python", 26 | "request": "launch", 27 | "module": "qaboard", 28 | "args": [ 29 | "--", // needed... 30 | "--help", 31 | ] 32 | }, 33 | ``` 34 | 35 | ```json 36 | { 37 | "--", 38 | "--database", 39 | ".", 40 | "run", 41 | "--input", 42 | "tv/tv_GW1_9296x256_REMOSAIC_V1_FULL_X_HP_PDA1", 43 | } 44 | ``` 45 | 46 | Here is a more in-depth review of your options at https://code.visualstudio.com/docs/python/debugging 47 | -------------------------------------------------------------------------------- /website/docs/drafts-contributing.txt: -------------------------------------------------------------------------------- 1 | 2 | ## Creating custom visualizations 3 | > You'll have to write some `javascript` that downloads results and displays them. It's not that hard 👍👽 4 | 5 | - **[Arthur Flam](mailto:arthur.flam@samsung.com) can advise you along the way**. 6 | - You can setup a nice interactive dev environment in 15 minutes and start coding / adapting existing visualization: 7 | 8 | ```bash 9 | # download and install nodejs 10 | # https://nodejs.org/en/download/ 11 | npm install 12 | npm start 13 | #=> dev server listening on http://localhost:3000 14 | ``` 15 | 16 | - We use the simple [`reactjs`](https://reactjs.org) framework. 17 | - Existing visualizations are varied so you never start from a blank page 18 | * existing viewers are [implemented here](http://gitlab-srv/dvs/slamvizapp/tree/master/slamvizapp-webapp/src/viewers) 19 | * the mapping from visualization-type <-> viewer are [defined here](http://gitlab-srv/dvs/slamvizapp/tree/master/slamvizapp-webapp/src/viewers/OutputCard.js) 20 | -------------------------------------------------------------------------------- /website/docs/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: faq 3 | sidebar_label: FAQ 4 | title: Frequently Asked Questions 5 | --- 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | 8 | ## There is a bug I'd like you to know about 9 | Open an issue [here](https://github.com/Samsung/qaboard/issues), or even mail [Arthur Flam](mailto:arthur.flam@samsung.com). 10 | 11 | ## What is QA-Board written with? 12 | - **CLI tool** (wraps your code): `python` 13 | - **Frontend:** views with `reactjs`, state with `reduxjs`, design with `blueprintjs`, images with `openseadragon`, plots with `plotly`/`threejs`... 14 | - **Backend**: `postgreSQL` (to store metadata) accessed via `flask` 15 | 16 | 17 | ## Does QA-Board work with `python2.7`? 18 | Well enough! Just call `python2 your_code.py` as any other executable. 19 | 20 | ## Where are results saved? 21 | - **Local runs** are saved under the *output/* directory in the project. 22 | - **During CI runs**, results are saved under the `storage` defined in [*qaboard.yaml*](https://github.com/Samsung/qaboard/blob/master/qaboard/sample_project/qaboard.yaml#L119). To be honest, the exact naming conventions is complicated... **Export the data using the UI's export utilities, or ask QA-Board' simple API.** 23 | 24 | ## Can I export the data or use a third-party viewer? 25 | **Yes!** All the outputs are saved as files, and QA-Board provides multiple ways to get them out. 26 | 27 | :::caution 28 | At the moment nothing prevents your from modifying/destroying files created from the CI. 29 | ::: 30 | 31 | 1. **In the "Visualization" tab, an export utility** lets you copy-to-clipboard a path with filtered/nicely-renamed results/files: 32 | Export batch outputs 33 | 34 | 2. **Next to each output**, there is always a button to copy-to-clipboard the path to the files it created. 35 | 36 | Export batch outputs 37 | 38 | 3. **From the Navigation bar**, you can copy-to-clipboard the windows-ish path where each commit saves its results: 39 | Export batch outputs 40 | 41 | 4. You can also **programmatically access QA-Board's data** by [querying its API](api). 42 | -------------------------------------------------------------------------------- /website/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: installation 3 | title: Installing QA-Board's client 4 | sidebar_label: Client Installation 5 | --- 6 | 7 | You need to install QA-Board's CLI client: `qa`. It wraps and runs your code. 8 | 9 | ```bash 10 | pip install --upgrade qaboard 11 | ``` 12 | 13 | To make sure the installation was successful, try printing a list of `qa`'s CLI commands: 14 | 15 | ```bash 16 | qa --help 17 | 18 | # If you get errors about not using a utf8 locale, you can likely: 19 | # export LC_ALL=C.utf8 LANG=C.utf8 20 | ``` 21 | 22 | ## Connecting to a custom QA-Board server 23 | By default `qa` tries to use a QA-Board server running locally (it assumes you used the default config). 24 | 25 | If you connect to a remote QA-Board server, you'll need to set those environment variables: 26 | 27 | ```bash 28 | export QABOARD_HOST=my-server:5151 29 | export QABOARD_PROTOCOL=http 30 | ``` 31 | -------------------------------------------------------------------------------- /website/docs/jenkins-integration.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: jenkins-integration 3 | title: Using jenkins as a task runner 4 | sidebar_label: Jenkins Integration 5 | --- 6 | 7 | [Jenkins](https://www.jenkins.io/) can be used a distributed task queue. It's not pretty, but it can work... 8 | 9 | ## Creating a Build Job that `qa` will use 10 | 1. QA-Board needs to be [setup](deploy) with the ENV variable `JENKINS_AUTH`, in order to communicate with jenkins. 11 | 2. Create a Build Job configured with: 12 | * Enable "Trigger builds remotely", note the token 13 | * Parametrized: **task** should be a String parameter that gets a command to run 14 | * Build > "Execute Windows Batch command" 15 | 16 | ``` 17 | # if you need to make sure shared drive are available... 18 | net use \\netapp\algo_data 19 | net use \\netapp\raid 20 | net use \\mars\stage\jenkins_ws 21 | net use \\mars\stage\algo_jenkins_ws 22 | net use \\mars\raid 23 | 24 | @echo "%task%" 25 | "%task%"" 26 | ``` 27 | 28 | 29 | 3. _qaboard.yaml_ needs to be configured with something like: 30 | ```yaml 31 | runners: 32 | jenkins: 33 | build_url: http://jenmaster1:8080/job/ALGO/job/WindowsExecutor 34 | token: "1234" 35 | ``` 36 | 37 | ## How to use this runner? 38 | Multiple options: 39 | - On the CLI: `qa batch --runner=windows` 40 | - In our YAML files defining batches: 41 | ```yaml 42 | my-batch: 43 | runner: windows 44 | inputs: 45 | - my/images/ 46 | configurations: [base, delta] 47 | ``` 48 | -------------------------------------------------------------------------------- /website/docs/local-multiprocessing.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: local-multiprocessing 3 | title: Local Multiprocessing 4 | sidebar_label: Local Multiprocessing 5 | --- 6 | 7 | By default, if you don't configure an async task queue, QA-Board will use [`joblib`](https://joblib.readthedocs.io/en/latest/) and **local** multiprocessing to run your batches. 8 | 9 | ## Running batches locally 10 | If you configured an another runner, you can always: 11 | 12 | ```bash 13 | qa batch --runner=local my-batch 14 | ``` 15 | 16 | ## Configuration 17 | There is only 1 option: `concurrency`. You have multiple options to set it up: 18 | 19 | - Per batch: 20 | 21 | ```yaml title="qa/batches.yaml" 22 | my-batch: 23 | inputs: 24 | - image.jpg 25 | local: 26 | concurrency: -1 # 1 concurrent run per CPUs, default 27 | ``` 28 | 29 | - Globally: 30 | 31 | ```yaml title="qaboard.yaml" 32 | runners: 33 | default: local # default 34 | local: 35 | concurrency: 1 # serially 36 | ``` 37 | 38 | - On the CLI: 39 | 40 | ``` 41 | qa batch --local-concurrency 1 42 | ``` 43 | 44 | - via the `QA_BATCH_CONCURRENCY` environment variable 45 | 46 | :::tip 47 | Read [joblib's docs](https://joblib.readthedocs.io/en/latest/generated/joblib.Parallel.html) 48 | ::: 49 | -------------------------------------------------------------------------------- /website/docs/monorepo-support.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: monorepo-support 3 | sidebar_label: Monorepos 4 | title: Monorepo and subproject support 5 | --- 6 | Each *qaboard.yaml* is a project 7 | 8 | ## Subprojects 9 | Paths in *qaboard.yaml* are always relative to the repository root. 10 | `project.avatar_url` and `project.description` 11 | from qaboard import ci_repo_root.... 12 | ## Inheritance 13 | - inherit 14 | - `super` 15 | -------------------------------------------------------------------------------- /website/docs/project-init.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: project-init 3 | sidebar_label: Project Setup 4 | title: Adding QA-Board to your project 5 | --- 6 | 7 | Go at the root of your project's git repository and run: 8 | 9 | ```bash 10 | qa init 11 | #=> 🎉🎉🎉 12 | ``` 13 | 14 | Along with previously existing files and directories, your root directory will now contain a structure similar to: 15 | 16 | ``` 17 | root-git-repository 18 | ├── qaboard.yaml # configuration 19 | └── qa 20 | ├── main.py # sample entrypoint that runs your code 21 | ├── batches.yaml # examples of how to run multiple tests 22 | └── metrics.yaml # examples of how to define KPIs 23 | ``` 24 | 25 | 26 | ## *(Optional)* Gitlab Integration 27 | If you integrate with Gitlab, you'll be able to: 28 | - Update GitlabCI with your runs' statuses 29 | - Get direct links to your code 30 | - See users' avatars 31 | - Delete old results 32 | - Access commits by their tag or branch names 33 | - Wait for CI pipelines to end when checking if results changed vs the last version 34 | 35 | :::note 36 | In the past Gitlab was required. We'll work on enabling those features for e.g. Github. 37 | ::: 38 | 39 | ### How-to 40 | 1. Make sure you [started QA-Board with](/docs/deploy) a `GITLAB_HOST` and a `GITLAB_ACCESS_TOKEN` with `read_repository` scope. 41 | 2. Be one of the project's Maintainers. 42 | 3. Go to http://gitlab-srv/$YOUR_GROUP/PROJECT/settings/integrations. 43 | 4. Add an integration with: 44 | * __URL:__ `http://localhost:5151/webhook/gitlab`. Replace localhost with your hostname if you setup any DNS/SSL.. 45 | * __Secret token:__ *(leave the field empty)* 46 | 47 | > To test everything went well, Gitlab lets you "Test" your new hook. You should get a blue happy `200 OK` message 🔵🎉. 48 | -------------------------------------------------------------------------------- /website/docs/references-and-milestones.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: references-and-milestones 3 | title: References & Milestones 4 | sidebar_label: References & Milestones 5 | --- 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | 8 | when looking at results, it is important to *compare* them to a reference. It could to a previous version, results from a benchmark 9 | 10 | ## Comparing versus a reference 11 | In results pages, QA-Board always compares the commit you selected (labeled `new`) versus a reference (`ref`): 12 | 13 | new-vs-reference 14 | 15 | The reference is by default the latest commit from the project's reference branch: 16 | 17 | ```yaml title="qaboard.yaml" 18 | project: 19 | reference_branch: master 20 | ``` 21 | 22 | To change the selected `new` or `ref` commit, you can edit the commit ID field in the navbar. Hovering it gives you a menu with other options: 23 | commit-select-menu.png 24 | 25 | :::tip 26 | Clicking on the branch name in the navbar will select the latest commit on the branch. 27 | ::: 28 | 29 | ## Project References 30 | You can also list in *qaboard.yaml* other versions as milestones. 31 | 32 | ```yaml {4-7} title=""qaboard.yaml" 33 | project: 34 | reference_branch: master 35 | milestones: 36 | - release/v1.0.0 # tag 37 | - feature/better-perf # branch 38 | - e45123a3565 # commit id 39 | ``` 40 | 41 | ## Defining Milestones from QA-Board 42 | Every user can save milestones with the “star” icon in each commit navbar: 43 | 44 | save-as-milestone 45 | 46 | If needed, you can give them a name and leave notes: 47 | 48 | milestone-details 49 | 50 | You'll now be able to select them in the commit ID hover menu. 51 | 52 | :::tip 53 | Milestones can be shared with everybody - or kept private. 54 | ::: -------------------------------------------------------------------------------- /website/docs/storage/artifacts.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: artifacts 3 | sidebar_label: Artifacts 4 | title: Artifacts 5 | --- 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | 8 | Artifacts are needed by QA-Board to start tuning experiments 9 | 10 | ## What are artifacts? 11 | To run your code, you generally need: 12 | - binaries compiled automatically during the continuous integration. 13 | - wrapper scripts... 14 | - depending on how you view things, trained networks or calibrated data. 15 | 16 | > Defining those as artifacts makes it possible to upload them to QA-Board's storage, then start tuning experiments. 17 | 18 | 19 | ## Defining artifacts 20 | Artifacts are defined with: 21 | 22 | ```yaml title="qaboard.yaml" 23 | # Basic example 24 | artifacts: 25 | binary: 26 | glob: build/binary 27 | configurations: 28 | globs: 29 | - configurations/*.json 30 | - "*.yaml" 31 | ``` 32 | 33 | :::tip 34 | For convenience, *qaboard.yaml* and *qa/* are pre-defined as artifacts. 35 | ::: 36 | 37 | In QA-Board, you can view each commit's artifacts in the "Configuration" tab. 38 | 39 | 40 | Artifacts 41 | 42 | ## How to save artifacts? 43 | After your build is done, call: 44 | 45 | ```bash 46 | qa save-artifacts 47 | ``` 48 | 49 | Usually you can do it simply in your CI tool: 50 | 51 | ```yaml title="gitlab-ci.yml" 52 | after_script: 53 | - qa save-artifacts 54 | ``` -------------------------------------------------------------------------------- /website/docs/tuning-from-the-webapp.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: tuning-from-the-webapp 3 | sidebar_label: Tuning from QA-Board 4 | title: Starting tuning experiments from QA-Board 5 | --- 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | 8 | 9 | ## How to do tuning or trigger extra runs from QA-Board 10 | When doing QA or during development, you often want to run the code/configs from a given commit on new tests. QA-Board lets you define and runs batches of tests with extra tuning parameters: 11 | 12 | 13 | 14 | 15 | 16 | ## Enabling tuning from QA-Board 17 | 18 | :::note 19 | Tuning parameters that will be merged into `context.params` and appended to `context.configs`: in most cases you don't have to do anything special 20 | ::: 21 | 22 | ### 1. Build artifacts 23 | You must have defined and be using [artifacts](artifacts) 24 | 25 | ### 2. Distributed task queue 26 | You need to configure a task runner, that will execute tuning runs asynchronously. QA-Board ships with a `celery` integration so you'll just have to register a "worker" to get started. [Read more here](celery-integration)! 27 | -------------------------------------------------------------------------------- /website/docs/tuning-workflows.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: tuning-workflows 3 | sidebar_label: Tuning Workflows 4 | title: "Various Tuning Workflows" 5 | --- 6 | import useBaseUrl from '@docusaurus/useBaseUrl'; 7 | 8 | 9 | ## Tuning from QA-Board 10 | When doing QA or during development, you often want to run the code/configs from a given commit on new tests. QA-Board lets you define and runs batches of tests with extra tuning parameters: 11 | 12 | Tuning from the UI 13 | 14 | $revision ; make ; qa run` away. 15 | 16 | 17 | ## Workflows used for Tuning 18 | ### **Local** Workflow 19 | If you already have great development/debugging tools, use them! 20 | - At SIRC, `CDE` provides a great environment to run hardware chains and view images.** 21 | - For deep learning `tensorboard` is a great tool to investigate NNs. 22 | - Many people love to write one-off `matlab` script. 23 | 24 | > You can continue to use the existing tools! 25 | 26 | This said, it's worth having your IDE/debugger/scripts call your code via QA-Board's `qa` CLI. [Here is how to do it](debugging-runs-with-an-IDE). 27 | 28 | ### **Local configs > SharedStorage > Tuning from QA-Board** Workflow 29 | > Details: WIP 30 | 31 | ### **Local > QA-Board** Workflow 32 | QA-Board lets you runs your *local* code/configurations, and see results in the web application. **It gives you an easy way to tweak/compile/run your code and compare results across runs:** 33 | 34 | ```bash 35 | qa --share run [...] 36 | qa --share --label testing-some-logic-tweaks batch [...] 37 | ``` 38 | 39 | Results will appear in a new batch: 40 | 41 | selecting local runs 42 | local runs warning 43 | 44 | 45 | ### **Commit > CI > QA-Board** Qorkflow 46 | If you make changes in configuration files, you need to commit them. 47 | 1. Make changes 48 | 2. Commit the changes 49 | 3. Push your commit 50 | 4. See results in the UI 51 | -------------------------------------------------------------------------------- /website/makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | .PHONY: build publish publish-http 5 | 6 | build: 7 | NO_PROXY=localhost PORT=7000 NODE_TLS_REJECT_UNAUTHORIZED=0 DANGEROUSLY_DISABLE_HOST_CHECK=true HOST=0.0.0.0 yarn build && rm -rf build_qadocs && \cp -rf build build_qadocs 8 | 9 | 10 | # https://docusaurus.io/docs/en/publishing 11 | publish: 12 | PUBLISH=github_samsung_private DEPLOYMENT_BRANCH=gh-pages GITHUB_HOST=github.sec.samsung.net USE_SSH=true GIT_USER=git NO_PROXY=localhost PORT=7000 NODE_TLS_REJECT_UNAUTHORIZED=0 DANGEROUSLY_DISABLE_HOST_CHECK=true REACT_EDITOR=subl HOST=0.0.0.0 yarn deploy 13 | 14 | publish-http: 15 | PUBLISH=github_samsung_private DEPLOYMENT_BRANCH=gh-pages GITHUB_HOST=github.sec.samsung.net GIT_USER=arthur-flam NO_PROXY=localhost PORT=7000 NODE_TLS_REJECT_UNAUTHORIZED=0 DANGEROUSLY_DISABLE_HOST_CHECK=true REACT_EDITOR=subl HOST=0.0.0.0 yarn deploy 16 | 17 | publish-public: 18 | # ssh 19 | # PUBLISH=github_samsung_public DEPLOYMENT_BRANCH=gh-pages GITHUB_HOST=github.com USE_SSH=true GIT_USER=git NO_PROXY=localhost PORT=7000 NODE_TLS_REJECT_UNAUTHORIZED=0 DANGEROUSLY_DISABLE_HOST_CHECK=true REACT_EDITOR=subl HOST=0.0.0.0 yarn deploy 20 | # http 21 | PUBLISH=github_samsung_public DEPLOYMENT_BRANCH=gh-pages GITHUB_HOST=github.com GIT_USER=arthur-flam NO_PROXY=localhost PORT=7000 NODE_TLS_REJECT_UNAUTHORIZED=0 DANGEROUSLY_DISABLE_HOST_CHECK=true REACT_EDITOR=subl HOST=0.0.0.0 yarn deploy 22 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Apache-2.0", 3 | "license": "Apache-2.0", 4 | "version": "0.0.1", 5 | "private": false, 6 | "scripts": { 7 | "docusaurus": "docusaurus", 8 | "start": "docusaurus start", 9 | "build": "docusaurus build", 10 | "swizzle": "docusaurus swizzle", 11 | "deploy": "docusaurus deploy", 12 | "clear": "docusaurus clear", 13 | "serve": "docusaurus serve", 14 | "write-translations": "docusaurus write-translations", 15 | "write-heading-ids": "docusaurus write-heading-ids" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.0.0-beta.15", 19 | "@docusaurus/preset-classic": "2.0.0-beta.15", 20 | "@mdx-js/react": "^1.6.21", 21 | "clsx": "^1.1.1", 22 | "prism-react-renderer": "^1.2.1", 23 | "react": "^17.0.1", 24 | "react-dom": "^17.0.1" 25 | }, 26 | "browserslist": { 27 | "production": [ 28 | ">0.5%", 29 | "not dead", 30 | "not op_mini all" 31 | ], 32 | "development": [ 33 | "last 1 chrome version", 34 | "last 1 firefox version", 35 | "last 1 safari version" 36 | ] 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import styles from './HomepageFeatures.module.css'; 4 | 5 | const FeatureList = [ 6 | { 7 | title: <>What is QA-Board?, 8 | Svg: require('../../static/img/undraw/undraw_forming_ideas_0pav.svg').default, 9 | description: ( 10 | <> 11 | QA-Board helps Algorithms and QA engineers build great products. It offers powerful quality evaluation and collaboration tools. 12 | 13 | ), 14 | }, 15 | { 16 | title: <>What does it do?, 17 | Svg: require('../../static/img/undraw/undraw_ideation_2a64.svg').default, 18 | description: ( 19 | <> 20 | Compare results between commits. Create advanced visualizations from your existing output files. Track metrics across time. Start tuning experiments. 21 | 22 | ), 23 | }, 24 | { 25 | title: <>How do I use it?, 26 | Svg: require('../../static/img/undraw/undraw_factory_dy0a.svg').default, 27 | description: ( 28 | <> 29 | Run your code with a small CLI wrapper. You will see results from the web application. 30 | 31 | ), 32 | }, 33 | ]; 34 | 35 | 36 | function Feature({Svg, title, description}) { 37 | return ( 38 |
39 |
40 | 41 |
42 |
43 |

{title}

44 |

{description}

45 |
46 |
47 | ); 48 | } 49 | 50 | export default function HomepageFeatures() { 51 | return ( 52 |
53 |
54 |
55 | {FeatureList.map((props, idx) => ( 56 | 57 | ))} 58 |
59 |
60 |
61 | ); 62 | } 63 | -------------------------------------------------------------------------------- /website/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /website/src/components/feature.js: -------------------------------------------------------------------------------- 1 | // https://github.com/kamilkisiela/graphql-inspector/blob/2784988e06e38a36ebd82da02cb34a771387acfe/website/src/components/feature.js 2 | import React from 'react'; 3 | 4 | import clsx from 'clsx'; 5 | import styles from './feature.module.css'; 6 | 7 | export function Feature({reversed, title, img, text}) { 8 | const left =
{img}
; 9 | const right = ( 10 |
11 |

{title}

12 | {text} 13 |
14 | ); 15 | 16 | return ( 17 |
18 |
21 | {reversed ? ( 22 | <> 23 | {right} 24 | {left} 25 | 26 | ) : ( 27 | <> 28 | {left} 29 | {right} 30 | 31 | )} 32 |
33 |
34 | ); 35 | } -------------------------------------------------------------------------------- /website/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 966px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | 26 | 27 | .features { 28 | display: flex; 29 | align-items: center; 30 | padding: 2rem 0; 31 | width: 100%; 32 | } 33 | 34 | .featureImage { 35 | height: 200px; 36 | width: 200px; 37 | } 38 | 39 | 40 | 41 | .indexCtas { 42 | display: flex; 43 | align-items: center; 44 | margin-top: 24px; 45 | } 46 | 47 | .indexCtasGetStartedButton { 48 | border: 1px solid var(--ifm-color-primary); 49 | display: inline-block; 50 | line-height: 1.2em; 51 | text-decoration: none !important; 52 | text-transform: uppercase; 53 | transition: background 0.3s, color 0.3s; 54 | border-radius: 8px; 55 | border-width: 2px; 56 | color: #fff; 57 | font-size: 24px; 58 | font-weight: bold; 59 | padding: 18px 36px; 60 | } 61 | 62 | .indexCtasGitHubButtonWrapper { 63 | display: flex; 64 | } 65 | 66 | .indexCtasGitHubButton { 67 | border: none; 68 | margin-left: 24px; 69 | overflow: hidden; 70 | } 71 | 72 | @media only screen and (max-width: 768px) { 73 | .indexCtas { 74 | justify-content: center; 75 | } 76 | 77 | .indexCtasGitHubButton { 78 | display: none; 79 | } 80 | } -------------------------------------------------------------------------------- /website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/.nojekyll -------------------------------------------------------------------------------- /website/static/img/artifacts-tab.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/artifacts-tab.jpg -------------------------------------------------------------------------------- /website/static/img/bit-accuracy-viewer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/bit-accuracy-viewer.jpg -------------------------------------------------------------------------------- /website/static/img/commit-select-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/commit-select-menu.png -------------------------------------------------------------------------------- /website/static/img/commits-index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/commits-index.jpg -------------------------------------------------------------------------------- /website/static/img/comparing-new-and-reference.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/comparing-new-and-reference.png -------------------------------------------------------------------------------- /website/static/img/configure-jenkins-build-triggers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/configure-jenkins-build-triggers.png -------------------------------------------------------------------------------- /website/static/img/copy-windows-output-dir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/copy-windows-output-dir.png -------------------------------------------------------------------------------- /website/static/img/deep-learning/qa-wand-infoa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/deep-learning/qa-wand-infoa.png -------------------------------------------------------------------------------- /website/static/img/deep-learning/wandb-experiments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/deep-learning/wandb-experiments.png -------------------------------------------------------------------------------- /website/static/img/deep-learning/wandb-files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/deep-learning/wandb-files.png -------------------------------------------------------------------------------- /website/static/img/deep-learning/wandb-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/deep-learning/wandb-metrics.png -------------------------------------------------------------------------------- /website/static/img/deep-learning/wandb-reports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/deep-learning/wandb-reports.png -------------------------------------------------------------------------------- /website/static/img/dynamic-outputs-select.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/dynamic-outputs-select.gif -------------------------------------------------------------------------------- /website/static/img/dynamic-outputs.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/dynamic-outputs.gif -------------------------------------------------------------------------------- /website/static/img/dynamic-visualizations-frames.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/dynamic-visualizations-frames.jpg -------------------------------------------------------------------------------- /website/static/img/export-commit-folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/export-commit-folder.png -------------------------------------------------------------------------------- /website/static/img/export-files-commit.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/export-files-commit.jpg -------------------------------------------------------------------------------- /website/static/img/export-files-output.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/export-files-output.jpg -------------------------------------------------------------------------------- /website/static/img/export-files-viz.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/export-files-viz.jpg -------------------------------------------------------------------------------- /website/static/img/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /website/static/img/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /website/static/img/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /website/static/img/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #2d89ef 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /website/static/img/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /website/static/img/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /website/static/img/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/favicon.ico -------------------------------------------------------------------------------- /website/static/img/favicon/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/mstile-144x144.png -------------------------------------------------------------------------------- /website/static/img/favicon/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/mstile-150x150.png -------------------------------------------------------------------------------- /website/static/img/favicon/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/mstile-310x150.png -------------------------------------------------------------------------------- /website/static/img/favicon/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/mstile-310x310.png -------------------------------------------------------------------------------- /website/static/img/favicon/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/favicon/mstile-70x70.png -------------------------------------------------------------------------------- /website/static/img/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "QA-Board", 3 | "short_name": "QA-Board", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /website/static/img/first-outputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/first-outputs.png -------------------------------------------------------------------------------- /website/static/img/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/github.png -------------------------------------------------------------------------------- /website/static/img/gitlab-jenkins.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/gitlab-jenkins.gif -------------------------------------------------------------------------------- /website/static/img/hidden_by_default_switches.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/hidden_by_default_switches.png -------------------------------------------------------------------------------- /website/static/img/image-perceptural-diff.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/image-perceptural-diff.png -------------------------------------------------------------------------------- /website/static/img/image-viewer-autoroi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/image-viewer-autoroi.png -------------------------------------------------------------------------------- /website/static/img/image-viewer-selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/image-viewer-selection.png -------------------------------------------------------------------------------- /website/static/img/image-viewer.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/image-viewer.gif -------------------------------------------------------------------------------- /website/static/img/jenkins-gitlab.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/jenkins-gitlab.png -------------------------------------------------------------------------------- /website/static/img/local-runs-warning.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/local-runs-warning.jpg -------------------------------------------------------------------------------- /website/static/img/logo-name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/logo-name.png -------------------------------------------------------------------------------- /website/static/img/logo-text-white.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/logo-text-white.png -------------------------------------------------------------------------------- /website/static/img/milestone-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/milestone-details.png -------------------------------------------------------------------------------- /website/static/img/output-files.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/output-files.png -------------------------------------------------------------------------------- /website/static/img/output-windows-dir.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/output-windows-dir.jpg -------------------------------------------------------------------------------- /website/static/img/plotly-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/plotly-1.png -------------------------------------------------------------------------------- /website/static/img/plotly-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/plotly-2.png -------------------------------------------------------------------------------- /website/static/img/plotly-3d-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/plotly-3d-example.png -------------------------------------------------------------------------------- /website/static/img/projects-index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/projects-index.jpg -------------------------------------------------------------------------------- /website/static/img/pycharm-debugging-setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/pycharm-debugging-setup.png -------------------------------------------------------------------------------- /website/static/img/quantitative-metrics-on-viz.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/quantitative-metrics-on-viz.png -------------------------------------------------------------------------------- /website/static/img/quantitative-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/quantitative-metrics.png -------------------------------------------------------------------------------- /website/static/img/rich-metrics-in-card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/rich-metrics-in-card.png -------------------------------------------------------------------------------- /website/static/img/run-badges.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/run-badges.png -------------------------------------------------------------------------------- /website/static/img/samsung-logo-black.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/samsung-logo-black.jpg -------------------------------------------------------------------------------- /website/static/img/samsung-logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/samsung-logo-black.png -------------------------------------------------------------------------------- /website/static/img/samsung-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/samsung-logo.png -------------------------------------------------------------------------------- /website/static/img/save-as-milestone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/save-as-milestone.png -------------------------------------------------------------------------------- /website/static/img/select-batch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/select-batch.png -------------------------------------------------------------------------------- /website/static/img/selecting-local-runs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/selecting-local-runs.jpg -------------------------------------------------------------------------------- /website/static/img/share-github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/share-github.png -------------------------------------------------------------------------------- /website/static/img/share.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/share.jpg -------------------------------------------------------------------------------- /website/static/img/slides/aggregate.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/aggregate.jpg -------------------------------------------------------------------------------- /website/static/img/slides/always-compare.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/always-compare.jpg -------------------------------------------------------------------------------- /website/static/img/slides/commit-list.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/commit-list.jpg -------------------------------------------------------------------------------- /website/static/img/slides/flame-graphs.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/flame-graphs.jpg -------------------------------------------------------------------------------- /website/static/img/slides/image-viewer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/image-viewer.jpg -------------------------------------------------------------------------------- /website/static/img/slides/plotly-maps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/plotly-maps.png -------------------------------------------------------------------------------- /website/static/img/slides/regressions.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/regressions.jpg -------------------------------------------------------------------------------- /website/static/img/slides/show-files.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/show-files.jpg -------------------------------------------------------------------------------- /website/static/img/slides/triggers.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/triggers.jpg -------------------------------------------------------------------------------- /website/static/img/slides/triggers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/triggers.png -------------------------------------------------------------------------------- /website/static/img/slides/tuning.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/slides/tuning.jpg -------------------------------------------------------------------------------- /website/static/img/summary-metrics.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/summary-metrics.png -------------------------------------------------------------------------------- /website/static/img/text-viewer.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/text-viewer.jpg -------------------------------------------------------------------------------- /website/static/img/tuning-from-the-ui.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/tuning-from-the-ui.jpg -------------------------------------------------------------------------------- /website/static/img/ui-triggers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/ui-triggers.png -------------------------------------------------------------------------------- /website/static/img/winows-explorer-output-dir.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Samsung/qaboard/9485201c76b04c69e6901d8fc63db0c12b6cf35b/website/static/img/winows-explorer-output-dir.jpg --------------------------------------------------------------------------------