├── .cruft.json
├── .dockerignore
├── .editorconfig
├── .envs
├── .ci
│ ├── .django
│ └── .postgres
├── .local
│ ├── .django
│ └── .postgres
└── .production
│ └── .django-example
├── .gitattributes
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.md
│ └── new-issue.md
├── dependabot.yml
├── pull_request_template.md
└── workflows
│ ├── test.backend.yml
│ └── test.frontend.yml
├── .gitignore
├── .pre-commit-config.yaml
├── .readthedocs.yml
├── .vscode
├── extensions.json
└── settings.json
├── CONTRIBUTORS.txt
├── LICENSE
├── README.md
├── ami
├── __init__.py
├── base
│ ├── admin.py
│ ├── fields.py
│ ├── filters.py
│ ├── models.py
│ ├── pagination.py
│ ├── permissions.py
│ ├── schemas.py
│ ├── serializers.py
│ └── views.py
├── conftest.py
├── contrib
│ ├── __init__.py
│ ├── middleware.py
│ └── sites
│ │ ├── __init__.py
│ │ └── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_alter_domain_unique.py
│ │ ├── 0003_set_site_domain_and_name.py
│ │ ├── 0004_alter_options_ordering_domain.py
│ │ └── __init__.py
├── exports
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── base.py
│ ├── format_types.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── registry.py
│ ├── serializers.py
│ ├── signals.py
│ ├── tests.py
│ ├── utils.py
│ └── views.py
├── jobs
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── management
│ │ └── commands
│ │ │ └── update_stale_jobs.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_alter_job_config.py
│ │ ├── 0003_job_pipeline.py
│ │ ├── 0004_job_delay.py
│ │ ├── 0005_alter_job_progress.py
│ │ ├── 0006_alter_job_config_alter_job_progress.py
│ │ ├── 0007_alter_job_options_alter_job_progress_and_more.py
│ │ ├── 0008_alter_job_pipeline.py
│ │ ├── 0009_remove_job_config.py
│ │ ├── 0010_job_limit_job_shuffle.py
│ │ ├── 0011_alter_job_limit.py
│ │ ├── 0011_job_job_type_key.py
│ │ ├── 0012_alter_job_limit.py
│ │ ├── 0013_add_job_logs.py
│ │ ├── 0013_merge_0011_alter_job_limit_0012_alter_job_limit.py
│ │ ├── 0014_alter_job_progress.py
│ │ ├── 0015_merge_20250117_2100.py
│ │ ├── 0016_job_data_export_job_params_alter_job_job_type_key.py
│ │ └── __init__.py
│ ├── models.py
│ ├── serializers.py
│ ├── tasks.py
│ ├── tests.py
│ └── views.py
├── labelstudio
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── hooks.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ └── __init__.py
│ ├── models.py
│ ├── serializers.py
│ └── views.py
├── main
│ ├── __init__.py
│ ├── admin.py
│ ├── api
│ │ ├── serializers.py
│ │ └── views.py
│ ├── apps.py
│ ├── charts.py
│ ├── fixtures
│ │ ├── occurrences.json
│ │ ├── quebec-vermont-taxa-20221220.json
│ │ └── quebec-vermont-uk-denmark-taxa-20230914.json
│ ├── management
│ │ ├── commands
│ │ │ ├── assign_roles.py
│ │ │ ├── create_demo_project.py
│ │ │ ├── fix_missing_relationships.py
│ │ │ ├── fix_timestamps.py
│ │ │ ├── import_source_images.py
│ │ │ ├── import_taxa.py
│ │ │ └── import_trapdata_project.py
│ │ └── main.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_remove_identification_unique_primary_identification_and_more.py
│ │ ├── 0003_alter_identification_options_and_more.py
│ │ ├── 0004_identification_agreed_with_identification_and_more.py
│ │ ├── 0004_search_extensions.py
│ │ ├── 0005_taxon_display_name.py
│ │ ├── 0006_merge_20230926_2353.py
│ │ ├── 0007_sourceimageupload.py
│ │ ├── 0008_rename_capture_sourceimageupload_source_image_and_more.py
│ │ ├── 0009_alter_sourceimageupload_source_image.py
│ │ ├── 0010_alter_sourceimagecollection_kwargs_and_more.py
│ │ ├── 0011_taxon_author_taxon_bold_taxon_bin_and_more.py
│ │ ├── 0012_alter_taxon_rank.py
│ │ ├── 0013_sourceimage_detections_count.py
│ │ ├── 0014_job_source_image_collection_job_source_image_single_and_more.py
│ │ ├── 0015_delete_job.py
│ │ ├── 0016_pipeline.py
│ │ ├── 0017_alter_site_options_site_project.py
│ │ ├── 0017_remove_pipeline_algorithms.py
│ │ ├── 0018_delete_existing_pipeline_algorithms.py
│ │ ├── 0018_device_project.py
│ │ ├── 0019_delete_pipeline_remove_detection_detection_algorithm_and_more.py
│ │ ├── 0019_s3storagesource_project.py
│ │ ├── 0020_detection_detection_algorithm.py
│ │ ├── 0021_merge_20231123_0307.py
│ │ ├── 0022_alter_sourceimagecollection_method_alter_taxon_rank.py
│ │ ├── 0023_taxon_main_taxon_orderin_4ffb7b_idx.py
│ │ ├── 0024_deployment_captures_count_and_more.py
│ │ ├── 0025_update_deployment_aggregates.py
│ │ ├── 0026_occurrence_determination_score.py
│ │ ├── 0027_update_occurrence_scores.py
│ │ ├── 0028_alter_occurrence_options_alter_project_options_and_more.py
│ │ ├── 0029_alter_deployment_device_and_more.py
│ │ ├── 0030_identification_comment.py
│ │ ├── 0031_s3storagesource_use_presigned_urls_and_more.py
│ │ ├── 0032_alter_s3storagesource_public_base_url.py
│ │ ├── 0033_remove_s3storagesource_use_presigned_urls_and_more.py
│ │ ├── 0034_remove_taxon_parents_taxon_parents_json.py
│ │ ├── 0035_alter_taxon_parents_json_alter_taxon_rank.py
│ │ ├── 0036_event_calculated_fields_updated_at_and_more.py
│ │ ├── 0037_alter_detection_path_and_more.py
│ │ ├── 0038_alter_detection_path_alter_sourceimage_event_and_more.py
│ │ ├── 0039_project_users_squashed_0043_rename_users_project_members.py
│ │ ├── 0039_remove_classification_raw_output_and_more.py
│ │ ├── 0040_alter_classification_logits_and_more.py
│ │ ├── 0044_alter_project_options_squashed_0047_alter_project_options.py
│ │ ├── 0044_merge_20250124_2333.py
│ │ ├── 0045_alter_classification_algorithm.py
│ │ ├── 0046_add_taxon_common_name_placeholder.py
│ │ ├── 0048_alter_project_options_squashed_0051_alter_project_options.py
│ │ ├── 0052_merge_20250207_1012.py
│ │ ├── 0053_alter_classification_algorithm.py
│ │ ├── 0054_alter_project_options_squashed_0055_alter_project_options.py
│ │ ├── 0056_merge_20250218_1405.py
│ │ ├── 0057_merge_20250220_0022.py
│ │ ├── 0058_alter_project_options.py
│ │ ├── 0059_alter_project_options.py
│ │ └── __init__.py
│ ├── models.py
│ ├── signals.py
│ └── tests.py
├── ml
│ ├── __init__.py
│ ├── admin.py
│ ├── apps.py
│ ├── management
│ │ └── commands
│ │ │ ├── create_missing_detection_images.py
│ │ │ └── remove_duplicate_classifications.py
│ ├── media.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_pipeline_endpoint_url.py
│ │ ├── 0003_pipeline_slug.py
│ │ ├── 0004_populate_pipeline_slugs.py
│ │ ├── 0005_alter_pipeline_slug.py
│ │ ├── 0006_alter_pipeline_endpoint_url_alter_pipeline_projects.py
│ │ ├── 0007_add_processing_service.py
│ │ ├── 0007_algorithmcategorymap_algorithm_category_map.py
│ │ ├── 0008_algorithmcategorymap_description_and_more.py
│ │ ├── 0009_algorithm_task_type.py
│ │ ├── 0010_alter_algorithm_version.py
│ │ ├── 0011_alter_algorithm_task_type_alter_algorithm_url_and_more.py
│ │ ├── 0012_alter_algorithm_unique_together.py
│ │ ├── 0013_remove_algorithm_url_remove_algorithmcategorymap_url_and_more.py
│ │ ├── 0014_rename_model_keys.py
│ │ ├── 0015_update_existing_intermediate_classifications.py
│ │ ├── 0016_merge_20250117_2101.py
│ │ ├── 0017_alter_algorithm_unique_together.py
│ │ ├── 0018_add_processing_services_status_check_celery_beat_task.py
│ │ ├── 0019_alter_algorithm_task_type.py
│ │ ├── 0020_projectpipelineconfig_alter_pipeline_projects.py
│ │ ├── 0021_pipeline_default_config.py
│ │ ├── 0022_alter_pipeline_default_config.py
│ │ └── __init__.py
│ ├── models
│ │ ├── __init__.py
│ │ ├── algorithm.py
│ │ ├── pipeline.py
│ │ ├── processing_service.py
│ │ └── project_pipeline_config.py
│ ├── schemas.py
│ ├── serializers.py
│ ├── tasks.py
│ ├── tests.py
│ └── views.py
├── static
│ ├── css
│ │ └── project.css
│ ├── fonts
│ │ └── .gitkeep
│ ├── images
│ │ └── favicons
│ │ │ └── favicon.ico
│ └── js
│ │ └── project.js
├── tasks.py
├── templates
│ ├── 403.html
│ ├── 404.html
│ ├── 500.html
│ ├── account
│ │ ├── account_inactive.html
│ │ ├── base.html
│ │ ├── email.html
│ │ ├── email_confirm.html
│ │ ├── login.html
│ │ ├── logout.html
│ │ ├── password_change.html
│ │ ├── password_reset.html
│ │ ├── password_reset_done.html
│ │ ├── password_reset_from_key.html
│ │ ├── password_reset_from_key_done.html
│ │ ├── password_set.html
│ │ ├── signup.html
│ │ ├── signup_closed.html
│ │ ├── verification_sent.html
│ │ └── verified_email_required.html
│ ├── base.html
│ ├── labelstudio
│ │ ├── all_in_one.xml
│ │ ├── binary_classification.xml
│ │ ├── initial_object_detection.xml
│ │ └── species_classification.xml
│ ├── pages
│ │ ├── about.html
│ │ └── home.html
│ └── users
│ │ ├── user_detail.html
│ │ └── user_form.html
├── tests
│ ├── fixtures
│ │ ├── images.py
│ │ ├── main.py
│ │ ├── ml.py
│ │ ├── signals.py
│ │ └── storage.py
│ ├── test_doctests.py
│ ├── test_merge_production_dotenvs_in_dotenv.py
│ └── test_storage.py
├── users
│ ├── __init__.py
│ ├── adapters.py
│ ├── admin.py
│ ├── api
│ │ └── serializers.py
│ ├── apps.py
│ ├── context_processors.py
│ ├── forms.py
│ ├── managers.py
│ ├── migrations
│ │ ├── 0001_initial.py
│ │ ├── 0002_user_image.py
│ │ ├── 0003_lowercase_existing_emails.py
│ │ └── __init__.py
│ ├── models.py
│ ├── roles.py
│ ├── signals.py
│ ├── tasks.py
│ ├── tests
│ │ ├── __init__.py
│ │ ├── factories.py
│ │ ├── test_admin.py
│ │ ├── test_drf_urls.py
│ │ ├── test_drf_views.py
│ │ ├── test_forms.py
│ │ ├── test_managers.py
│ │ ├── test_models.py
│ │ ├── test_swagger.py
│ │ ├── test_tasks.py
│ │ └── test_user_auth.py
│ ├── urls.py
│ └── views.py
└── utils
│ ├── __init__.py
│ ├── dates.py
│ ├── fields.py
│ ├── requests.py
│ ├── s3.py
│ ├── schemas.py
│ ├── storages.py
│ └── tests.py
├── compose
├── local
│ ├── django
│ │ ├── Dockerfile
│ │ ├── celery
│ │ │ ├── beat
│ │ │ │ └── start
│ │ │ ├── flower
│ │ │ │ └── start
│ │ │ └── worker
│ │ │ │ └── start
│ │ └── start
│ ├── docs
│ │ ├── Dockerfile
│ │ └── start
│ ├── minio
│ │ ├── init.sh
│ │ └── nginx.conf
│ ├── postgres
│ │ ├── Dockerfile
│ │ ├── maintenance
│ │ │ ├── _sourced
│ │ │ │ ├── constants.sh
│ │ │ │ ├── countdown.sh
│ │ │ │ ├── messages.sh
│ │ │ │ └── yes_no.sh
│ │ │ ├── backup
│ │ │ ├── backups
│ │ │ ├── pgtune
│ │ │ └── restore
│ │ └── postgresql.conf
│ └── ui
│ │ └── Dockerfile
└── production
│ ├── aws
│ ├── Dockerfile
│ └── maintenance
│ │ ├── download
│ │ └── upload
│ ├── django
│ ├── Dockerfile
│ ├── celery
│ │ ├── beat
│ │ │ └── start
│ │ ├── flower
│ │ │ └── start
│ │ └── worker
│ │ │ └── start
│ ├── entrypoint
│ └── start
│ ├── nginx
│ ├── Dockerfile
│ └── default.conf
│ └── traefik
│ ├── Dockerfile
│ └── traefik.yml
├── config
├── __init__.py
├── api_router.py
├── asgi.py
├── celery_app.py
├── newrelic.ini
├── settings
│ ├── __init__.py
│ ├── base.py
│ ├── local.py
│ ├── production.py
│ └── test.py
├── urls.py
├── websocket.py
└── wsgi.py
├── data
├── README.md
└── db
│ └── snapshots
│ └── .gitkeep
├── docker-compose.ci.yml
├── docker-compose.production.yml
├── docker-compose.staging.yml
├── docker-compose.worker.yml
├── docker-compose.yml
├── docs
├── Makefile
├── __init__.py
├── conf.py
├── diagrams
│ ├── models.mermaid
│ └── models.mermaid.svg
├── howto.rst
├── index.rst
├── make.bat
└── users.rst
├── locale
├── README.md
├── en_US
│ └── LC_MESSAGES
│ │ └── django.po
└── pt_BR
│ └── LC_MESSAGES
│ └── django.po
├── manage.py
├── merge_production_dotenvs_in_dotenv.py
├── processing_services
├── README.md
├── docker-compose.yml
├── example
│ ├── Dockerfile
│ ├── api
│ │ ├── __init__.py
│ │ ├── algorithms.py
│ │ ├── api.py
│ │ ├── pipelines.py
│ │ ├── schemas.py
│ │ ├── test.py
│ │ └── utils.py
│ ├── docker-compose.yml
│ ├── main.py
│ └── requirements.txt
└── minimal
│ ├── Dockerfile
│ ├── api
│ ├── __init__.py
│ ├── algorithms.py
│ ├── api.py
│ ├── pipelines.py
│ ├── schemas.py
│ ├── test.py
│ └── utils.py
│ ├── docker-compose.yml
│ ├── main.py
│ └── requirements.txt
├── pyproject.toml
├── requirements
├── base.txt
├── local.txt
└── production.txt
├── setup.cfg
└── ui
├── .eslintrc.json
├── .gitattributes
├── .gitignore
├── .nvmrc
├── .prettierrc.json
├── .yarn
└── releases
│ └── yarn-berry.cjs
├── .yarnrc.yml
├── README.md
├── index.html
├── netlify.toml
├── package.json
├── postcss.config.js
├── public
├── android-chrome-192x192.png
├── android-chrome-256x256.png
├── android-chrome-512x512.png
├── apple-touch-icon-precomposed.png
├── apple-touch-icon.png
├── browserconfig.xml
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon.ico
├── fonts
│ └── Mazzard
│ │ ├── mazzardsoftm-bold.otf
│ │ ├── mazzardsoftm-bolditalic.otf
│ │ ├── mazzardsoftm-italic.otf
│ │ ├── mazzardsoftm-medium.otf
│ │ ├── mazzardsoftm-mediumitalic.otf
│ │ ├── mazzardsoftm-regular.otf
│ │ ├── mazzardsoftm-semibold.otf
│ │ └── mazzardsoftm-semibolditalic.otf
├── robots.txt
└── site.webmanifest
├── src
├── app.module.scss
├── app.tsx
├── components
│ ├── analytics.tsx
│ ├── blueprint-collection
│ │ ├── blueprint-collection.module.scss
│ │ └── blueprint-collection.tsx
│ ├── breadcrumbs
│ │ ├── breadcrumbs.module.scss
│ │ └── breadcrumbs.tsx
│ ├── constants.ts
│ ├── cookie-dialog
│ │ ├── cookie-dialog.module.scss
│ │ └── cookie-dialog.tsx
│ ├── empty-state
│ │ └── empty-state.tsx
│ ├── error-boundary
│ │ ├── error-boundary.module.scss
│ │ └── error-boundary.tsx
│ ├── error-state
│ │ └── error-state.tsx
│ ├── fetch-info
│ │ ├── fetch-info.module.scss
│ │ └── fetch-info.tsx
│ ├── filtering
│ │ ├── filter-control.tsx
│ │ ├── filter-section.tsx
│ │ ├── filters
│ │ │ ├── algorithm-filter.tsx
│ │ │ ├── collection-filter.tsx
│ │ │ ├── date-filter.tsx
│ │ │ ├── image-filter.tsx
│ │ │ ├── pipeline-filter.tsx
│ │ │ ├── score-filter.tsx
│ │ │ ├── session-filter.tsx
│ │ │ ├── station-filter.tsx
│ │ │ ├── status-filter.tsx
│ │ │ ├── taxa-list-filter.tsx
│ │ │ ├── taxon-filter.tsx
│ │ │ ├── type-filter.tsx
│ │ │ ├── types.ts
│ │ │ ├── verification-status-filter.tsx
│ │ │ └── verified-by-filter.tsx
│ │ └── utils.ts
│ ├── form
│ │ ├── delete-form
│ │ │ └── delete-form.tsx
│ │ ├── form-controller.tsx
│ │ ├── form-field.tsx
│ │ ├── layout
│ │ │ ├── layout.module.scss
│ │ │ └── layout.tsx
│ │ └── types.ts
│ ├── gallery
│ │ ├── gallery.module.scss
│ │ └── gallery.tsx
│ ├── header
│ │ ├── antenna-primary.svg
│ │ ├── header.module.scss
│ │ ├── header.tsx
│ │ ├── user-info-dialog
│ │ │ ├── user-info-dialog.module.scss
│ │ │ ├── user-info-dialog.tsx
│ │ │ ├── user-info-form
│ │ │ │ ├── user-email-field.tsx
│ │ │ │ ├── user-info-form.module.scss
│ │ │ │ ├── user-info-form.tsx
│ │ │ │ └── user-password-field.tsx
│ │ │ └── user-info-image-upload
│ │ │ │ ├── user-info-image-upload.module.scss
│ │ │ │ └── user-info-image-upload.tsx
│ │ └── version-info
│ │ │ ├── version-info.module.scss
│ │ │ └── version-info.tsx
│ ├── info-page
│ │ ├── code-of-conduct-page
│ │ │ ├── code-of-conduct-page.tsx
│ │ │ └── code-of-conduct.md
│ │ ├── info-page.module.scss
│ │ ├── info-page.tsx
│ │ └── terms-of-service-page
│ │ │ ├── terms-of-service-page.tsx
│ │ │ └── terms-of-service.md
│ ├── license-info
│ │ └── license-info.tsx
│ ├── menu
│ │ ├── menu.module.scss
│ │ └── menu.tsx
│ ├── taxon-search
│ │ ├── buildTree.ts
│ │ ├── taxon-search.tsx
│ │ ├── taxon-select.tsx
│ │ ├── types.ts
│ │ └── useTaxonSearch.ts
│ ├── taxon
│ │ └── taxon-ranks
│ │ │ ├── taxon-ranks.module.scss
│ │ │ └── taxon-ranks.tsx
│ └── terms-of-service-info
│ │ ├── terms-of-service-info.module.scss
│ │ └── terms-of-service-info.tsx
├── data-services
│ ├── constants.ts
│ ├── hooks
│ │ ├── algorithm
│ │ │ ├── useAlgorithmDetails.ts
│ │ │ └── useAlgorithms.ts
│ │ ├── auth
│ │ │ ├── tests
│ │ │ │ ├── useAuthorizedQuery.test.ts
│ │ │ │ ├── useLogin.test.ts
│ │ │ │ ├── useLogout.test.ts
│ │ │ │ └── useUserInfo.test.ts
│ │ │ ├── useAuthorizedQuery.ts
│ │ │ ├── useLogin.ts
│ │ │ ├── useLogout.ts
│ │ │ ├── useResetPassword.ts
│ │ │ ├── useResetPasswordConfirm.ts
│ │ │ ├── useSignUp.ts
│ │ │ ├── useUpdateUserEmail.ts
│ │ │ ├── useUpdateUserInfo.ts
│ │ │ ├── useUpdateUserPassword.ts
│ │ │ └── useUserInfo.ts
│ │ ├── captures
│ │ │ ├── useCaptureDetails.ts
│ │ │ ├── useCaptures.ts
│ │ │ ├── useDeleteCapture.ts
│ │ │ ├── useStarCapture.ts
│ │ │ └── useUploadCapture.ts
│ │ ├── collections
│ │ │ ├── useCollectionDetails.ts
│ │ │ ├── useCollections.ts
│ │ │ └── usePopulateCollection.ts
│ │ ├── deployments
│ │ │ ├── useCreateDeployment.ts
│ │ │ ├── useDeleteDeployment.ts
│ │ │ ├── useDeployments.ts
│ │ │ ├── useDeploymentsDetails.ts
│ │ │ ├── useSyncDeploymentSourceImages.ts
│ │ │ ├── useUpdateDeployment.ts
│ │ │ └── utils.ts
│ │ ├── entities
│ │ │ ├── types.ts
│ │ │ ├── useCreateEntity.ts
│ │ │ ├── useDeleteEntity.ts
│ │ │ ├── useEntities.ts
│ │ │ ├── useUpdateEntity.ts
│ │ │ └── utils.ts
│ │ ├── exports
│ │ │ ├── useExportDetails.ts
│ │ │ └── useExports.ts
│ │ ├── identifications
│ │ │ ├── types.ts
│ │ │ ├── useClassificationDetails.ts
│ │ │ ├── useCreateIdentification.ts
│ │ │ ├── useCreateIdentifications.ts
│ │ │ └── useDeleteIdentification.ts
│ │ ├── jobs
│ │ │ ├── useCancelJob.ts
│ │ │ ├── useCreateJob.ts
│ │ │ ├── useDeleteJob.ts
│ │ │ ├── useJobDetails.ts
│ │ │ ├── useJobs.ts
│ │ │ ├── useQueueJob.ts
│ │ │ └── useRetryJob.ts
│ │ ├── occurrences
│ │ │ ├── useOccurrenceDetails.ts
│ │ │ └── useOccurrences.ts
│ │ ├── pages
│ │ │ ├── types.ts
│ │ │ └── usePageDetails.ts
│ │ ├── pipelines
│ │ │ ├── usePipelineDetails.ts
│ │ │ └── usePipelines.ts
│ │ ├── processing-services
│ │ │ ├── usePopulateProcessingService.ts
│ │ │ ├── useProcessingServiceDetails.ts
│ │ │ ├── useProcessingServices.ts
│ │ │ └── useTestProcessingServiceConnection.ts
│ │ ├── projects
│ │ │ ├── useCreateProject.ts
│ │ │ ├── useDeleteProject.ts
│ │ │ ├── useProjectDetails.ts
│ │ │ ├── useProjects.ts
│ │ │ ├── useUpdateProject.ts
│ │ │ └── utils.ts
│ │ ├── sessions
│ │ │ ├── useSessionDetails.ts
│ │ │ ├── useSessionTimeline.ts
│ │ │ └── useSessions.ts
│ │ ├── species
│ │ │ ├── useSpecies.ts
│ │ │ └── useSpeciesDetails.ts
│ │ ├── storage-sources
│ │ │ ├── useStorageDetails.ts
│ │ │ ├── useStorageSources.ts
│ │ │ └── useTestStorageConnection.ts
│ │ ├── taxa-lists
│ │ │ └── useTaxaLists.ts
│ │ └── useStatus.ts
│ ├── models
│ │ ├── algorithm.ts
│ │ ├── capture-details.ts
│ │ ├── capture.ts
│ │ ├── classification-details.ts
│ │ ├── collection.ts
│ │ ├── deployment-details.ts
│ │ ├── deployment.ts
│ │ ├── entity.ts
│ │ ├── export.ts
│ │ ├── job-details.ts
│ │ ├── job.ts
│ │ ├── occurrence-details.ts
│ │ ├── occurrence.ts
│ │ ├── pipeline.ts
│ │ ├── processing-service.ts
│ │ ├── project.ts
│ │ ├── session-details.ts
│ │ ├── session.ts
│ │ ├── species-details.ts
│ │ ├── species.ts
│ │ ├── storage.ts
│ │ ├── taxa-list.ts
│ │ ├── taxa.ts
│ │ └── timeline-tick.ts
│ ├── types.ts
│ └── utils.ts
├── design-system
│ ├── components
│ │ ├── box
│ │ │ ├── box.module.scss
│ │ │ └── box.tsx
│ │ ├── bulk-action-bar
│ │ │ ├── bulk-action-bar.module.scss
│ │ │ └── bulk-action-bar.tsx
│ │ ├── button
│ │ │ ├── button.module.scss
│ │ │ └── button.tsx
│ │ ├── card
│ │ │ ├── card.module.scss
│ │ │ └── card.tsx
│ │ ├── checkbox
│ │ │ ├── checkbox.module.scss
│ │ │ └── checkbox.tsx
│ │ ├── collections-picker.tsx
│ │ ├── combo-box
│ │ │ ├── combo-box-simple
│ │ │ │ └── combo-box-simple.tsx
│ │ │ └── styles.module.scss
│ │ ├── dialog
│ │ │ ├── dialog.module.scss
│ │ │ └── dialog.tsx
│ │ ├── file-input
│ │ │ ├── file-input.module.scss
│ │ │ ├── file-input.tsx
│ │ │ └── types.ts
│ │ ├── form-stepper
│ │ │ ├── form-stepper.module.scss
│ │ │ └── form-stepper.tsx
│ │ ├── icon-button
│ │ │ ├── icon-button.module.scss
│ │ │ └── icon-button.tsx
│ │ ├── icon
│ │ │ ├── assets
│ │ │ │ ├── batch-id.svg
│ │ │ │ ├── checkmark.svg
│ │ │ │ ├── close.svg
│ │ │ │ ├── deployments.svg
│ │ │ │ ├── detections.svg
│ │ │ │ ├── download.svg
│ │ │ │ ├── filters.svg
│ │ │ │ ├── gallery-view.svg
│ │ │ │ ├── identifiers.svg
│ │ │ │ ├── images.svg
│ │ │ │ ├── info.svg
│ │ │ │ ├── members.svg
│ │ │ │ ├── occurrences.svg
│ │ │ │ ├── overview.svg
│ │ │ │ ├── photograph.svg
│ │ │ │ ├── play-button.svg
│ │ │ │ ├── radix
│ │ │ │ │ ├── check.svg
│ │ │ │ │ ├── circle-backslash.svg
│ │ │ │ │ ├── clock.svg
│ │ │ │ │ ├── cross.svg
│ │ │ │ │ ├── error.svg
│ │ │ │ │ ├── external-link.svg
│ │ │ │ │ ├── heart-filled.svg
│ │ │ │ │ ├── heart.svg
│ │ │ │ │ ├── minus.svg
│ │ │ │ │ ├── options.svg
│ │ │ │ │ ├── pencil.svg
│ │ │ │ │ ├── plus.svg
│ │ │ │ │ ├── question-mark.svg
│ │ │ │ │ ├── search.svg
│ │ │ │ │ ├── toggle-down.svg
│ │ │ │ │ ├── toggle-left.svg
│ │ │ │ │ ├── toggle-right.svg
│ │ │ │ │ ├── trash.svg
│ │ │ │ │ └── update.svg
│ │ │ │ ├── sessions.svg
│ │ │ │ ├── settings.svg
│ │ │ │ ├── shield-check.svg
│ │ │ │ ├── sort.svg
│ │ │ │ ├── species.svg
│ │ │ │ └── table-view.svg
│ │ │ ├── icon.module.scss
│ │ │ └── icon.tsx
│ │ ├── image-carousel
│ │ │ ├── image-carousel.module.scss
│ │ │ ├── image-carousel.tsx
│ │ │ ├── types.ts
│ │ │ └── utils.ts
│ │ ├── image-upload
│ │ │ ├── image-upload.module.scss
│ │ │ └── image-upload.tsx
│ │ ├── info-block
│ │ │ ├── info-block.module.scss
│ │ │ └── info-block.tsx
│ │ ├── input
│ │ │ ├── input.module.scss
│ │ │ └── input.tsx
│ │ ├── loading-spinner
│ │ │ ├── loading-spinner.module.scss
│ │ │ └── loading-spinner.tsx
│ │ ├── navigation
│ │ │ ├── navigation-bar.module.scss
│ │ │ └── navigation-bar.tsx
│ │ ├── page-footer
│ │ │ ├── page-footer.module.scss
│ │ │ └── page-footer.tsx
│ │ ├── page-header
│ │ │ ├── page-header.module.scss
│ │ │ └── page-header.tsx
│ │ ├── pagination-bar
│ │ │ ├── getPageWindow.ts
│ │ │ ├── info-label
│ │ │ │ ├── getValueInRange.ts
│ │ │ │ ├── info-label.module.scss
│ │ │ │ └── info-label.tsx
│ │ │ ├── page-button
│ │ │ │ ├── page-button.module.scss
│ │ │ │ └── page-button.tsx
│ │ │ ├── pagination-bar.module.scss
│ │ │ └── pagination-bar.tsx
│ │ ├── plot-grid
│ │ │ ├── plot-grid.module.scss
│ │ │ └── plot-grid.tsx
│ │ ├── plot
│ │ │ ├── lazy-plot.tsx
│ │ │ ├── plot.module.scss
│ │ │ ├── plot.tsx
│ │ │ └── types.ts
│ │ ├── popover
│ │ │ ├── popover.module.scss
│ │ │ └── popover.tsx
│ │ ├── select
│ │ │ ├── select.module.scss
│ │ │ └── select.tsx
│ │ ├── slider
│ │ │ ├── dial.svg
│ │ │ ├── styles.module.scss
│ │ │ └── timestamp-slider.tsx
│ │ ├── status
│ │ │ ├── status-bar.tsx
│ │ │ ├── status-marker
│ │ │ │ ├── status-marker.module.scss
│ │ │ │ └── status-marker.tsx
│ │ │ └── types.ts
│ │ ├── table
│ │ │ ├── basic-table-cell
│ │ │ │ ├── basic-table-cell.module.scss
│ │ │ │ └── basic-table-cell.tsx
│ │ │ ├── column-settings
│ │ │ │ ├── column-settings.module.scss
│ │ │ │ └── column-settings.tsx
│ │ │ ├── image-table-cell
│ │ │ │ ├── image-table-cell.module.scss
│ │ │ │ └── image-table-cell.tsx
│ │ │ ├── status-table-cell
│ │ │ │ ├── status-table-cell.module.scss
│ │ │ │ └── status-table-cell.tsx
│ │ │ ├── table-header
│ │ │ │ ├── table-header.module.scss
│ │ │ │ └── table-header.tsx
│ │ │ ├── table
│ │ │ │ ├── sticky-header-table.tsx
│ │ │ │ ├── table.module.scss
│ │ │ │ ├── table.tsx
│ │ │ │ ├── useScrollFader.ts
│ │ │ │ └── vh-sticky-table-header
│ │ │ │ │ └── index.ts
│ │ │ └── types.ts
│ │ ├── tabs
│ │ │ ├── tabs.module.scss
│ │ │ └── tabs.tsx
│ │ ├── toggle-group
│ │ │ ├── toggle-group.module.scss
│ │ │ └── toggle-group.tsx
│ │ ├── tooltip
│ │ │ ├── basic-tooltip.tsx
│ │ │ ├── tooltip.module.scss
│ │ │ └── tooltip.tsx
│ │ └── wizard
│ │ │ ├── status-bullet
│ │ │ ├── status-bullet.module.scss
│ │ │ └── status-bullet.tsx
│ │ │ ├── wizard.module.scss
│ │ │ └── wizard.tsx
│ ├── map
│ │ ├── config.ts
│ │ ├── editable-map
│ │ │ ├── editable-map.tsx
│ │ │ └── editable-marker.tsx
│ │ ├── minimap-control.tsx
│ │ ├── multi-marker-map
│ │ │ └── multi-marker-map.tsx
│ │ ├── pin.svg
│ │ ├── styles.module.scss
│ │ └── types.ts
│ └── variables
│ │ ├── colors.scss
│ │ ├── typography.scss
│ │ └── variables.scss
├── index.css
├── index.tsx
├── pages
│ ├── algorithm-details
│ │ ├── algorithm-details-dialog.tsx
│ │ └── styles.module.scss
│ ├── auth
│ │ ├── auth.module.scss
│ │ ├── auth.tsx
│ │ ├── login.tsx
│ │ ├── reset-password-confirm.tsx
│ │ ├── reset-password.tsx
│ │ └── sign-up.tsx
│ ├── collection-details
│ │ ├── capture-columns.tsx
│ │ ├── capture-gallery.tsx
│ │ └── collection-details.tsx
│ ├── deployment-details
│ │ ├── delete-deployment-dialog.tsx
│ │ ├── deployment-details-dialog.tsx
│ │ ├── deployment-details-form
│ │ │ ├── config.ts
│ │ │ ├── deployment-details-form.tsx
│ │ │ ├── section-example-captures
│ │ │ │ ├── section-example-captures.module.scss
│ │ │ │ ├── section-example-captures.tsx
│ │ │ │ └── useCaptureError.ts
│ │ │ ├── section-general
│ │ │ │ └── section-general.tsx
│ │ │ ├── section-location
│ │ │ │ ├── geo-search
│ │ │ │ │ ├── geo-search.tsx
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── useGeoSearch.ts
│ │ │ │ ├── location-map
│ │ │ │ │ ├── location-map.module.scss
│ │ │ │ │ └── location-map.tsx
│ │ │ │ └── section-location.tsx
│ │ │ ├── section-source-images
│ │ │ │ ├── actions
│ │ │ │ │ ├── styles.module.scss
│ │ │ │ │ └── sync-source-images.tsx
│ │ │ │ └── section-source-images.tsx
│ │ │ └── types.ts
│ │ ├── deployment-details-info.tsx
│ │ ├── new-deployment-dialog.tsx
│ │ └── styles.module.scss
│ ├── deployments
│ │ ├── deployment-columns.tsx
│ │ ├── deployments.module.scss
│ │ └── deployments.tsx
│ ├── export-details
│ │ ├── export-details-dialog.tsx
│ │ └── styles.module.scss
│ ├── job-details
│ │ ├── job-actions
│ │ │ ├── cancel-job.tsx
│ │ │ ├── queue-job.tsx
│ │ │ └── retry-job.tsx
│ │ ├── job-details-form
│ │ │ ├── job-details-form.tsx
│ │ │ └── pipelines-picker.tsx
│ │ ├── job-details.module.scss
│ │ ├── job-details.tsx
│ │ ├── job-stage-label
│ │ │ ├── job-stage-label.module.scss
│ │ │ └── job-stage-label.tsx
│ │ └── new-job-dialog.tsx
│ ├── jobs
│ │ ├── delete-jobs-dialog.tsx
│ │ ├── jobs-columns.tsx
│ │ ├── jobs.module.scss
│ │ └── jobs.tsx
│ ├── occurrence-details
│ │ ├── agree
│ │ │ └── agree.tsx
│ │ ├── identification-card
│ │ │ ├── human-identification.tsx
│ │ │ ├── machine-avatar.svg
│ │ │ └── machine-prediction.tsx
│ │ ├── occurrence-details.module.scss
│ │ ├── occurrence-details.tsx
│ │ ├── reject-id
│ │ │ ├── constants.ts
│ │ │ ├── id-button.tsx
│ │ │ ├── id-quick-actions.module.scss
│ │ │ ├── id-quick-actions.tsx
│ │ │ ├── useRecentOptions.ts
│ │ │ └── utils.ts
│ │ ├── status-label
│ │ │ ├── status-label.module.scss
│ │ │ └── status-label.tsx
│ │ ├── suggest-id
│ │ │ ├── suggest-id-popover.tsx
│ │ │ └── suggest-id.tsx
│ │ └── taxonomy-info
│ │ │ ├── taxonomy-info.module.scss
│ │ │ └── taxonomy-info.tsx
│ ├── occurrences
│ │ ├── occurrence-actions.tsx
│ │ ├── occurrence-columns.tsx
│ │ ├── occurrence-gallery.tsx
│ │ ├── occurrence-navigation.tsx
│ │ ├── occurrences.module.scss
│ │ └── occurrences.tsx
│ ├── pipeline-details
│ │ ├── pipeline-algorithms.tsx
│ │ ├── pipeline-details-dialog.tsx
│ │ ├── pipeline-stages.tsx
│ │ └── styles.module.scss
│ ├── processing-service-details
│ │ ├── processing-service-details-dialog.tsx
│ │ ├── processing-service-pipelines.tsx
│ │ └── styles.module.scss
│ ├── project-details
│ │ ├── delete-project-dialog.tsx
│ │ ├── new-project-dialog.tsx
│ │ ├── project-details-form.tsx
│ │ └── styles.module.scss
│ ├── project
│ │ ├── algorithms
│ │ │ ├── algorithms-columns.tsx
│ │ │ └── algorithms.tsx
│ │ ├── collections
│ │ │ ├── collection-actions.tsx
│ │ │ ├── collection-columns.tsx
│ │ │ ├── collections.tsx
│ │ │ └── constants.tsx
│ │ ├── entities
│ │ │ ├── delete-entity-dialog.tsx
│ │ │ ├── details-form
│ │ │ │ ├── collection-details-form.tsx
│ │ │ │ ├── constants.ts
│ │ │ │ ├── entity-details-form.tsx
│ │ │ │ ├── export-details-form.tsx
│ │ │ │ ├── processing-service-details-form.tsx
│ │ │ │ ├── storage-details-form.tsx
│ │ │ │ └── types.ts
│ │ │ ├── devices.tsx
│ │ │ ├── entities-columns.tsx
│ │ │ ├── entities-picker.tsx
│ │ │ ├── entities.tsx
│ │ │ ├── entity-details-dialog.tsx
│ │ │ ├── new-entity-dialog.tsx
│ │ │ ├── sites.tsx
│ │ │ └── styles.module.scss
│ │ ├── exports
│ │ │ ├── exports-columns.tsx
│ │ │ └── exports.tsx
│ │ ├── general
│ │ │ └── general.tsx
│ │ ├── pipelines
│ │ │ ├── pipelines-columns.tsx
│ │ │ └── pipelines.tsx
│ │ ├── processing-services
│ │ │ ├── connection-status.tsx
│ │ │ ├── processing-services-actions.tsx
│ │ │ ├── processing-services-columns.tsx
│ │ │ ├── processing-services.module.scss
│ │ │ ├── processing-services.tsx
│ │ │ └── status-info
│ │ │ │ ├── status-info.module.scss
│ │ │ │ ├── status-info.tsx
│ │ │ │ └── types.ts
│ │ ├── project.tsx
│ │ ├── sidebar
│ │ │ ├── sidebar.tsx
│ │ │ └── useSidebarSections.tsx
│ │ ├── storage
│ │ │ ├── connection-status.tsx
│ │ │ ├── status-info
│ │ │ │ ├── status-info.module.scss
│ │ │ │ ├── status-info.tsx
│ │ │ │ └── types.ts
│ │ │ ├── storage-columns.tsx
│ │ │ ├── storage.module.scss
│ │ │ └── storage.tsx
│ │ └── summary
│ │ │ ├── deployments-map.tsx
│ │ │ └── summary.tsx
│ ├── projects
│ │ ├── project-gallery.tsx
│ │ └── projects.tsx
│ ├── session-details
│ │ ├── playback
│ │ │ ├── activity-plot
│ │ │ │ ├── activity-plot.tsx
│ │ │ │ ├── lazy-activity-plot.module.scss
│ │ │ │ ├── lazy-activity-plot.tsx
│ │ │ │ ├── types.ts
│ │ │ │ └── useDynamicPlotWidth.ts
│ │ │ ├── capture-details
│ │ │ │ ├── capture-details.module.scss
│ │ │ │ ├── capture-details.tsx
│ │ │ │ └── capture-job
│ │ │ │ │ ├── capture-job-dialog.tsx
│ │ │ │ │ ├── capture-job.tsx
│ │ │ │ │ └── process-now.tsx
│ │ │ ├── capture-navigation
│ │ │ │ ├── capture-navigation.module.scss
│ │ │ │ └── capture-navigation.tsx
│ │ │ ├── frame
│ │ │ │ ├── frame.module.scss
│ │ │ │ ├── frame.tsx
│ │ │ │ └── types.ts
│ │ │ ├── playback.module.scss
│ │ │ ├── playback.tsx
│ │ │ ├── session-captures-slider
│ │ │ │ └── session-captures-slider.tsx
│ │ │ ├── threshold-slider
│ │ │ │ └── threshold-slider.tsx
│ │ │ ├── useActiveCapture.ts
│ │ │ ├── useActiveOccurrences.ts
│ │ │ └── utils.tsx
│ │ ├── session-details.module.scss
│ │ ├── session-details.tsx
│ │ └── session-info
│ │ │ ├── session-info.module.scss
│ │ │ └── session-info.tsx
│ ├── sessions
│ │ ├── session-columns.tsx
│ │ ├── session-gallery.tsx
│ │ └── sessions.tsx
│ ├── species-details
│ │ ├── species-details.module.scss
│ │ └── species-details.tsx
│ └── species
│ │ ├── species-columns.tsx
│ │ ├── species-gallery.tsx
│ │ └── species.tsx
├── setupTests.ts
└── utils
│ ├── breadcrumbContext.tsx
│ ├── constants.ts
│ ├── cookieConsent
│ ├── constants.ts
│ ├── cookieConsentContext.tsx
│ └── types.ts
│ ├── date
│ ├── getCompactDatespanString
│ │ ├── getCompactDatespanString.test.ts
│ │ └── getCompactDatespanString.ts
│ ├── getCompactTimespanString
│ │ └── getCompactTimespanString.ts
│ ├── getFormatedDateString
│ │ └── getFormatedDateString.ts
│ ├── getFormatedDateTimeString
│ │ └── getFormatedDateTimeString.ts
│ └── getFormatedTimeString
│ │ └── getFormatedTimeString.ts
│ ├── formContext
│ ├── formContext.tsx
│ └── types.ts
│ ├── getAppRoute.ts
│ ├── isEmpty
│ ├── isEmpty.test.ts
│ └── isEmpty.ts
│ ├── language.ts
│ ├── numberFormats
│ ├── index.ts
│ └── numberFormats.test.ts
│ ├── parseServerError
│ ├── parseServerError.test.ts
│ └── parseServerError.ts
│ ├── snakeCaseToSentenceCase.ts
│ ├── testHelpers.tsx
│ ├── useClientSideSort.ts
│ ├── useColumnSettings.tsx
│ ├── useDebounce.ts
│ ├── useFilters.ts
│ ├── useFormError.ts
│ ├── useNavItems.ts
│ ├── usePageBreadcrumb.ts
│ ├── usePageTitle.ts
│ ├── usePagination.ts
│ ├── useSelectedView.ts
│ ├── useSort.ts
│ ├── useSyncSectionStatus.ts
│ ├── useWindowSize.ts
│ ├── user
│ ├── constants.ts
│ ├── types.ts
│ ├── userContext.test.tsx
│ ├── userContext.tsx
│ └── userInfoContext.tsx
│ └── userPreferences
│ ├── constants.ts
│ ├── types.ts
│ └── userPreferencesContext.tsx
├── tailwind.config.js
├── tsconfig.json
├── vite-env.d.ts
├── vite.config.ts
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | .editorconfig
2 | .gitattributes
3 | .github
4 | .gitignore
5 | .gitlab-ci.yml
6 | .idea
7 | .pre-commit-config.yaml
8 | .readthedocs.yml
9 | .travis.yml
10 | .git
11 | ui
12 | ami/media
13 | backups
14 | venv
15 | .venv
16 | .env
17 | .envs
18 | .envs/*
19 | node_modules
20 | data
21 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.{py,rst,ini}]
12 | indent_style = space
13 | indent_size = 4
14 |
15 | [*.{html,css,scss,json,yml,xml}]
16 | indent_style = space
17 | indent_size = 2
18 |
19 | [*.md]
20 | trim_trailing_whitespace = false
21 |
22 | [Makefile]
23 | indent_style = tab
24 |
25 | [default.conf]
26 | indent_style = space
27 | indent_size = 2
28 |
--------------------------------------------------------------------------------
/.envs/.ci/.django:
--------------------------------------------------------------------------------
1 | # General
2 | # ------------------------------------------------------------------------------
3 | USE_DOCKER=yes
4 | DJANGO_SETTINGS_MODULE="config.settings.local"
5 |
6 | # Redis
7 | # ------------------------------------------------------------------------------
8 | REDIS_URL=redis://redis:6379/0
9 |
10 | DJANGO_CSRF_TRUSTED_ORIGINS=http://localhost:3000,http://localhost:4000
11 |
12 | MINIO_ENDPOINT=http://minio:9000
13 | MINIO_ROOT_USER=amistorage
14 | MINIO_ROOT_PASSWORD=amistorage
15 | MINIO_DEFAULT_BUCKET=ami-ci
16 | MINIO_STORAGE_USE_HTTPS=False
17 | MINIO_TEST_BUCKET=ami-test-ci
18 | MINIO_BROWSER_REDIRECT_URL=http://minio:9001
19 |
--------------------------------------------------------------------------------
/.envs/.ci/.postgres:
--------------------------------------------------------------------------------
1 | POSTGRES_HOST=postgres
2 | POSTGRES_PORT=5432
3 | POSTGRES_DB=ami-ci
4 | POSTGRES_USER=4JXkOnTAeDmDyIapSRrGEE
5 | POSTGRES_PASSWORD=d4xojpnJU3OzPQ0apSCLP1oHR1TYvyMzAlF5KpE9HFL6MPlnbDibwI
6 |
--------------------------------------------------------------------------------
/.envs/.local/.postgres:
--------------------------------------------------------------------------------
1 | POSTGRES_HOST=postgres
2 | POSTGRES_PORT=5432
3 | POSTGRES_DB=ami
4 | POSTGRES_USER=xekSryPnqczJXkOnTAeDmDyIapSRrGEE
5 | POSTGRES_PASSWORD=iMRQjJEGflj5xojpnJU3OzPQ0apSCLP1oHR1TYvyMzAlF5KpE9HFL6MPlnbDibwI
6 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "monthly"
7 |
8 | - package-ecosystem: "npm"
9 | directory: "/frontend/"
10 | schedule:
11 | interval: "monthly"
12 |
13 | - package-ecosystem: "docker"
14 | directory: "/frontend/"
15 | schedule:
16 | interval: "monthly"
17 |
18 | - package-ecosystem: "docker"
19 | directory: "/backend/"
20 | schedule:
21 | interval: "monthly"
22 |
23 | - package-ecosystem: "docker"
24 | directory: "/"
25 | schedule:
26 | interval: "monthly"
27 |
28 | - package-ecosystem: "pip"
29 | directory: "/backend/"
30 | schedule:
31 | interval: "monthly"
32 |
--------------------------------------------------------------------------------
/.readthedocs.yml:
--------------------------------------------------------------------------------
1 | # Read the Docs configuration file
2 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
3 |
4 | # Required
5 | version: 2
6 |
7 | # Set the version of Python and other tools you might need
8 | build:
9 | os: ubuntu-22.04
10 | tools:
11 | python: '3.11'
12 |
13 | # Build documentation in the docs/ directory with Sphinx
14 | sphinx:
15 | configuration: docs/conf.py
16 |
17 | # Python requirements required to build your docs
18 | python:
19 | install:
20 | - requirements: requirements/local.txt
21 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "batisteo.vscode-django",
4 | "ms-python.black-formatter",
5 | "dbaeumer.vscode-eslint",
6 | "ms-python.isort",
7 | "esbenp.prettier-vscode",
8 | "ms-python.python",
9 | "foxundermoon.shell-format",
10 | "ms-azuretools.vscode-docker"
11 |
12 | ],
13 | "unwantedRecommendations": [
14 |
15 | ]
16 | }
17 |
--------------------------------------------------------------------------------
/CONTRIBUTORS.txt:
--------------------------------------------------------------------------------
1 | Rolnick Lab
2 |
--------------------------------------------------------------------------------
/ami/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.1.0"
2 | __version_info__ = tuple(int(num) if num.isdigit() else num for num in __version__.replace("-", ".", 1).split("."))
3 |
--------------------------------------------------------------------------------
/ami/base/admin.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/base/admin.py
--------------------------------------------------------------------------------
/ami/base/filters.py:
--------------------------------------------------------------------------------
1 | from django.db.models import F, OrderBy
2 | from rest_framework.filters import OrderingFilter
3 |
4 |
5 | class NullsLastOrderingFilter(OrderingFilter):
6 | def get_ordering(self, request, queryset, view):
7 | values = super().get_ordering(request, queryset, view)
8 | if not values:
9 | return values
10 | return [OrderBy(F(value.lstrip("-")), descending=value.startswith("-"), nulls_last=True) for value in values]
11 |
--------------------------------------------------------------------------------
/ami/conftest.py:
--------------------------------------------------------------------------------
1 | import pytest
2 |
3 | from ami.users.models import User
4 | from ami.users.tests.factories import UserFactory
5 |
6 |
7 | @pytest.fixture(autouse=True)
8 | def media_storage(settings, tmpdir):
9 | settings.MEDIA_ROOT = tmpdir.strpath
10 |
11 |
12 | @pytest.fixture
13 | def user(db) -> User:
14 | return UserFactory()
15 |
--------------------------------------------------------------------------------
/ami/contrib/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | To understand why this file is here, please read:
3 |
4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
5 | """
6 |
--------------------------------------------------------------------------------
/ami/contrib/sites/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | To understand why this file is here, please read:
3 |
4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
5 | """
6 |
--------------------------------------------------------------------------------
/ami/contrib/sites/migrations/0002_alter_domain_unique.py:
--------------------------------------------------------------------------------
1 | import django.contrib.sites.models
2 | from django.db import migrations, models
3 |
4 |
5 | class Migration(migrations.Migration):
6 |
7 | dependencies = [("sites", "0001_initial")]
8 |
9 | operations = [
10 | migrations.AlterField(
11 | model_name="site",
12 | name="domain",
13 | field=models.CharField(
14 | max_length=100,
15 | unique=True,
16 | validators=[django.contrib.sites.models._simple_domain_name_validator],
17 | verbose_name="domain name",
18 | ),
19 | )
20 | ]
21 |
--------------------------------------------------------------------------------
/ami/contrib/sites/migrations/0004_alter_options_ordering_domain.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 3.1.7 on 2021-02-04 14:49
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 |
8 | dependencies = [
9 | ("sites", "0003_set_site_domain_and_name"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name="site",
15 | options={
16 | "ordering": ["domain"],
17 | "verbose_name": "site",
18 | "verbose_name_plural": "sites",
19 | },
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/ami/contrib/sites/migrations/__init__.py:
--------------------------------------------------------------------------------
1 | """
2 | To understand why this file is here, please read:
3 |
4 | http://cookiecutter-django.readthedocs.io/en/latest/faq.html#why-is-there-a-django-contrib-sites-directory-in-cookiecutter-django
5 | """
6 |
--------------------------------------------------------------------------------
/ami/exports/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/exports/__init__.py
--------------------------------------------------------------------------------
/ami/exports/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 |
3 |
4 | class ExportsConfig(AppConfig):
5 | default_auto_field = "django.db.models.BigAutoField"
6 | name = "ami.exports"
7 |
8 | def ready(self):
9 | import ami.exports.signals # noqa: F401
10 |
--------------------------------------------------------------------------------
/ami/exports/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/exports/migrations/__init__.py
--------------------------------------------------------------------------------
/ami/jobs/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/jobs/__init__.py
--------------------------------------------------------------------------------
/ami/jobs/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class JobsConfig(AppConfig):
6 | name = "ami.jobs"
7 | verbose_name = _("Jobs")
8 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0003_job_pipeline.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-02 01:40
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0016_pipeline"),
10 | ("jobs", "0002_alter_job_config"),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name="job",
16 | name="pipeline",
17 | field=models.ForeignKey(
18 | blank=True,
19 | null=True,
20 | on_delete=django.db.models.deletion.SET_NULL,
21 | related_name="jobs",
22 | to="main.pipeline",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0004_job_delay.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-02 02:48
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("jobs", "0003_job_pipeline"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="job",
14 | name="delay",
15 | field=models.IntegerField(
16 | default=0, help_text="Delay before running the job", verbose_name="Delay in seconds"
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0005_alter_job_progress.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-03 02:46
2 |
3 | import ami.jobs.models
4 | from django.db import migrations
5 | import django_pydantic_field.fields
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("jobs", "0004_job_delay"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="job",
16 | name="progress",
17 | field=django_pydantic_field.fields.PydanticSchemaField(
18 | config=None,
19 | default={"stages": [], "summary": {"progress": 0.0, "status": "CREATED"}},
20 | schema=ami.jobs.models.JobProgress,
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0008_alter_job_pipeline.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-10 01:33
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("ml", "0001_initial"),
10 | ("jobs", "0007_alter_job_options_alter_job_progress_and_more"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="job",
16 | name="pipeline",
17 | field=models.ForeignKey(
18 | blank=True,
19 | null=True,
20 | on_delete=django.db.models.deletion.SET_NULL,
21 | related_name="jobs",
22 | to="ml.pipeline",
23 | ),
24 | ),
25 | ]
26 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0009_remove_job_config.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-10 01:52
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("jobs", "0008_alter_job_pipeline"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveField(
13 | model_name="job",
14 | name="config",
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0011_alter_job_limit.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-11-03 23:50
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("jobs", "0010_job_limit_job_shuffle"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="job",
14 | name="limit",
15 | field=models.IntegerField(
16 | blank=True,
17 | default=None,
18 | help_text="Limit the number of images to process",
19 | null=True,
20 | verbose_name="Limit",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0012_alter_job_limit.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-11-11 17:42
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("jobs", "0011_job_job_type_key"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="job",
14 | name="limit",
15 | field=models.IntegerField(
16 | blank=True,
17 | default=None,
18 | help_text="Limit the number of images to process",
19 | null=True,
20 | verbose_name="Limit",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0013_merge_0011_alter_job_limit_0012_alter_job_limit.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-12-17 22:28
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("jobs", "0011_alter_job_limit"),
9 | ("jobs", "0012_alter_job_limit"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0014_alter_job_progress.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-12-17 20:13
2 |
3 | import ami.jobs.models
4 | from django.db import migrations
5 | import django_pydantic_field.fields
6 |
7 |
8 | class Migration(migrations.Migration):
9 | dependencies = [
10 | ("jobs", "0013_add_job_logs"),
11 | ]
12 |
13 | operations = [
14 | migrations.AlterField(
15 | model_name="job",
16 | name="progress",
17 | field=django_pydantic_field.fields.PydanticSchemaField(
18 | config=None,
19 | default={"errors": [], "logs": [], "stages": [], "summary": {"progress": 0.0, "status": "CREATED"}},
20 | schema=ami.jobs.models.JobProgress,
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/0015_merge_20250117_2100.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-01-17 21:00
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("jobs", "0013_merge_0011_alter_job_limit_0012_alter_job_limit"),
9 | ("jobs", "0014_alter_job_progress"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/jobs/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/jobs/migrations/__init__.py
--------------------------------------------------------------------------------
/ami/labelstudio/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/labelstudio/__init__.py
--------------------------------------------------------------------------------
/ami/labelstudio/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class LabelStudioConfig(AppConfig):
6 | name = "ami.labelstudio"
7 | verbose_name = _("Label Studio Integration")
8 |
--------------------------------------------------------------------------------
/ami/labelstudio/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/labelstudio/migrations/__init__.py
--------------------------------------------------------------------------------
/ami/main/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/main/__init__.py
--------------------------------------------------------------------------------
/ami/main/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.db.models.signals import post_migrate
3 | from django.utils.translation import gettext_lazy as _
4 |
5 |
6 | class MainConfig(AppConfig):
7 | name = "ami.main"
8 | verbose_name = _("Main")
9 |
10 | def ready(self):
11 | import ami.main.signals # noqa: F401
12 | from ami.tests.fixtures.signals import initialize_demo_project
13 | from ami.users.signals import create_roles
14 |
15 | post_migrate.connect(initialize_demo_project, sender=self)
16 | post_migrate.connect(create_roles, sender=self)
17 |
--------------------------------------------------------------------------------
/ami/main/management/commands/import_source_images.py:
--------------------------------------------------------------------------------
1 | from django.core.management.base import BaseCommand, CommandError # noqa
2 |
3 | from ...models import Deployment
4 |
5 |
6 | class Command(BaseCommand):
7 | r"""Import source images from s3 bucket configured for Deployment."""
8 |
9 | help = "Import trap data from a Deployment's data source"
10 |
11 | def add_arguments(self, parser):
12 | parser.add_argument("deployment_id", type=int)
13 |
14 | def handle(self, *args, **options):
15 | deployment_id = options["deployment_id"]
16 | deployment = Deployment.objects.get(id=deployment_id)
17 | created = deployment.import_captures()
18 | msg = f"Imported {len(created)} source images for {deployment}"
19 | self.stdout.write(self.style.SUCCESS(msg))
20 |
--------------------------------------------------------------------------------
/ami/main/management/main.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/main/management/main.py
--------------------------------------------------------------------------------
/ami/main/migrations/0002_remove_identification_unique_primary_identification_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.5 on 2023-09-21 20:05
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveConstraint(
13 | model_name="identification",
14 | name="unique_primary_identification",
15 | ),
16 | migrations.AddConstraint(
17 | model_name="identification",
18 | constraint=models.UniqueConstraint(
19 | condition=models.Q(("primary", True)), fields=("occurrence",), name="unique_primary_identification"
20 | ),
21 | ),
22 | ]
23 |
--------------------------------------------------------------------------------
/ami/main/migrations/0004_search_extensions.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.5 on 2023-09-22 18:06
2 |
3 | from django.db import migrations
4 | from django.contrib.postgres.operations import TrigramExtension
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0003_alter_identification_options_and_more"),
10 | ]
11 |
12 | operations = [TrigramExtension()]
13 |
--------------------------------------------------------------------------------
/ami/main/migrations/0005_taxon_display_name.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.5 on 2023-09-22 21:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0004_search_extensions"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="taxon",
14 | name="display_name",
15 | field=models.CharField(
16 | blank=True, max_length=255, null=True, unique=True, verbose_name="Cached display name"
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/ami/main/migrations/0006_merge_20230926_2353.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-09-26 23:53
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0004_identification_agreed_with_identification_and_more"),
9 | ("main", "0005_taxon_display_name"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/main/migrations/0009_alter_sourceimageupload_source_image.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-10-16 23:43
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0008_rename_capture_sourceimageupload_source_image_and_more"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="sourceimageupload",
15 | name="source_image",
16 | field=models.OneToOneField(
17 | blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to="main.sourceimage"
18 | ),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/ami/main/migrations/0013_sourceimage_detections_count.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-10-31 17:17
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0012_alter_taxon_rank"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="sourceimage",
15 | name="detections_count",
16 | field=models.IntegerField(blank=True, null=True),
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/ami/main/migrations/0015_delete_job.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-02 00:50
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0014_job_source_image_collection_job_source_image_single_and_more"),
9 | ]
10 |
11 | operations = [
12 | migrations.DeleteModel(
13 | name="Job",
14 | ),
15 | ]
16 |
--------------------------------------------------------------------------------
/ami/main/migrations/0017_alter_site_options_site_project.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-23 02:04
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0016_pipeline"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterModelOptions(
14 | name="site",
15 | options={"verbose_name": "Research Site"},
16 | ),
17 | migrations.AddField(
18 | model_name="site",
19 | name="project",
20 | field=models.ForeignKey(
21 | null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="sites", to="main.project"
22 | ),
23 | ),
24 | ]
25 |
--------------------------------------------------------------------------------
/ami/main/migrations/0017_remove_pipeline_algorithms.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-10 01:33
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0016_pipeline"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveField(
13 | model_name="pipeline",
14 | name="algorithms",
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/ami/main/migrations/0018_delete_existing_pipeline_algorithms.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-10 01:33
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("ml", "0001_initial"),
10 | ("main", "0017_remove_pipeline_algorithms"),
11 | ("jobs", "0008_alter_job_pipeline"),
12 | ]
13 |
14 | operations = [
15 | migrations.RunSQL("", ""),
16 | ]
17 |
--------------------------------------------------------------------------------
/ami/main/migrations/0018_device_project.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-23 02:09
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0017_alter_site_options_site_project"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="device",
15 | name="project",
16 | field=models.ForeignKey(
17 | null=True, on_delete=django.db.models.deletion.SET_NULL, related_name="devices", to="main.project"
18 | ),
19 | ),
20 | ]
21 |
--------------------------------------------------------------------------------
/ami/main/migrations/0019_s3storagesource_project.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-23 02:21
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0018_device_project"),
10 | ]
11 |
12 | operations = [
13 | migrations.AddField(
14 | model_name="s3storagesource",
15 | name="project",
16 | field=models.ForeignKey(
17 | null=True,
18 | on_delete=django.db.models.deletion.SET_NULL,
19 | related_name="storage_sources",
20 | to="main.project",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/main/migrations/0020_detection_detection_algorithm.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-10 01:38
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("ml", "0001_initial"),
10 | ("main", "0019_delete_pipeline_remove_detection_detection_algorithm_and_more"),
11 | ]
12 |
13 | operations = [
14 | migrations.AddField(
15 | model_name="detection",
16 | name="detection_algorithm",
17 | field=models.ForeignKey(
18 | blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to="ml.algorithm"
19 | ),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/ami/main/migrations/0021_merge_20231123_0307.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-23 03:07
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0019_s3storagesource_project"),
9 | ("main", "0020_detection_detection_algorithm"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/main/migrations/0023_taxon_main_taxon_orderin_4ffb7b_idx.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-29 23:54
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0022_alter_sourceimagecollection_method_alter_taxon_rank"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddIndex(
13 | model_name="taxon",
14 | index=models.Index(fields=["ordering", "name"], name="main_taxon_orderin_4ffb7b_idx"),
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/ami/main/migrations/0026_occurrence_determination_score.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-12-02 01:08
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0025_update_deployment_aggregates"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="occurrence",
14 | name="determination_score",
15 | field=models.FloatField(blank=True, null=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/ami/main/migrations/0027_update_occurrence_scores.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-12-02 01:08
2 |
3 | from django.db import migrations
4 |
5 |
6 | # Call save on all occurrences to update their scores
7 | def update_occurrence_scores(apps, schema_editor):
8 | Occurrence = apps.get_model("main", "Occurrence")
9 | # from ami.main.models import Occurrence
10 |
11 | for occurrence in Occurrence.objects.all():
12 | occurrence.save()
13 |
14 |
15 | class Migration(migrations.Migration):
16 | dependencies = [
17 | ("main", "0026_occurrence_determination_score"),
18 | ]
19 |
20 | operations = [
21 | migrations.RunPython(update_occurrence_scores, migrations.RunPython.noop),
22 | ]
23 |
--------------------------------------------------------------------------------
/ami/main/migrations/0030_identification_comment.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2024-04-16 18:56
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0029_alter_deployment_device_and_more"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="identification",
14 | name="comment",
15 | field=models.TextField(blank=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/ami/main/migrations/0032_alter_s3storagesource_public_base_url.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2024-05-07 20:03
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0031_s3storagesource_use_presigned_urls_and_more"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="s3storagesource",
14 | name="public_base_url",
15 | field=models.CharField(blank=True, max_length=255, null=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/ami/main/migrations/0033_remove_s3storagesource_use_presigned_urls_and_more.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-07-09 19:17
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0032_alter_s3storagesource_public_base_url"),
9 | ]
10 |
11 | operations = [
12 | migrations.RemoveField(
13 | model_name="s3storagesource",
14 | name="use_presigned_urls",
15 | ),
16 | migrations.AlterField(
17 | model_name="sourceimage",
18 | name="public_base_url",
19 | field=models.CharField(blank=True, max_length=255, null=True),
20 | ),
21 | ]
22 |
--------------------------------------------------------------------------------
/ami/main/migrations/0044_alter_project_options_squashed_0047_alter_project_options.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-01-27 00:10
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | replaces = [
8 | ("main", "0044_alter_project_options"),
9 | ("main", "0045_alter_project_options"),
10 | ("main", "0046_alter_project_options"),
11 | ("main", "0047_alter_project_options"),
12 | ]
13 |
14 | dependencies = [
15 | ("main", "0039_project_users_squashed_0043_rename_users_project_members"),
16 | ]
17 |
18 | operations = [
19 | migrations.AlterModelOptions(
20 | name="project",
21 | options={"ordering": ["-priority", "created_at"]},
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/main/migrations/0044_merge_20250124_2333.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-01-24 23:33
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0039_project_users_squashed_0043_rename_users_project_members"),
9 | ("main", "0040_alter_classification_logits_and_more"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/main/migrations/0045_alter_classification_algorithm.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-02-16 16:00
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0044_merge_20250124_2333"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="classification",
15 | name="algorithm",
16 | field=models.ForeignKey(
17 | null=True,
18 | on_delete=django.db.models.deletion.SET_NULL,
19 | related_name="classifications",
20 | to="ml.algorithm",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/main/migrations/0052_merge_20250207_1012.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-02-07 10:12
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0044_merge_20250124_2333"),
9 | ("main", "0048_alter_project_options_squashed_0051_alter_project_options"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/main/migrations/0053_alter_classification_algorithm.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-02-10 06:31
2 |
3 | from django.db import migrations, models
4 | import django.db.models.deletion
5 |
6 |
7 | class Migration(migrations.Migration):
8 | dependencies = [
9 | ("main", "0052_merge_20250207_1012"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="classification",
15 | name="algorithm",
16 | field=models.ForeignKey(
17 | null=True,
18 | on_delete=django.db.models.deletion.SET_NULL,
19 | related_name="classifications",
20 | to="ml.algorithm",
21 | ),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/main/migrations/0056_merge_20250218_1405.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-02-18 14:05
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0045_alter_classification_algorithm"),
9 | ("main", "0054_alter_project_options_squashed_0055_alter_project_options"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/main/migrations/0057_merge_20250220_0022.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-02-20 00:22
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0046_add_taxon_common_name_placeholder"),
9 | ("main", "0056_merge_20250218_1405"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/main/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/main/migrations/__init__.py
--------------------------------------------------------------------------------
/ami/ml/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/ml/__init__.py
--------------------------------------------------------------------------------
/ami/ml/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class MLConfig(AppConfig):
6 | name = "ami.ml"
7 | verbose_name = _("Machine Learning")
8 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0002_pipeline_endpoint_url.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-22 02:37
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("ml", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="pipeline",
14 | name="endpoint_url",
15 | field=models.URLField(blank=True, null=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0003_pipeline_slug.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-22 03:02
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("ml", "0002_pipeline_endpoint_url"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="pipeline",
14 | name="slug",
15 | field=models.SlugField(max_length=255, null=True, unique=True),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0004_populate_pipeline_slugs.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-22 03:03
2 |
3 | from django.db import migrations
4 |
5 | from django.utils.text import slugify
6 |
7 |
8 | def populate_pipeline_slugs(apps, schema_editor):
9 | Pipeline = apps.get_model("ml", "Pipeline")
10 | for pipeline in Pipeline.objects.all():
11 | pipeline.slug = slugify(pipeline.name)
12 | pipeline.save()
13 |
14 |
15 | class Migration(migrations.Migration):
16 | dependencies = [
17 | ("ml", "0003_pipeline_slug"),
18 | ]
19 |
20 | operations = [
21 | migrations.RunPython(populate_pipeline_slugs, migrations.RunPython.noop),
22 | ]
23 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0005_alter_pipeline_slug.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-11-22 03:07
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("ml", "0004_populate_pipeline_slugs"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="pipeline",
14 | name="slug",
15 | field=models.SlugField(default="2023-11-22 03:07:46.287011", max_length=255, unique=True),
16 | preserve_default=False,
17 | ),
18 | ]
19 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0006_alter_pipeline_endpoint_url_alter_pipeline_projects.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-10-11 15:19
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("main", "0038_alter_detection_path_alter_sourceimage_event_and_more"),
9 | ("ml", "0005_alter_pipeline_slug"),
10 | ]
11 |
12 | operations = [
13 | migrations.AlterField(
14 | model_name="pipeline",
15 | name="endpoint_url",
16 | field=models.CharField(blank=True, max_length=1024, null=True),
17 | ),
18 | migrations.AlterField(
19 | model_name="pipeline",
20 | name="projects",
21 | field=models.ManyToManyField(blank=True, related_name="pipelines", to="main.project"),
22 | ),
23 | ]
24 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0010_alter_algorithm_version.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-12-05 01:49
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("ml", "0009_algorithm_task_type"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterField(
13 | model_name="algorithm",
14 | name="version",
15 | field=models.IntegerField(
16 | default=1, help_text="An internal, sortable and incrementable version number for the model."
17 | ),
18 | ),
19 | ]
20 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0012_alter_algorithm_unique_together.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-12-05 04:20
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("ml", "0011_alter_algorithm_task_type_alter_algorithm_url_and_more"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterUniqueTogether(
13 | name="algorithm",
14 | unique_together=set(),
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0016_merge_20250117_2101.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-01-17 21:01
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("ml", "0007_add_processing_service"),
9 | ("ml", "0015_update_existing_intermediate_classifications"),
10 | ]
11 |
12 | operations = []
13 |
--------------------------------------------------------------------------------
/ami/ml/migrations/0017_alter_algorithm_unique_together.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2025-02-12 19:12
2 |
3 | from django.db import migrations
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("ml", "0016_merge_20250117_2101"),
9 | ]
10 |
11 | operations = [
12 | migrations.AlterUniqueTogether(
13 | name="algorithm",
14 | unique_together={("name", "version")},
15 | ),
16 | ]
17 |
--------------------------------------------------------------------------------
/ami/ml/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/ml/migrations/__init__.py
--------------------------------------------------------------------------------
/ami/ml/models/__init__.py:
--------------------------------------------------------------------------------
1 | from ami.ml.models.algorithm import Algorithm, AlgorithmCategoryMap
2 | from ami.ml.models.pipeline import Pipeline
3 | from ami.ml.models.processing_service import ProcessingService
4 | from ami.ml.models.project_pipeline_config import ProjectPipelineConfig
5 |
6 | __all__ = [
7 | "Algorithm",
8 | "AlgorithmCategoryMap",
9 | "Pipeline",
10 | "ProcessingService",
11 | "ProjectPipelineConfig",
12 | ]
13 |
--------------------------------------------------------------------------------
/ami/static/css/project.css:
--------------------------------------------------------------------------------
1 | /* These styles are generated from project.scss. */
2 |
3 | .alert-debug {
4 | color: black;
5 | background-color: white;
6 | border-color: #d6e9c6;
7 | }
8 |
9 | .alert-error {
10 | color: #b94a48;
11 | background-color: #f2dede;
12 | border-color: #eed3d7;
13 | }
14 |
--------------------------------------------------------------------------------
/ami/static/fonts/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/static/fonts/.gitkeep
--------------------------------------------------------------------------------
/ami/static/images/favicons/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/static/images/favicons/favicon.ico
--------------------------------------------------------------------------------
/ami/static/js/project.js:
--------------------------------------------------------------------------------
1 | /* Project specific Javascript goes here. */
2 |
--------------------------------------------------------------------------------
/ami/templates/403.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}
4 | Forbidden (403)
5 | {% endblock title %}
6 | {% block content %}
7 |
Forbidden (403)
8 |
9 | {% if exception %}
10 | {{ exception }}
11 | {% else %}
12 | You're not allowed to access this page.
13 | {% endif %}
14 |
15 | {% endblock content %}
16 |
--------------------------------------------------------------------------------
/ami/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}
4 | Page not found
5 | {% endblock title %}
6 | {% block content %}
7 | Page not found
8 |
9 | {% if exception %}
10 | {{ exception }}
11 | {% else %}
12 | This is not the page you were looking for.
13 | {% endif %}
14 |
15 | {% endblock content %}
16 |
--------------------------------------------------------------------------------
/ami/templates/500.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}
4 | Server Error
5 | {% endblock title %}
6 | {% block content %}
7 | Ooops!!! 500
8 | Looks like something went wrong!
9 |
10 | We track these errors automatically, but if the problem persists feel free to contact us. In the meantime, try refreshing.
11 |
12 | {% endblock content %}
13 |
--------------------------------------------------------------------------------
/ami/templates/account/account_inactive.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}
6 | {% translate "Account Inactive" %}
7 | {% endblock head_title %}
8 | {% block inner %}
9 | {% translate "Account Inactive" %}
10 | {% translate "This account is inactive." %}
11 | {% endblock inner %}
12 |
--------------------------------------------------------------------------------
/ami/templates/account/base.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}
4 | {% block head_title %}
5 | {% endblock head_title %}
6 | {% endblock title %}
7 | {% block content %}
8 |
9 |
10 | {% block inner %}
11 | {% endblock inner %}
12 |
13 |
14 | {% endblock content %}
15 |
--------------------------------------------------------------------------------
/ami/templates/account/logout.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}
6 | {% translate "Sign Out" %}
7 | {% endblock head_title %}
8 | {% block inner %}
9 | {% translate "Sign Out" %}
10 | {% translate "Are you sure you want to sign out?" %}
11 |
20 | {% endblock inner %}
21 |
--------------------------------------------------------------------------------
/ami/templates/account/password_change.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 |
6 | {% block head_title %}
7 | {% translate "Change Password" %}
8 | {% endblock head_title %}
9 | {% block inner %}
10 | {% translate "Change Password" %}
11 |
18 | {% endblock inner %}
19 |
--------------------------------------------------------------------------------
/ami/templates/account/password_reset_done.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load account %}
5 |
6 | {% block head_title %}
7 | {% translate "Password Reset" %}
8 | {% endblock head_title %}
9 | {% block inner %}
10 | {% translate "Password Reset" %}
11 | {% if user.is_authenticated %}
12 | {% include "account/snippets/already_logged_in.html" %}
13 | {% endif %}
14 |
15 | {% blocktranslate %}We have sent you an e-mail. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}
16 |
17 | {% endblock inner %}
18 |
--------------------------------------------------------------------------------
/ami/templates/account/password_reset_from_key_done.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}
6 | {% translate "Change Password" %}
7 | {% endblock head_title %}
8 | {% block inner %}
9 | {% translate "Change Password" %}
10 | {% translate "Your password is now changed." %}
11 | {% endblock inner %}
12 |
--------------------------------------------------------------------------------
/ami/templates/account/password_set.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 | {% load crispy_forms_tags %}
5 |
6 | {% block head_title %}
7 | {% translate "Set Password" %}
8 | {% endblock head_title %}
9 | {% block inner %}
10 | {% translate "Set Password" %}
11 |
21 | {% endblock inner %}
22 |
--------------------------------------------------------------------------------
/ami/templates/account/signup_closed.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}
6 | {% translate "Sign Up Closed" %}
7 | {% endblock head_title %}
8 | {% block inner %}
9 | {% translate "Sign Up Closed" %}
10 | {% translate "We are sorry, but the sign up is currently closed." %}
11 | {% endblock inner %}
12 |
--------------------------------------------------------------------------------
/ami/templates/account/verification_sent.html:
--------------------------------------------------------------------------------
1 | {% extends "account/base.html" %}
2 |
3 | {% load i18n %}
4 |
5 | {% block head_title %}
6 | {% translate "Verify Your E-mail Address" %}
7 | {% endblock head_title %}
8 | {% block inner %}
9 | {% translate "Verify Your E-mail Address" %}
10 |
11 | {% blocktranslate %}We have sent an e-mail to you for verification. Follow the link provided to finalize the signup process. Please contact us if you do not receive it within a few minutes.{% endblocktranslate %}
12 |
13 | {% endblock inner %}
14 |
--------------------------------------------------------------------------------
/ami/templates/labelstudio/binary_classification.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Deployment:
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/ami/templates/labelstudio/initial_object_detection.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/ami/templates/labelstudio/species_classification.xml:
--------------------------------------------------------------------------------
1 |
2 | Deployment:
3 |
4 |
5 |
6 | {{ label_config.taxonomy_choices_xml|safe }}
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ami/templates/pages/about.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
--------------------------------------------------------------------------------
/ami/templates/pages/home.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
--------------------------------------------------------------------------------
/ami/templates/users/user_form.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% load crispy_forms_tags %}
4 |
5 | {% block title %}
6 | {{ user.name }}
7 | {% endblock title %}
8 | {% block content %}
9 | {{ user.name }}
10 |
21 | {% endblock content %}
22 |
--------------------------------------------------------------------------------
/ami/tests/fixtures/signals.py:
--------------------------------------------------------------------------------
1 | import logging
2 |
3 | from django.conf import settings
4 |
5 | from ami.main.models import Project
6 |
7 | from .main import create_complete_test_project, create_local_admin_user, update_site_settings
8 |
9 | logger = logging.getLogger(__name__)
10 |
11 |
12 | def initialize_demo_project(sender, **kwargs):
13 | """
14 | Signal handler to create a demo project after `migrate` is run.
15 | """
16 | if not Project.objects.exists():
17 | update_site_settings(domain=settings.EXTERNAL_HOSTNAME)
18 | create_complete_test_project()
19 | create_local_admin_user()
20 |
--------------------------------------------------------------------------------
/ami/tests/test_doctests.py:
--------------------------------------------------------------------------------
1 | import doctest
2 | import pkgutil
3 |
4 | import ami as root_package
5 |
6 |
7 | def load_tests(loader, tests, ignore):
8 | modules = pkgutil.walk_packages(root_package.__path__, root_package.__name__ + ".")
9 | for _, module_name, _ in modules:
10 | try:
11 | suite = doctest.DocTestSuite(module_name)
12 | except ValueError:
13 | # Presumably a "no docstrings" error. That's OK.
14 | pass
15 | except ModuleNotFoundError:
16 | pass
17 | else:
18 | tests.addTests(suite)
19 | return tests
20 |
--------------------------------------------------------------------------------
/ami/users/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/users/__init__.py
--------------------------------------------------------------------------------
/ami/users/apps.py:
--------------------------------------------------------------------------------
1 | from django.apps import AppConfig
2 | from django.utils.translation import gettext_lazy as _
3 |
4 |
5 | class UsersConfig(AppConfig):
6 | name = "ami.users"
7 | verbose_name = _("Users")
8 |
9 | def ready(self):
10 | try:
11 | import ami.users.signals # noqa: F401
12 | except ImportError:
13 | pass
14 |
--------------------------------------------------------------------------------
/ami/users/context_processors.py:
--------------------------------------------------------------------------------
1 | from django.conf import settings
2 |
3 |
4 | def allauth_settings(request):
5 | """Expose some settings from django-allauth in templates."""
6 | return {
7 | "ACCOUNT_ALLOW_REGISTRATION": settings.ACCOUNT_ALLOW_REGISTRATION,
8 | }
9 |
--------------------------------------------------------------------------------
/ami/users/migrations/0002_user_image.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.2 on 2023-09-08 02:30
2 |
3 | from django.db import migrations, models
4 |
5 |
6 | class Migration(migrations.Migration):
7 | dependencies = [
8 | ("users", "0001_initial"),
9 | ]
10 |
11 | operations = [
12 | migrations.AddField(
13 | model_name="user",
14 | name="image",
15 | field=models.ImageField(blank=True, null=True, upload_to="users"),
16 | ),
17 | ]
18 |
--------------------------------------------------------------------------------
/ami/users/migrations/0003_lowercase_existing_emails.py:
--------------------------------------------------------------------------------
1 | # Generated by Django 4.2.10 on 2024-09-19 17:54
2 | from django.db import migrations
3 |
4 |
5 | def lowercase_emails(apps, schema_editor):
6 | User = apps.get_model("users", "User")
7 | for user in User.objects.all():
8 | lowercase_email = user.email.lower()
9 | if user.email != lowercase_email:
10 | user.email = lowercase_email
11 | user.save(update_fields=["email"])
12 |
13 |
14 | class Migration(migrations.Migration):
15 |
16 | dependencies = [
17 | ("users", "0002_user_image"),
18 | ]
19 |
20 | operations = [
21 | migrations.RunPython(lowercase_emails),
22 | ]
23 |
--------------------------------------------------------------------------------
/ami/users/migrations/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/users/migrations/__init__.py
--------------------------------------------------------------------------------
/ami/users/tasks.py:
--------------------------------------------------------------------------------
1 | from django.contrib.auth import get_user_model
2 |
3 | from config import celery_app
4 |
5 | User = get_user_model()
6 |
7 |
8 | @celery_app.task()
9 | def get_users_count():
10 | """A pointless Celery task to demonstrate usage."""
11 | return User.objects.count()
12 |
--------------------------------------------------------------------------------
/ami/users/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ami/users/tests/__init__.py
--------------------------------------------------------------------------------
/ami/users/tests/test_drf_urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import resolve, reverse
2 |
3 | from ami.users.models import User
4 |
5 |
6 | def test_user_detail(user: User):
7 | assert reverse("api:user-detail", kwargs={"id": user.pk}) == f"/api/v2/users/{user.pk}/"
8 | assert resolve(f"/api/v2/users/{user.pk}/").view_name == "api:user-detail"
9 |
10 |
11 | def test_user_list():
12 | assert reverse("api:user-list") == "/api/v2/users/"
13 | assert resolve("/api/v2/users/").view_name == "api:user-list"
14 |
15 |
16 | def test_user_me():
17 | assert reverse("api:user-me") == "/api/v2/users/me/"
18 | assert resolve("/api/v2/users/me/").view_name == "api:user-me"
19 |
--------------------------------------------------------------------------------
/ami/users/tests/test_drf_views.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from djoser.views import UserViewSet
3 | from rest_framework.test import APIRequestFactory
4 |
5 | from ami.users.models import User
6 |
7 |
8 | # Djoser has its own tests for the UserViewSet, so we only need to test our own code.
9 | class TestUserViewSet:
10 | @pytest.fixture
11 | def api_rf(self) -> APIRequestFactory:
12 | return APIRequestFactory()
13 |
14 | def test_get_queryset(self, user: User, api_rf: APIRequestFactory):
15 | view = UserViewSet()
16 | view.action = "list"
17 | request = api_rf.get("/fake-url/")
18 | request.user = user
19 |
20 | view.request = request
21 |
22 | assert user in view.get_queryset()
23 |
--------------------------------------------------------------------------------
/ami/users/tests/test_models.py:
--------------------------------------------------------------------------------
1 | from ami.users.models import User
2 |
3 |
4 | def test_user_get_absolute_url(user: User):
5 | assert user.get_absolute_url() == f"/api/v2/users/{user.pk}/"
6 |
--------------------------------------------------------------------------------
/ami/users/tests/test_swagger.py:
--------------------------------------------------------------------------------
1 | import pytest # noqa
2 | from django.urls import reverse
3 |
4 |
5 | def test_swagger_accessible_by_admin(admin_client):
6 | url = reverse("api-docs")
7 | response = admin_client.get(url)
8 | assert response.status_code == 200
9 |
10 |
11 | # @TODO re-enable once we configure authentication
12 | # @pytest.mark.django_db
13 | # def test_swagger_ui_not_accessible_by_normal_user(client):
14 | # url = reverse("api-docs")
15 | # response = client.get(url)
16 | # assert response.status_code == 403
17 |
18 |
19 | def test_api_schema_generated_successfully(admin_client):
20 | url = reverse("api-schema")
21 | response = admin_client.get(url)
22 | assert response.status_code == 200
23 |
--------------------------------------------------------------------------------
/ami/users/tests/test_tasks.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from celery.result import EagerResult
3 |
4 | from ami.users.tasks import get_users_count
5 | from ami.users.tests.factories import UserFactory
6 |
7 | pytestmark = pytest.mark.django_db
8 |
9 |
10 | def test_user_count(settings):
11 | """A basic test to execute the get_users_count Celery task."""
12 | UserFactory.create_batch(3)
13 | settings.CELERY_TASK_ALWAYS_EAGER = True
14 | task_result = get_users_count.delay()
15 | assert isinstance(task_result, EagerResult)
16 | assert task_result.result == 3
17 |
--------------------------------------------------------------------------------
/ami/users/urls.py:
--------------------------------------------------------------------------------
1 | from django.urls import path
2 |
3 | from ami.users.views import user_detail_view, user_redirect_view, user_update_view
4 |
5 | app_name = "users"
6 | urlpatterns = [
7 | path("~redirect/", view=user_redirect_view, name="redirect"),
8 | path("~update/", view=user_update_view, name="update"),
9 | path("/", view=user_detail_view, name="detail"),
10 | ]
11 |
--------------------------------------------------------------------------------
/ami/utils/__init__.py:
--------------------------------------------------------------------------------
1 | from . import dates, s3
2 |
3 | __all__ = ["dates", "s3"]
4 |
--------------------------------------------------------------------------------
/ami/utils/fields.py:
--------------------------------------------------------------------------------
1 | from django.forms import BooleanField
2 | from rest_framework.request import Request
3 |
4 |
5 | def url_boolean_param(request: Request, param: str, default: bool = False) -> bool:
6 | """
7 | The presence of the parameter in the query string with no value indicates True.
8 |
9 | If the parameter is present and has a value, it is parsed as a BooleanField.
10 | Which means that "true", "True", "1", and "yes" are all True, and everything
11 | else is False.
12 | """
13 | try:
14 | value = request.query_params[param]
15 | except KeyError:
16 | value = False
17 | else:
18 | if value == "":
19 | value = True
20 | else:
21 | value = BooleanField(required=False).clean(value)
22 |
23 | return value or default
24 |
--------------------------------------------------------------------------------
/ami/utils/storages.py:
--------------------------------------------------------------------------------
1 | from dataclasses import dataclass
2 |
3 | from storages.backends.s3boto3 import S3Boto3Storage
4 |
5 | IMAGE_FILE_EXTENSIONS = ["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "ico", "tiff", "tif"]
6 |
7 |
8 | class StaticRootS3Boto3Storage(S3Boto3Storage):
9 | location = "static"
10 | default_acl = "public-read"
11 |
12 |
13 | class MediaRootS3Boto3Storage(S3Boto3Storage):
14 | location = "media"
15 | file_overwrite = False
16 |
17 |
18 | @dataclass
19 | class ConnectionTestResult:
20 | connection_successful: bool
21 | prefix_exists: bool
22 | latency: float
23 | total_time: float
24 | error_code: str | None
25 | error_message: str | None
26 | files_checked: int
27 | first_file_found: str | None
28 | full_uri: str | None
29 |
--------------------------------------------------------------------------------
/compose/local/django/celery/beat/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o nounset
5 |
6 |
7 | rm -f './celerybeat.pid'
8 | exec watchfiles --filter python celery.__main__.main --args '-A config.celery_app beat -l INFO'
9 |
--------------------------------------------------------------------------------
/compose/local/django/celery/flower/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o nounset
5 |
6 | exec watchfiles --filter python celery.__main__.main \
7 | --args \
8 | "-A config.celery_app -b \"${CELERY_BROKER_URL}\" flower --db=\"${CELERY_FLOWER_DB-/data/flower.db}\""
9 |
--------------------------------------------------------------------------------
/compose/local/django/celery/worker/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o nounset
5 |
6 |
7 | exec watchfiles --filter python celery.__main__.main --args '-A config.celery_app worker -l INFO'
8 |
--------------------------------------------------------------------------------
/compose/local/django/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | set -o nounset
6 |
7 |
8 | python manage.py migrate
9 | exec uvicorn config.asgi:application --host 0.0.0.0 --reload --reload-include '*.html'
10 |
--------------------------------------------------------------------------------
/compose/local/docs/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | set -o nounset
6 |
7 | exec make livehtml
8 |
--------------------------------------------------------------------------------
/compose/local/minio/init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | # Create a default bucket
4 | # /usr/bin/mc set alias minio "${MINIO_ENDPOINT}" "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}"
5 | /usr/bin/mc config host add local "${MINIO_ENDPOINT}" "${MINIO_ROOT_USER}" "${MINIO_ROOT_PASSWORD}"
6 | /usr/bin/mc mb local/"${MINIO_DEFAULT_BUCKET}" --ignore-existing
7 | /usr/bin/mc mb local/"${MINIO_TEST_BUCKET}" --ignore-existing
8 |
9 | # Give it public read access
10 | /usr/bin/mc anonymous set public local/"${MINIO_DEFAULT_BUCKET}"
11 | /usr/bin/mc anonymous set public local/"${MINIO_TEST_BUCKET}"
12 |
13 | exit 0
14 |
--------------------------------------------------------------------------------
/compose/local/minio/nginx.conf:
--------------------------------------------------------------------------------
1 | worker_processes auto;
2 |
3 | events {
4 | worker_connections 1024;
5 | }
6 |
7 | http {
8 | server {
9 | listen 9000 default;
10 | server_name _;
11 |
12 | location / {
13 | # Set Host header to mitigate MinIO's signature discrepancy issue
14 | # e.g. "SignatureDoesNotMatch: The request signature we calculated
15 | # does not match the signature you provided. Check your key and signing method."
16 | proxy_set_header Host minio:9000;
17 | proxy_pass http://minio:9000;
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/compose/local/postgres/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM postgres:16
2 | # FROM esgn/pgtuned:latest
3 |
4 | COPY ./compose/local/postgres/maintenance /usr/local/bin/maintenance
5 | RUN chmod +x /usr/local/bin/maintenance/*
6 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \
7 | && rmdir /usr/local/bin/maintenance
8 |
--------------------------------------------------------------------------------
/compose/local/postgres/maintenance/_sourced/constants.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | BACKUP_DIR_PATH='/backups'
5 | BACKUP_FILE_PREFIX='backup'
6 |
--------------------------------------------------------------------------------
/compose/local/postgres/maintenance/_sourced/countdown.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | countdown() {
5 | declare desc="A simple countdown. Source: https://superuser.com/a/611582"
6 | local seconds="${1}"
7 | local d=$(($(date +%s) + "${seconds}"))
8 | while [ "$d" -ge `date +%s` ]; do
9 | echo -ne "$(date -u --date @$(($d - `date +%s`)) +%H:%M:%S)\r";
10 | sleep 0.1
11 | done
12 | }
13 |
--------------------------------------------------------------------------------
/compose/local/postgres/maintenance/_sourced/messages.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | message_newline() {
5 | echo
6 | }
7 |
8 | message_debug()
9 | {
10 | echo -e "DEBUG: ${@}"
11 | }
12 |
13 | message_welcome()
14 | {
15 | echo -e "\e[1m${@}\e[0m"
16 | }
17 |
18 | message_warning()
19 | {
20 | echo -e "\e[33mWARNING\e[0m: ${@}"
21 | }
22 |
23 | message_error()
24 | {
25 | echo -e "\e[31mERROR\e[0m: ${@}"
26 | }
27 |
28 | message_info()
29 | {
30 | echo -e "\e[37mINFO\e[0m: ${@}"
31 | }
32 |
33 | message_suggestion()
34 | {
35 | echo -e "\e[33mSUGGESTION\e[0m: ${@}"
36 | }
37 |
38 | message_success()
39 | {
40 | echo -e "\e[32mSUCCESS\e[0m: ${@}"
41 | }
42 |
--------------------------------------------------------------------------------
/compose/local/postgres/maintenance/_sourced/yes_no.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | yes_no() {
5 | declare desc="Prompt for confirmation. \$\"\{1\}\": confirmation message."
6 | local arg1="${1}"
7 |
8 | local response=
9 | read -r -p "${arg1} (y/[n])? " response
10 | if [[ "${response}" =~ ^[Yy]$ ]]
11 | then
12 | exit 0
13 | else
14 | exit 1
15 | fi
16 | }
17 |
--------------------------------------------------------------------------------
/compose/local/postgres/maintenance/backups:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 |
4 | ### View backups.
5 | ###
6 | ### Usage:
7 | ### $ docker-compose -f .yml (exec |run --rm) postgres backups
8 |
9 |
10 | set -o errexit
11 | set -o pipefail
12 | set -o nounset
13 |
14 |
15 | working_dir="$(dirname ${0})"
16 | source "${working_dir}/_sourced/constants.sh"
17 | source "${working_dir}/_sourced/messages.sh"
18 |
19 |
20 | message_welcome "These are the backups you have got:"
21 |
22 | ls -lht "${BACKUP_DIR_PATH}"
23 |
--------------------------------------------------------------------------------
/compose/local/ui/Dockerfile:
--------------------------------------------------------------------------------
1 | # Node version should always match the version in `ui/.nvmrc`
2 | FROM node:18
3 |
4 | # Set the working directory in the container
5 | WORKDIR /app
6 |
7 | # Configure git to trust the /app directory
8 | RUN git config --global --add safe.directory /app
9 |
10 | # Don't try to open a browser
11 | ENV BROWSER=none
12 |
13 | # Expose the port the app runs on
14 | EXPOSE 4000
15 |
16 | # Check for changed app dependencies on every start
17 | CMD ["sh", "-c", "yarn install && yarn start --debug --host 0.0.0.0 --port 4000"]
18 |
--------------------------------------------------------------------------------
/compose/production/aws/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM garland/aws-cli-docker:1.16.140
2 |
3 | COPY ./compose/production/aws/maintenance /usr/local/bin/maintenance
4 | COPY ./compose/local/postgres/maintenance/_sourced /usr/local/bin/maintenance/_sourced
5 |
6 | RUN chmod +x /usr/local/bin/maintenance/*
7 |
8 | RUN mv /usr/local/bin/maintenance/* /usr/local/bin \
9 | && rmdir /usr/local/bin/maintenance
10 |
--------------------------------------------------------------------------------
/compose/production/aws/maintenance/download:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | ### Download a file from your Amazon S3 bucket to the postgres /backups folder
4 | ###
5 | ### Usage:
6 | ### $ docker-compose -f production.yml run --rm awscli <1>
7 |
8 | set -o errexit
9 | set -o pipefail
10 | set -o nounset
11 |
12 | working_dir="$(dirname ${0})"
13 | source "${working_dir}/_sourced/constants.sh"
14 | source "${working_dir}/_sourced/messages.sh"
15 |
16 | export AWS_ACCESS_KEY_ID="${DJANGO_AWS_ACCESS_KEY_ID}"
17 | export AWS_SECRET_ACCESS_KEY="${DJANGO_AWS_SECRET_ACCESS_KEY}"
18 | export AWS_STORAGE_BUCKET_NAME="${DJANGO_AWS_STORAGE_BUCKET_NAME}"
19 |
20 |
21 | aws s3 cp s3://${AWS_STORAGE_BUCKET_NAME}${BACKUP_DIR_PATH}/${1} ${BACKUP_DIR_PATH}/${1}
22 |
23 | message_success "Finished downloading ${1}."
24 |
--------------------------------------------------------------------------------
/compose/production/django/celery/beat/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | set -o nounset
6 |
7 |
8 | exec celery -A config.celery_app beat -l INFO
9 |
--------------------------------------------------------------------------------
/compose/production/django/celery/flower/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o nounset
5 |
6 |
7 | exec celery \
8 | -A config.celery_app \
9 | -b "${CELERY_BROKER_URL}" \
10 | flower \
11 | --basic_auth="${CELERY_FLOWER_USER}:${CELERY_FLOWER_PASSWORD}"
12 | --db="${CELERY_FLOWER_DB:-/data/flower.db}"
13 |
--------------------------------------------------------------------------------
/compose/production/django/celery/worker/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | set -o nounset
6 |
7 | exec newrelic-admin run-program celery -A config.celery_app worker -l INFO
8 |
--------------------------------------------------------------------------------
/compose/production/django/start:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -o errexit
4 | set -o pipefail
5 | set -o nounset
6 |
7 | python /app/manage.py collectstatic --noinput
8 |
9 | exec newrelic-admin run-program /usr/local/bin/gunicorn config.asgi --bind 0.0.0.0:5000 --chdir=/app -k uvicorn.workers.UvicornWorker
10 |
--------------------------------------------------------------------------------
/compose/production/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:1.17.8-alpine
2 | COPY ./compose/production/nginx/default.conf /etc/nginx/conf.d/default.conf
3 |
--------------------------------------------------------------------------------
/compose/production/nginx/default.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | server_name localhost;
4 | location /media/ {
5 | alias /usr/share/nginx/media/;
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/compose/production/traefik/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM traefik:2.10.3
2 | RUN mkdir -p /etc/traefik/acme \
3 | && touch /etc/traefik/acme/acme.json \
4 | && chmod 600 /etc/traefik/acme/acme.json
5 | COPY ./compose/production/traefik/traefik.yml /etc/traefik
6 |
--------------------------------------------------------------------------------
/config/__init__.py:
--------------------------------------------------------------------------------
1 | # This will make sure the app is always imported when
2 | # Django starts so that shared_task will use this app.
3 | from .celery_app import app as celery_app
4 |
5 | __all__ = ("celery_app",)
6 |
--------------------------------------------------------------------------------
/config/celery_app.py:
--------------------------------------------------------------------------------
1 | import os
2 |
3 | from celery import Celery
4 |
5 | # set the default Django settings module for the 'celery' program.
6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "config.settings.local")
7 |
8 | app = Celery("ami")
9 |
10 | # Using a string here means the worker doesn't have to serialize
11 | # the configuration object to child processes.
12 | # - namespace='CELERY' means all celery-related configuration keys
13 | # should have a `CELERY_` prefix.
14 | app.config_from_object("django.conf:settings", namespace="CELERY")
15 |
16 | # Load task modules from all registered Django app configs.
17 | app.autodiscover_tasks()
18 |
--------------------------------------------------------------------------------
/config/settings/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/config/settings/__init__.py
--------------------------------------------------------------------------------
/config/websocket.py:
--------------------------------------------------------------------------------
1 | async def websocket_application(scope, receive, send):
2 | while True:
3 | event = await receive()
4 |
5 | if event["type"] == "websocket.connect":
6 | await send({"type": "websocket.accept"})
7 |
8 | if event["type"] == "websocket.disconnect":
9 | break
10 |
11 | if event["type"] == "websocket.receive":
12 | if event["text"] == "ping":
13 | await send({"type": "websocket.send", "text": "pong!"})
14 |
--------------------------------------------------------------------------------
/data/README.md:
--------------------------------------------------------------------------------
1 | This data folder is not kept under version control.
2 |
3 | However it must exist for `docker compose up` to work on its own.
4 |
5 | Use this folder for database snapshots, local test images, backups, etc.
6 |
--------------------------------------------------------------------------------
/data/db/snapshots/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/data/db/snapshots/.gitkeep
--------------------------------------------------------------------------------
/docs/__init__.py:
--------------------------------------------------------------------------------
1 | # Included so that Django's startproject comment runs against the docs directory
2 |
--------------------------------------------------------------------------------
/docs/diagrams/models.mermaid:
--------------------------------------------------------------------------------
1 | classDiagram
2 | Project -- Deployment
3 | Site -- Deployment
4 | DeviceType -- Deployment
5 | Deployment -- Event
6 | Project -- Site
7 | Event -- SourceImage
8 | Event -- SourceImageCollection
9 | SourceImageCollection -- SourceImage
10 | User -- Identification
11 | Identification -- Determination
12 | Pipeline -- Algorithms
13 | Job --> Detections : bbox, image embedding
14 | Job --> Predictions : classification scores
15 | Detections --> Occurrence : tracking
16 | Occurrence -- Determination
17 | Job <-- SourceImageCollection
18 | Job <-- Pipeline
19 | Project -- TaxaList
20 | TaxaList -- Determination
21 | Predictions -- Determination
22 |
--------------------------------------------------------------------------------
/docs/index.rst:
--------------------------------------------------------------------------------
1 | .. Automated Monitoring of Insects ML Platform documentation master file, created by
2 | sphinx-quickstart.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to Automated Monitoring of Insects ML Platform's documentation!
7 | ======================================================================
8 |
9 | .. toctree::
10 | :maxdepth: 2
11 | :caption: Contents:
12 |
13 | howto
14 | users
15 |
16 |
17 |
18 | Indices and tables
19 | ==================
20 |
21 | * :ref:`genindex`
22 | * :ref:`modindex`
23 | * :ref:`search`
24 |
--------------------------------------------------------------------------------
/docs/users.rst:
--------------------------------------------------------------------------------
1 | .. _users:
2 |
3 | Users
4 | ======================================================================
5 |
6 | Starting a new project, it’s highly recommended to set up a custom user model,
7 | even if the default User model is sufficient for you.
8 |
9 | This model behaves identically to the default user model,
10 | but you’ll be able to customize it in the future if the need arises.
11 |
12 | .. automodule:: ami.users.models
13 | :members:
14 | :noindex:
15 |
16 |
--------------------------------------------------------------------------------
/locale/en_US/LC_MESSAGES/django.po:
--------------------------------------------------------------------------------
1 | # Translations for the Automated Monitoring of Insects ML Platform project
2 | # Copyright (C) 2023 Rolnick Lab
3 | # Rolnick Lab , 2023.
4 | #
5 | #, fuzzy
6 | msgid ""
7 | msgstr ""
8 | "Project-Id-Version: 0.1.0\n"
9 | "Language: en-US\n"
10 | "MIME-Version: 1.0\n"
11 | "Content-Type: text/plain; charset=UTF-8\n"
12 | "Content-Transfer-Encoding: 8bit\n"
13 |
--------------------------------------------------------------------------------
/merge_production_dotenvs_in_dotenv.py:
--------------------------------------------------------------------------------
1 | import os
2 | from collections.abc import Sequence
3 | from pathlib import Path
4 |
5 | BASE_DIR = Path(__file__).parent.resolve()
6 | PRODUCTION_DOTENVS_DIR = BASE_DIR / ".envs" / ".production"
7 | PRODUCTION_DOTENV_FILES = [
8 | PRODUCTION_DOTENVS_DIR / ".django",
9 | PRODUCTION_DOTENVS_DIR / ".postgres",
10 | ]
11 | DOTENV_FILE = BASE_DIR / ".env"
12 |
13 |
14 | def merge(
15 | output_file: Path,
16 | files_to_merge: Sequence[Path],
17 | ) -> None:
18 | merged_content = ""
19 | for merge_file in files_to_merge:
20 | merged_content += merge_file.read_text()
21 | merged_content += os.linesep
22 | output_file.write_text(merged_content)
23 |
24 |
25 | if __name__ == "__main__":
26 | merge(DOTENV_FILE, PRODUCTION_DOTENV_FILES)
27 |
--------------------------------------------------------------------------------
/processing_services/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | ml_backend_minimal:
3 | build:
4 | context: ./minimal
5 | volumes:
6 | - ./minimal/:/app:z
7 | ports:
8 | - "2000:2000"
9 | extra_hosts:
10 | - minio:host-gateway
11 | networks:
12 | - antenna_network
13 |
14 | ml_backend_example:
15 | build:
16 | context: ./example
17 | volumes:
18 | - ./example/:/app:z
19 | ports:
20 | - "2003:2000"
21 | extra_hosts:
22 | - minio:host-gateway
23 | networks:
24 | - antenna_network
25 |
26 | networks:
27 | antenna_network:
28 | name: antenna_network
29 |
--------------------------------------------------------------------------------
/processing_services/example/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim
2 |
3 | # Set up ml backend FastAPI
4 | WORKDIR /app
5 | COPY . /app
6 | RUN pip install -r ./requirements.txt
7 | CMD ["python", "/app/main.py"]
8 |
--------------------------------------------------------------------------------
/processing_services/example/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/processing_services/example/api/__init__.py
--------------------------------------------------------------------------------
/processing_services/example/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | ml_backend_example:
3 | build:
4 | context: .
5 | volumes:
6 | - ./:/app:z
7 | ports:
8 | - "2003:2000"
9 | extra_hosts:
10 | - minio:host-gateway
11 | networks:
12 | - antenna_network
13 | deploy:
14 | resources:
15 | reservations:
16 | devices:
17 | - driver: nvidia
18 | count: 1
19 | capabilities: [gpu]
20 |
21 | networks:
22 | antenna_network:
23 | name: antenna_network
24 |
--------------------------------------------------------------------------------
/processing_services/example/main.py:
--------------------------------------------------------------------------------
1 | if __name__ == "__main__":
2 | import uvicorn
3 |
4 | uvicorn.run("api.api:app", host="0.0.0.0", port=2000, reload=True)
5 |
--------------------------------------------------------------------------------
/processing_services/example/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | pydantic
4 | Pillow
5 | requests
6 | transformers==4.50.3
7 | torch==2.6.0
8 | torchvision==0.21.0
9 |
--------------------------------------------------------------------------------
/processing_services/minimal/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM python:3.11-slim
2 |
3 | WORKDIR /app
4 |
5 | COPY . /app
6 |
7 | RUN pip install -r ./requirements.txt
8 |
9 | CMD ["python", "/app/main.py"]
10 |
--------------------------------------------------------------------------------
/processing_services/minimal/api/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/processing_services/minimal/api/__init__.py
--------------------------------------------------------------------------------
/processing_services/minimal/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | ml_backend_minimal:
3 | build:
4 | context: .
5 | volumes:
6 | - ./:/app:z
7 | ports:
8 | - "2000:2000"
9 | extra_hosts:
10 | - minio:host-gateway
11 | networks:
12 | - antenna_network
13 |
14 | networks:
15 | antenna_network:
16 | name: antenna_network
17 |
--------------------------------------------------------------------------------
/processing_services/minimal/main.py:
--------------------------------------------------------------------------------
1 | if __name__ == "__main__":
2 | import uvicorn
3 |
4 | uvicorn.run("api.api:app", host="0.0.0.0", port=2000, reload=True)
5 |
--------------------------------------------------------------------------------
/processing_services/minimal/requirements.txt:
--------------------------------------------------------------------------------
1 | fastapi
2 | uvicorn
3 | pydantic
4 | Pillow
5 | requests
6 |
--------------------------------------------------------------------------------
/requirements/local.txt:
--------------------------------------------------------------------------------
1 | -r base.txt
2 |
--------------------------------------------------------------------------------
/requirements/production.txt:
--------------------------------------------------------------------------------
1 | # PRECAUTION: avoid production dependencies that aren't in development
2 |
3 | -r base.txt
4 |
5 |
6 | # Django
7 | # ------------------------------------------------------------------------------
8 | # django-anymail[mailgun]==10.0 # https://github.com/anymail/django-anymail
9 | # django-anymail[sendgrid]==10.0 # https://github.com/anymail/django-anymail
10 |
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | # flake8 and pycodestyle don't support pyproject.toml
2 | # https://github.com/PyCQA/flake8/issues/234
3 | # https://github.com/PyCQA/pycodestyle/issues/813
4 | [flake8]
5 | max-line-length = 119
6 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv
7 |
8 | [pycodestyle]
9 | max-line-length = 119
10 | exclude = .tox,.git,*/migrations/*,*/static/CACHE/*,docs,node_modules,venv,.venv
11 |
--------------------------------------------------------------------------------
/ui/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": ["@typescript-eslint"],
5 | "extends": [
6 | "eslint:recommended",
7 | "plugin:@typescript-eslint/eslint-recommended",
8 | "plugin:@typescript-eslint/recommended"
9 | ],
10 | "rules": {
11 | "no-console": 1,
12 | "@typescript-eslint/no-unused-vars": 1,
13 | "@typescript-eslint/no-empty-function": 0,
14 | "@typescript-eslint/no-explicit-any": 0
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ui/.gitattributes:
--------------------------------------------------------------------------------
1 | .yarn/releases/** binary
2 | .yarn/plugins/** binary
3 |
4 |
--------------------------------------------------------------------------------
/ui/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | # sass
22 | /.sass-cache
23 |
24 | # Yarn 3 files
25 | .pnp.*
26 | .yarn/*
27 | !.yarn/patches
28 | !.yarn/plugins
29 | !.yarn/releases
30 | !.yarn/sdks
31 | !.yarn/versions
32 |
--------------------------------------------------------------------------------
/ui/.nvmrc:
--------------------------------------------------------------------------------
1 | v18.12.0
2 |
3 | # This should always match the version in `local/ui/Dockerfile`
4 |
--------------------------------------------------------------------------------
/ui/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "semi": false,
5 | "singleQuote": true
6 | }
7 |
--------------------------------------------------------------------------------
/ui/.yarnrc.yml:
--------------------------------------------------------------------------------
1 | nodeLinker: node-modules
2 |
3 | yarnPath: .yarn/releases/yarn-berry.cjs
4 |
--------------------------------------------------------------------------------
/ui/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | You need to enable JavaScript to run this app.
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/ui/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/ui/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/ui/public/android-chrome-256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/android-chrome-256x256.png
--------------------------------------------------------------------------------
/ui/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/ui/public/apple-touch-icon-precomposed.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/apple-touch-icon-precomposed.png
--------------------------------------------------------------------------------
/ui/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/ui/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #47518f
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/ui/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/favicon-16x16.png
--------------------------------------------------------------------------------
/ui/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/favicon-32x32.png
--------------------------------------------------------------------------------
/ui/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/favicon.ico
--------------------------------------------------------------------------------
/ui/public/fonts/Mazzard/mazzardsoftm-bold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-bold.otf
--------------------------------------------------------------------------------
/ui/public/fonts/Mazzard/mazzardsoftm-bolditalic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-bolditalic.otf
--------------------------------------------------------------------------------
/ui/public/fonts/Mazzard/mazzardsoftm-italic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-italic.otf
--------------------------------------------------------------------------------
/ui/public/fonts/Mazzard/mazzardsoftm-medium.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-medium.otf
--------------------------------------------------------------------------------
/ui/public/fonts/Mazzard/mazzardsoftm-mediumitalic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-mediumitalic.otf
--------------------------------------------------------------------------------
/ui/public/fonts/Mazzard/mazzardsoftm-regular.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-regular.otf
--------------------------------------------------------------------------------
/ui/public/fonts/Mazzard/mazzardsoftm-semibold.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-semibold.otf
--------------------------------------------------------------------------------
/ui/public/fonts/Mazzard/mazzardsoftm-semibolditalic.otf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/RolnickLab/antenna/015aaf64da7898440eee60aac965f8ae3a5553f4/ui/public/fonts/Mazzard/mazzardsoftm-semibolditalic.otf
--------------------------------------------------------------------------------
/ui/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/ui/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "Antenna Data Platform",
3 | "short_name": "Antenna",
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 | "start_url": "."
20 | }
21 |
--------------------------------------------------------------------------------
/ui/src/components/breadcrumbs/breadcrumbs.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .breadcrumbs {
5 | display: flex;
6 | align-items: center;
7 |
8 | > :not(:last-child) {
9 | margin-right: 16px;
10 | }
11 | }
12 |
13 | .breadcrumb {
14 | display: block;
15 | @include paragraph-x-small();
16 | font-weight: 600;
17 | color: $color-neutral-700;
18 |
19 | span {
20 | &:last-child {
21 | display: none;
22 | }
23 | }
24 |
25 | &.link {
26 | color: $color-primary-1-600;
27 | }
28 | }
29 |
30 | @media only screen and (max-width: $small-screen-breakpoint) {
31 | .breadcrumb:not(:first-child):not(:last-child) {
32 | span {
33 | &:first-child {
34 | display: none;
35 | }
36 | &:last-child {
37 | display: inline;
38 | }
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ui/src/components/constants.ts:
--------------------------------------------------------------------------------
1 | export const BREAKPOINTS = {
2 | SM: 576,
3 | MD: 768,
4 | LG: 1024,
5 | XL: 1280,
6 | }
7 |
--------------------------------------------------------------------------------
/ui/src/components/error-boundary/error-boundary.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .wrapper {
5 | display: flex;
6 | flex-direction: column;
7 | align-items: center;
8 | justify-content: center;
9 | gap: 16px;
10 | width: 100%;
11 | height: 100%;
12 | padding: 32px;
13 | background-color: $color-generic-white;
14 | @include paragraph-medium();
15 | color: $color-neutral-700;
16 | box-sizing: border-box;
17 | }
18 |
19 | .iconWrapper {
20 | display: flex;
21 | }
22 |
--------------------------------------------------------------------------------
/ui/src/components/fetch-info/fetch-info.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 | @import 'src/design-system/variables/variables.scss';
4 |
5 | .wrapper {
6 | height: 32px;
7 | display: flex;
8 | align-items: center;
9 | justify-content: center;
10 | z-index: 1;
11 |
12 | :not(:last-child) {
13 | margin-right: 6px;
14 | }
15 |
16 | span {
17 | @include paragraph-x-small();
18 | font-weight: 600;
19 | color: $color-neutral-300;
20 | }
21 | }
22 |
23 | @media only screen and (max-width: $small-screen-breakpoint) {
24 | .wrapper {
25 | display: none;
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/ui/src/components/fetch-info/fetch-info.tsx:
--------------------------------------------------------------------------------
1 | import { LoadingSpinner } from 'design-system/components/loading-spinner/loading-spinner'
2 | import { STRING, translate } from 'utils/language'
3 | import styles from './fetch-info.module.scss'
4 |
5 | enum FetchInfoType {
6 | Loading = 'loading',
7 | Updating = 'updating',
8 | }
9 |
10 | const messages: { [key in FetchInfoType]: string } = {
11 | [FetchInfoType.Loading]: translate(STRING.LOADING_DATA),
12 | [FetchInfoType.Updating]: translate(STRING.UPDATING_DATA),
13 | }
14 |
15 | export const FetchInfo = ({ isLoading }: { isLoading?: boolean }) => {
16 | const type = isLoading ? FetchInfoType.Loading : FetchInfoType.Updating
17 |
18 | return (
19 |
20 |
21 | {messages[type]}...
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/components/filtering/filters/image-filter.tsx:
--------------------------------------------------------------------------------
1 | import { useCaptureDetails } from 'data-services/hooks/captures/useCaptureDetails'
2 | import { FilterProps } from './types'
3 |
4 | export const ImageFilter = ({ value }: FilterProps) => {
5 | const { capture, isLoading } = useCaptureDetails(value)
6 |
7 | const label = (() => {
8 | if (capture) {
9 | return `#${capture.id}`
10 | }
11 | if (value && isLoading) {
12 | return 'Loading...'
13 | }
14 | return 'All images'
15 | })()
16 |
17 | return (
18 |
19 | {label}
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/ui/src/components/filtering/filters/session-filter.tsx:
--------------------------------------------------------------------------------
1 | import { useSessionDetails } from 'data-services/hooks/sessions/useSessionDetails'
2 | import { FilterProps } from './types'
3 |
4 | export const SessionFilter = ({ value }: FilterProps) => {
5 | const { session, isLoading } = useSessionDetails(value)
6 |
7 | const label = (() => {
8 | if (session) {
9 | return session.label
10 | }
11 | if (value && isLoading) {
12 | return 'Loading...'
13 | }
14 | return 'All sessions'
15 | })()
16 |
17 | return (
18 |
19 | {label}
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/ui/src/components/filtering/filters/taxa-list-filter.tsx:
--------------------------------------------------------------------------------
1 | import { TaxaList } from 'data-services/models/taxa-list'
2 | import { Select } from 'nova-ui-kit'
3 | import { FilterProps } from './types'
4 |
5 | export const TaxaListFilter = ({ data = [], value, onAdd }: FilterProps) => {
6 | const taxaLists = data as TaxaList[]
7 |
8 | return (
9 |
14 |
15 |
16 |
17 |
18 | {taxaLists.map((list) => (
19 |
20 | {list.name}
21 |
22 | ))}
23 |
24 |
25 | )
26 | }
27 |
--------------------------------------------------------------------------------
/ui/src/components/filtering/filters/type-filter.tsx:
--------------------------------------------------------------------------------
1 | import { Job, SERVER_JOB_TYPES } from 'data-services/models/job'
2 | import { Select } from 'nova-ui-kit'
3 | import { FilterProps } from './types'
4 |
5 | const OPTIONS = SERVER_JOB_TYPES.map((key) => {
6 | const typeInfo = Job.getJobTypeInfo(key)
7 |
8 | return {
9 | ...typeInfo,
10 | }
11 | })
12 |
13 | export const TypeFilter = ({ value, onAdd }: FilterProps) => (
14 |
15 |
16 |
17 |
18 |
19 | {OPTIONS.map((option) => (
20 |
21 | {option.label}
22 |
23 | ))}
24 |
25 |
26 | )
27 |
--------------------------------------------------------------------------------
/ui/src/components/filtering/filters/types.ts:
--------------------------------------------------------------------------------
1 | export interface FilterProps {
2 | data?: any
3 | error?: string
4 | onAdd: (value: string) => void
5 | onClear: () => void
6 | value: string | undefined
7 | }
8 |
--------------------------------------------------------------------------------
/ui/src/components/filtering/utils.ts:
--------------------------------------------------------------------------------
1 | // Help functions to handle boolean filters (search param values are defined as strings and need to be converted)
2 | export const stringToBoolean = (string?: string) => {
3 | switch (string?.toLowerCase()) {
4 | case 'true':
5 | case '1':
6 | return true
7 | case 'false':
8 | case '0':
9 | return false
10 | default:
11 | return undefined
12 | }
13 | }
14 |
15 | export const booleanToString = (value?: boolean) =>
16 | value !== undefined ? `${value}` : ''
17 |
18 | // Help function to decide if a filter section should be open or not on page load
19 | export const someActive = (
20 | fields: string[],
21 | activeFilters: { field: string }[]
22 | ) => activeFilters.some(({ field }) => fields.includes(field))
23 |
--------------------------------------------------------------------------------
/ui/src/components/form/types.ts:
--------------------------------------------------------------------------------
1 | export interface FieldConfig {
2 | label: string
3 | description?: string
4 | rules?: {
5 | required?: boolean
6 | minLength?: number
7 | maxLength?: number
8 | min?: number
9 | max?: number
10 | validate?: (value: any) => string | undefined
11 | }
12 | }
13 |
14 | export type FormConfig = {
15 | [name: string]: FieldConfig
16 | }
17 |
--------------------------------------------------------------------------------
/ui/src/components/header/user-info-dialog/user-info-form/user-info-form.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .miniForm {
5 | border-radius: 8px;
6 | border: 1px solid $color-neutral-100;
7 | overflow: hidden;
8 | }
9 |
10 | .miniFormContent {
11 | display: flex;
12 | flex-direction: column;
13 | gap: 16px;
14 | padding: 16px;
15 | }
16 |
17 | .miniFormActions {
18 | display: flex;
19 | justify-content: flex-end;
20 | gap: 16px;
21 | padding-top: 16px;
22 | }
23 |
--------------------------------------------------------------------------------
/ui/src/components/header/user-info-dialog/user-info-image-upload/user-info-image-upload.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .container {
5 | width: 100%;
6 | height: 0;
7 | padding-bottom: 100%;
8 | background-color: $color-primary-2-50;
9 | position: relative;
10 | }
11 |
12 | .content {
13 | position: absolute;
14 | width: 100%;
15 | height: 100%;
16 | display: flex;
17 | flex-direction: column;
18 | align-items: center;
19 | justify-content: center;
20 | gap: 8px;
21 | @include paragraph-small();
22 | color: $color-neutral-300;
23 |
24 | img {
25 | width: 100%;
26 | height: 100%;
27 | object-fit: cover;
28 | }
29 | }
30 |
31 | .overlay {
32 | position: absolute;
33 | width: 100%;
34 | height: 100%;
35 | vertical-align: middle;
36 | }
37 |
--------------------------------------------------------------------------------
/ui/src/components/header/version-info/version-info.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/variables.scss';
2 | @import 'src/design-system/variables/colors.scss';
3 | @import 'src/design-system/variables/typography.scss';
4 |
5 | .wrapper {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | gap: 16px;
10 | }
11 |
12 | .badge {
13 | padding: 6px 16px 4px;
14 | border-radius: 4px;
15 | background-color: $color-success-100;
16 | color: $color-success-700;
17 | text-align: center;
18 | @include label();
19 | }
20 |
21 | .version {
22 | @include paragraph-x-small();
23 | color: $color-neutral-700;
24 | white-space: nowrap;
25 | }
26 |
27 | .deprecated {
28 | .badge {
29 | background-color: $color-destructive-100;
30 | color: $color-destructive-600;
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/components/info-page/code-of-conduct-page/code-of-conduct-page.tsx:
--------------------------------------------------------------------------------
1 | import { InfoPage } from '../info-page'
2 | import markdown from './code-of-conduct.md'
3 |
4 | export const CodeOfConductPage = () => (
5 |
6 | )
7 |
--------------------------------------------------------------------------------
/ui/src/components/info-page/terms-of-service-page/terms-of-service-page.tsx:
--------------------------------------------------------------------------------
1 | import { InfoPage } from '../info-page'
2 | import markdown from './terms-of-service.md'
3 |
4 | export const TermsOfServicePage = () => (
5 |
6 | )
7 |
--------------------------------------------------------------------------------
/ui/src/components/license-info/license-info.tsx:
--------------------------------------------------------------------------------
1 | import { CSSProperties } from 'react'
2 |
3 | const LINK = 'https://creativecommons.org/licenses/by-nc/4.0/legalcode'
4 |
5 | interface LicenseInfoProps {
6 | style?: CSSProperties
7 | }
8 |
9 | export const LicenseInfo = ({ style }: LicenseInfoProps) => {
10 | // TODO: Check licence given the current project
11 |
12 | return (
13 |
14 | These images are licensed under{' '}
15 |
16 | CC BY-NC 4.0
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/ui/src/components/taxon-search/buildTree.ts:
--------------------------------------------------------------------------------
1 | import { Node, TreeItem } from './types'
2 |
3 | export const buildTree = (data: Node[]) => {
4 | const tree: TreeItem[] = []
5 | const treeMap: { [key: string]: TreeItem } = {}
6 |
7 | data.forEach((node: Node) => (treeMap[node.id] = { ...node, children: [] }))
8 |
9 | data.forEach((node: Node) => {
10 | if (node.parentId && treeMap[node.parentId]) {
11 | treeMap[node.parentId].children.push(treeMap[node.id])
12 | } else {
13 | tree.push(treeMap[node.id])
14 | }
15 | })
16 |
17 | return tree
18 | }
19 |
--------------------------------------------------------------------------------
/ui/src/components/taxon-search/types.ts:
--------------------------------------------------------------------------------
1 | export type Node = {
2 | id: string
3 | label: string
4 | details?: string
5 | parentId?: string
6 | }
7 |
8 | export type TreeItem = Node & { children: TreeItem[] }
9 |
--------------------------------------------------------------------------------
/ui/src/components/taxon/taxon-ranks/taxon-ranks.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .ranks {
5 | display: flex;
6 | flex-wrap: wrap;
7 | align-items: center;
8 | column-gap: 6px;
9 | margin-top: 8px;
10 |
11 | &.compact {
12 | flex-wrap: nowrap;
13 | }
14 | }
15 |
16 | .rank {
17 | display: block;
18 | @include paragraph-x-small();
19 | color: $color-neutral-600;
20 | white-space: nowrap;
21 |
22 | &.divider {
23 | color: $color-neutral-300;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ui/src/components/terms-of-service-info/terms-of-service-info.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 | @import 'src/design-system/variables/variables.scss';
4 |
5 | .wrapper {
6 | display: flex;
7 | align-items: center;
8 | justify-content: space-between;
9 | @include paragraph-x-small();
10 | padding: 8px 16px;
11 | border-radius: 4px;
12 | background-color: $color-success-100;
13 | color: $color-success-700;
14 | margin-bottom: 16px;
15 |
16 | p {
17 | @include paragraph-small();
18 | margin: 0;
19 | }
20 |
21 | a {
22 | font-weight: 600;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/auth/useLogin.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import axios from 'axios'
3 | import { API_ROUTES, API_URL } from 'data-services/constants'
4 | import { useUser } from 'utils/user/userContext'
5 |
6 | export const useLogin = ({ onSuccess }: { onSuccess?: () => void } = {}) => {
7 | const { setToken } = useUser()
8 | const { mutate, isLoading, isSuccess, error } = useMutation({
9 | mutationFn: (data: { email: string; password: string }) =>
10 | axios
11 | .post<{ auth_token: string }>(`${API_URL}/${API_ROUTES.LOGIN}/`, data)
12 | .then((res) => res.data.auth_token),
13 | onSuccess: (token) => {
14 | setToken(token)
15 | onSuccess?.()
16 | },
17 | })
18 |
19 | return { login: mutate, isLoading, isSuccess, error }
20 | }
21 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/auth/useLogout.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import axios from 'axios'
3 | import { API_ROUTES, API_URL } from 'data-services/constants'
4 | import { getAuthHeader } from 'data-services/utils'
5 | import { useUser } from 'utils/user/userContext'
6 |
7 | export const useLogout = () => {
8 | const { clearToken, user } = useUser()
9 | const { mutate, isLoading, isSuccess, error } = useMutation({
10 | mutationFn: () =>
11 | axios.post(`${API_URL}/${API_ROUTES.LOGOUT}/`, undefined, {
12 | headers: getAuthHeader(user),
13 | }),
14 | onSuccess: clearToken,
15 | onError: clearToken,
16 | })
17 |
18 | return { logout: mutate, isLoading, isSuccess, error }
19 | }
20 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/auth/useResetPassword.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import axios from 'axios'
3 | import { API_ROUTES, API_URL } from 'data-services/constants'
4 |
5 | export const useResetPassword = (onSuccess?: () => void) => {
6 | const { mutate, isLoading, isSuccess, error } = useMutation({
7 | mutationFn: (data: { email: string }) =>
8 | axios
9 | .post(`${API_URL}/${API_ROUTES.RESET_PASSWORD}/`, data)
10 | .then((res) => res.data.auth_token),
11 | onSuccess,
12 | })
13 |
14 | return { resetPassword: mutate, isLoading, isSuccess, error }
15 | }
16 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/auth/useResetPasswordConfirm.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import axios from 'axios'
3 | import { API_ROUTES, API_URL } from 'data-services/constants'
4 |
5 | export const useResetPasswordConfirm = (onSuccess?: () => void) => {
6 | const { mutate, isLoading, isSuccess, error } = useMutation({
7 | mutationFn: (data: {
8 | new_password: string
9 | token?: string
10 | uid?: string
11 | }) =>
12 | axios
13 | .post(`${API_URL}/${API_ROUTES.RESET_PASSWORD_CONFIRM}/`, data)
14 | .then((res) => res.data.auth_token),
15 | onSuccess,
16 | })
17 |
18 | return { resetPasswordConfirm: mutate, isLoading, isSuccess, error }
19 | }
20 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/auth/useSignUp.ts:
--------------------------------------------------------------------------------
1 | import { useMutation } from '@tanstack/react-query'
2 | import axios from 'axios'
3 | import { API_ROUTES, API_URL } from 'data-services/constants'
4 |
5 | export const useSignUp = ({ onSuccess }: { onSuccess?: () => void } = {}) => {
6 | const { mutate, isLoading, isSuccess, error } = useMutation({
7 | mutationFn: (data: { email: string; password: string }) =>
8 | axios
9 | .post(`${API_URL}/${API_ROUTES.USERS}/`, data)
10 | .then((res) => res.data),
11 | onSuccess,
12 | })
13 |
14 | return { signUp: mutate, isLoading, isSuccess, error }
15 | }
16 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/entities/types.ts:
--------------------------------------------------------------------------------
1 | export interface EntityFieldValues {
2 | description?: string
3 | name: string
4 | projectId: string
5 | customFields?: { [key: string]: string | number | object | undefined }
6 | }
7 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/entities/useDeleteEntity.ts:
--------------------------------------------------------------------------------
1 | import { useMutation, useQueryClient } from '@tanstack/react-query'
2 | import axios from 'axios'
3 | import { API_URL } from 'data-services/constants'
4 | import { getAuthHeader } from 'data-services/utils'
5 | import { useUser } from 'utils/user/userContext'
6 |
7 | export const useDeleteEntity = (collection: string, onSuccess?: () => void) => {
8 | const { user } = useUser()
9 | const queryClient = useQueryClient()
10 |
11 | const { mutateAsync, isLoading, isSuccess, error } = useMutation({
12 | mutationFn: (id: string) =>
13 | axios.delete(`${API_URL}/${collection}/${id}/`, {
14 | headers: getAuthHeader(user),
15 | }),
16 | onSuccess: () => {
17 | queryClient.invalidateQueries([collection])
18 | onSuccess?.()
19 | },
20 | })
21 |
22 | return { deleteEntity: mutateAsync, isLoading, isSuccess, error }
23 | }
24 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/entities/utils.ts:
--------------------------------------------------------------------------------
1 | import { EntityFieldValues } from './types'
2 |
3 | export const convertToServerFieldValues = (fieldValues: EntityFieldValues) => {
4 | const { description, name, projectId, customFields } = fieldValues
5 |
6 | return {
7 | ...(description ? { description } : {}),
8 | ...(name ? { name } : {}),
9 | project: projectId,
10 | ...(customFields ?? {}),
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/identifications/types.ts:
--------------------------------------------------------------------------------
1 | export interface IdentificationFieldValues {
2 | agreeWith?: {
3 | identificationId?: string
4 | predictionId?: string
5 | }
6 | occurrenceId: string
7 | taxonId: string
8 | comment?: string
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/pages/types.ts:
--------------------------------------------------------------------------------
1 | export interface Page {
2 | id: number
3 | name: string
4 | slug: string
5 | }
6 |
7 | export interface PageDetails extends Page {
8 | html: string
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/pages/usePageDetails.ts:
--------------------------------------------------------------------------------
1 | import { API_ROUTES, API_URL } from 'data-services/constants'
2 | import { useAuthorizedQuery } from '../auth/useAuthorizedQuery'
3 | import { PageDetails } from './types'
4 |
5 | export const usePageDetails = (
6 | slug: string
7 | ): {
8 | page?: PageDetails
9 | isLoading: boolean
10 | isFetching: boolean
11 | error?: unknown
12 | } => {
13 | const { data, isLoading, isFetching, error } =
14 | useAuthorizedQuery({
15 | queryKey: [API_ROUTES.PAGES, slug],
16 | url: `${API_URL}/${API_ROUTES.PAGES}/${slug}/`,
17 | })
18 |
19 | return {
20 | page: data,
21 | isLoading,
22 | isFetching,
23 | error,
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/ui/src/data-services/hooks/projects/utils.ts:
--------------------------------------------------------------------------------
1 | export const convertToServerFormData = (fieldValues: any) => {
2 | const data = new FormData()
3 |
4 | if (fieldValues.name) {
5 | data.append('name', fieldValues.name)
6 | }
7 | if (fieldValues.description) {
8 | data.append('description', fieldValues.description)
9 | }
10 | if (fieldValues.image) {
11 | data.append('image', fieldValues.image, fieldValues.image.name)
12 | } else if (fieldValues.image === null) {
13 | data.append('image', '')
14 | }
15 |
16 | return data
17 | }
18 |
--------------------------------------------------------------------------------
/ui/src/data-services/models/taxa-list.ts:
--------------------------------------------------------------------------------
1 | import { Entity, ServerEntity } from 'data-services/models/entity'
2 |
3 | export type ServerTaxaList = ServerEntity & {
4 | taxa: string // URL to taxa API endpoint (filtered by this TaxaList)
5 | projects: number[] // Array of project IDs
6 | }
7 |
8 | export class TaxaList extends Entity {
9 | protected readonly _taxaList: ServerTaxaList
10 |
11 | public constructor(taxaList: ServerTaxaList) {
12 | super(taxaList) // Call the parent class constructor
13 | this._taxaList = taxaList
14 | }
15 |
16 | get taxaUrl(): string {
17 | return this._taxaList.taxa
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/ui/src/data-services/types.ts:
--------------------------------------------------------------------------------
1 | export interface FetchParams {
2 | projectId?: string
3 | pagination?: { page: number; perPage: number }
4 | sort?: { field: string; order: 'asc' | 'desc' }
5 | filters?: { field: string; value?: string; error?: string }[]
6 | }
7 |
8 | export interface APIValidationError {
9 | detail: string
10 | }
11 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/box/box.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/variables.scss';
3 |
4 | .box {
5 | background: $color-generic-white;
6 | width: 100%;
7 | border-radius: 6px;
8 | border: 1px solid $color-neutral-100;
9 | overflow-x: auto;
10 | }
11 |
12 | .boxContent {
13 | padding: 32px;
14 | display: inline-flex;
15 | align-items: center;
16 | justify-content: center;
17 | min-width: 100%;
18 | height: 100%;
19 | box-sizing: border-box;
20 | }
21 |
22 | @media only screen and (max-width: $small-screen-breakpoint) {
23 | .boxContent {
24 | padding: 16px;
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/box/box.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import styles from './box.module.scss'
3 |
4 | export const Box = ({ children }: { children: ReactNode }) => (
5 |
8 | )
9 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/bulk-action-bar/bulk-action-bar.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .wrapper {
5 | bottom: 72px;
6 | position: fixed;
7 | border-radius: 32px;
8 | height: 64px;
9 | padding: 0 32px;
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | gap: 16px;
14 | background-color: $color-generic-white;
15 | box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
16 | }
17 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/bulk-action-bar/bulk-action-bar.tsx:
--------------------------------------------------------------------------------
1 | import { XIcon } from 'lucide-react'
2 | import { Button } from 'nova-ui-kit'
3 | import { ReactNode } from 'react'
4 | import styles from './bulk-action-bar.module.scss'
5 |
6 | interface BulkActionBarProps {
7 | children: ReactNode
8 | selectedItems: string[]
9 | onClear: () => void
10 | }
11 |
12 | export const BulkActionBar = ({
13 | children,
14 | selectedItems,
15 | onClear,
16 | }: BulkActionBarProps) => (
17 |
18 |
19 | {selectedItems.length} selected
20 |
21 | {children}
22 |
23 |
24 |
25 |
26 | )
27 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/file-input/file-input.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .container {
5 | display: flex;
6 | align-items: center;
7 | justify-content: flex-start;
8 | margin-top: 8px;
9 | gap: 16px;
10 | }
11 |
12 | .fileInput {
13 | width: 0.1px;
14 | height: 0.1px;
15 | opacity: 0;
16 | overflow: hidden;
17 | position: absolute;
18 | z-index: -1;
19 | }
20 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/file-input/types.ts:
--------------------------------------------------------------------------------
1 | export enum FileInputAccept {
2 | All = 'all',
3 | Images = 'images',
4 | }
5 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/checkmark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/occurrences.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/photograph.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/play-button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/check.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/circle-backslash.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/cross.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/heart-filled.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/minus.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/options.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/pencil.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/search.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/toggle-down.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/toggle-left.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/toggle-right.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/radix/trash.svg:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/assets/sort.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/icon/icon.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .wrapper {
5 | display: inline-flex;
6 | align-items: center;
7 | justify-content: center;
8 | transition: transform 400ms ease;
9 |
10 | &.light {
11 | color: $color-generic-white;
12 | }
13 |
14 | &.dark {
15 | color: $color-neutral-700;
16 | }
17 |
18 | &.neutral {
19 | color: $color-neutral-300;
20 | }
21 |
22 | &.primary {
23 | color: $color-primary-1-600;
24 | }
25 |
26 | &.success {
27 | color: $color-success-700;
28 | }
29 |
30 | &.error {
31 | color: $color-destructive-600;
32 | }
33 |
34 | &.fixedSized {
35 | > * {
36 | width: 100%;
37 | height: 100%;
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/image-carousel/types.ts:
--------------------------------------------------------------------------------
1 | export enum CarouselTheme {
2 | Default = 'default',
3 | Light = 'light',
4 | }
5 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/image-carousel/utils.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import { CSSProperties } from 'react'
3 |
4 | export const getImageBoxStyles = (
5 | width: string | number = 100
6 | ): CSSProperties => ({
7 | width: _.isNumber(width) ? `${width}px` : width,
8 | })
9 |
10 | export const getPlaceholderStyles = (ratio = 1): CSSProperties => ({
11 | paddingBottom: `${(1 / ratio) * 100}%`,
12 | })
13 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/image-upload/image-upload.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .container {
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | background-color: $color-primary-2-50;
9 |
10 | img {
11 | max-width: 100%;
12 | }
13 | }
14 |
15 | .info {
16 | @include paragraph-small();
17 | color: $color-neutral-300;
18 | margin: 96px 32px;
19 | }
20 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/info-block/info-block.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .field {
5 | margin: 0;
6 |
7 | &:not(:last-child) {
8 | margin-bottom: 16px;
9 | }
10 | }
11 |
12 | .fieldLabel {
13 | display: block;
14 | @include label();
15 | font-weight: 600;
16 | color: $color-neutral-300;
17 | }
18 |
19 | .fieldValue {
20 | display: block;
21 | @include paragraph-medium();
22 | color: $color-neutral-700;
23 |
24 | &.link {
25 | color: $color-primary-1-600;
26 | font-weight: 600;
27 | }
28 |
29 | &.bubble {
30 | @include bubble-label();
31 | display: inline-flex;
32 | margin-top: 4px;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/loading-spinner/loading-spinner.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 |
3 | .loadingSpinner {
4 | display: inline-block;
5 | border-style: solid;
6 | border-color: $color-primary-2-500;
7 | border-top-color: transparent;
8 | border-radius: 50%;
9 | animation: spin 0.7s ease-in-out infinite;
10 | }
11 |
12 | @keyframes spin {
13 | to {
14 | -webkit-transform: rotate(360deg);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/loading-spinner/loading-spinner.tsx:
--------------------------------------------------------------------------------
1 | import styles from './loading-spinner.module.scss'
2 |
3 | export const LoadingSpinner = ({ size = 56 }: { size?: number }) => {
4 | return (
5 |
13 | )
14 | }
15 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/page-footer/page-footer.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .wrapper {
5 | position: fixed;
6 | bottom: 0;
7 | left: 0;
8 | width: 100%;
9 | background-color: $color-generic-white;
10 | border-top: 1px solid $color-neutral-100;
11 | z-index: 2;
12 | }
13 |
14 | .content {
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | width: 100%;
19 | height: 64px;
20 | padding: 0 100px;
21 | box-sizing: border-box;
22 | }
23 |
24 | @media only screen and (max-width: $medium-screen-breakpoint) {
25 | .content {
26 | padding: 0 32px;
27 | }
28 | }
29 |
30 | @media only screen and (max-width: $small-screen-breakpoint) {
31 | .content {
32 | padding: 0 16px;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/page-footer/page-footer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import styles from './page-footer.module.scss'
3 |
4 | interface PageFooterProps {
5 | hide?: boolean
6 | children?: ReactNode
7 | }
8 |
9 | export const PageFooter = ({ hide, children }: PageFooterProps) => {
10 | if (!children || hide) {
11 | return null
12 | }
13 |
14 | return (
15 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/pagination-bar/getPageWindow.ts:
--------------------------------------------------------------------------------
1 | const WINDOW_SIZE = 2
2 |
3 | export const getPageWindow = (currentPage: number, numPages: number) => {
4 | const pages = Array.from(Array(numPages).keys())
5 | const startIndex = currentPage - WINDOW_SIZE
6 | const endIndex = currentPage + WINDOW_SIZE + 1
7 |
8 | const offset = (() => {
9 | if (startIndex < 0) {
10 | return -startIndex
11 | }
12 | if (endIndex >= pages.length) {
13 | return pages.length - endIndex
14 | }
15 | return 0
16 | })()
17 |
18 | return pages.slice(startIndex + offset, endIndex + offset)
19 | }
20 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/pagination-bar/info-label/getValueInRange.ts:
--------------------------------------------------------------------------------
1 | export const getValueInRange = (args: {
2 | value: number
3 | min: number
4 | max: number
5 | }) => Math.min(args.max, Math.max(args.min, args.value))
6 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/pagination-bar/info-label/info-label.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 | @import 'src/design-system/variables/variables.scss';
4 |
5 | .infoLabel {
6 | display: block;
7 | @include paragraph-small();
8 | font-weight: 600;
9 | color: $color-neutral-300;
10 | }
11 |
12 | @media only screen and (max-width: $small-screen-breakpoint) {
13 | .infoLabel {
14 | display: none;
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/pagination-bar/page-button/page-button.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/typography.scss';
2 |
3 | .pageButton {
4 | @include paragraph-small();
5 | font-weight: 600;
6 | padding: 0 8px;
7 | }
8 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/pagination-bar/page-button/page-button.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonTheme } from '../../button/button'
2 | import styles from './page-button.module.scss'
3 |
4 | interface PageButtonProps {
5 | page: number
6 | active?: boolean
7 | onClick: () => void
8 | }
9 |
10 | export const PageButton = ({ page, active, onClick }: PageButtonProps) => (
11 |
18 | )
19 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/pagination-bar/pagination-bar.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 | @import 'src/design-system/variables/variables.scss';
4 |
5 | .wrapper {
6 | width: 100%;
7 | height: 64px;
8 | display: flex;
9 | align-items: center;
10 | justify-content: space-between;
11 | }
12 |
13 | .pageSettings {
14 | display: flex;
15 | align-items: center;
16 | justify-content: flex-start;
17 | gap: 8px;
18 | }
19 |
20 | .pageWindow {
21 | display: flex;
22 | align-items: center;
23 | justify-content: center;
24 | }
25 |
26 | .divider {
27 | @include paragraph-small();
28 | font-weight: 600;
29 | color: $color-neutral-100;
30 | }
31 |
32 | @media only screen and (max-width: $small-screen-breakpoint) {
33 | .wrapper {
34 | justify-content: center;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/plot-grid/plot-grid.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | container-type: inline-size;
3 | container-name: container;
4 | }
5 |
6 | .grid {
7 | display: grid;
8 | grid-template-columns: 1fr 1fr 1fr;
9 | gap: 24px;
10 | }
11 |
12 | /* Adjust number of columns based on container width, to avoid horizontal scrolling.
13 | *
14 | * Breakpoints are calculated as follows:
15 | * maxWidth = plotWidth * numBoxes + gapSize * (numBoxes - 1)
16 | */
17 |
18 | @container container (max-width: calc(384px * 3 + 24px * 2)) {
19 | .grid {
20 | grid-template-columns: 1fr 1fr;
21 | }
22 | }
23 |
24 | @container container (max-width: calc(384px * 2 + 24px * 1)) {
25 | .grid {
26 | grid-template-columns: 1fr;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/plot-grid/plot-grid.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import styles from './plot-grid.module.scss'
3 |
4 | export const PlotGrid = ({ children }: { children: ReactNode }) => (
5 |
8 | )
9 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/plot/lazy-plot.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBoundary } from 'components/error-boundary/error-boundary'
2 | import React, { Suspense } from 'react'
3 | import { LoadingSpinner } from '../loading-spinner/loading-spinner'
4 | import { PlotProps } from './types'
5 |
6 | const _Plot = React.lazy(() => import('./plot'))
7 |
8 | export const Plot = (props: PlotProps) => (
9 | }>
10 |
11 | <_Plot {...props} />
12 |
13 |
14 | )
15 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/plot/plot.module.scss:
--------------------------------------------------------------------------------
1 | .plot {
2 | max-width: 100%;
3 | text-align: center;
4 |
5 | :global(.barlayer .point > path) {
6 | clip-path: inset(0% 0% 0% 0% round 4px);
7 | }
8 |
9 | &.round {
10 | :global(.barlayer .point > path) {
11 | clip-path: inset(0% 0% 0% 0% round 32px);
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/plot/types.ts:
--------------------------------------------------------------------------------
1 | export interface PlotProps {
2 | title: string
3 | orientation?: 'h' | 'v'
4 | data: {
5 | x: (string | number)[]
6 | y: (string | number)[]
7 | tickvals?: (string | number)[]
8 | ticktext?: string[]
9 | }
10 | type?: 'bar' | 'scatter'
11 | showRangeSlider?: boolean
12 | categorical?: boolean
13 | }
14 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/popover/popover.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .popoverContent {
5 | border-radius: 4px;
6 | background-color: $color-generic-white;
7 | box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
8 | outline: none;
9 | z-index: 3;
10 | }
11 |
12 | .popoverClose {
13 | position: absolute;
14 | top: 8px;
15 | right: 8px;
16 | padding: 0 4px;
17 |
18 | span {
19 | display: block;
20 | padding: 0 4px;
21 | @include paragraph-small();
22 | font-weight: 600;
23 | color: $color-neutral-300;
24 | }
25 |
26 | &:hover {
27 | cursor: pointer;
28 | opacity: 0.7;
29 | }
30 |
31 | &:focus-visible {
32 | box-shadow: 0 0 0 2px $color-generic-black;
33 | }
34 | }
35 |
36 | .popoverArrow {
37 | fill: $color-generic-white;
38 | }
39 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/slider/dial.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/status/status-marker/status-marker.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 |
3 | .statusMarker {
4 | width: 12px;
5 | height: 12px;
6 | border-radius: 50%;
7 | background-color: $color-neutral-100;
8 | }
9 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/status/status-marker/status-marker.tsx:
--------------------------------------------------------------------------------
1 | import classNames from 'classnames'
2 | import styles from './status-marker.module.scss'
3 |
4 | export const StatusMarker = ({ color }: { color: string }) => (
5 |
9 | )
10 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/status/types.ts:
--------------------------------------------------------------------------------
1 | export enum Status {
2 | Success = 'success',
3 | Warning = 'warning',
4 | Error = 'error',
5 | Neutral = 'neutral',
6 | }
7 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/table/basic-table-cell/basic-table-cell.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .tableCell {
5 | padding: 12px 32px 12px 16px;
6 |
7 | .label {
8 | display: block;
9 | @include paragraph-medium();
10 | color: $color-neutral-700;
11 | }
12 |
13 | .details {
14 | display: block;
15 | @include paragraph-x-small();
16 | color: $color-neutral-600;
17 | }
18 |
19 | &.primary {
20 | .label {
21 | color: $color-primary-1-600;
22 | font-weight: 600;
23 | }
24 | }
25 |
26 | &.bubble {
27 | .label {
28 | @include bubble-label();
29 | display: inline-flex;
30 | }
31 | }
32 | }
33 |
34 | @media only screen and (max-width: $small-screen-breakpoint) {
35 | .tableCell {
36 | padding: 12px 16px;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/table/column-settings/column-settings.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .triggerButton {
5 | flex-direction: row-reverse;
6 | }
7 |
8 | .wrapper {
9 | padding: 20px;
10 | min-width: 180px;
11 | box-sizing: border-box;
12 | }
13 |
14 | .description {
15 | display: block;
16 | @include paragraph-small();
17 | font-weight: 600;
18 | color: $color-neutral-300;
19 | margin-bottom: 16px;
20 | }
21 |
22 | .settings {
23 | display: flex;
24 | flex-direction: column;
25 | gap: 4px;
26 | }
27 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/table/image-table-cell/image-table-cell.module.scss:
--------------------------------------------------------------------------------
1 | .container {
2 | padding: 12px 16px;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | }
7 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/table/image-table-cell/image-table-cell.tsx:
--------------------------------------------------------------------------------
1 | import { ImageCarousel } from 'design-system/components/image-carousel/image-carousel'
2 | import { ImageCellTheme } from '../types'
3 | import styles from './image-table-cell.module.scss'
4 |
5 | interface ImageTableCellProps {
6 | autoPlay?: boolean
7 | images: {
8 | src: string
9 | alt?: string
10 | }[]
11 | total?: number
12 | theme?: ImageCellTheme
13 | to?: string
14 | }
15 |
16 | export const ImageTableCell = (props: ImageTableCellProps) => (
17 |
18 |
19 |
20 | )
21 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/table/status-table-cell/status-table-cell.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .tableCell {
5 | padding: 12px 32px 12px 16px;
6 |
7 | .content {
8 | display: flex;
9 | align-items: center;
10 | justify-content: flex-start;
11 | gap: 8px;
12 | }
13 |
14 | .label {
15 | display: block;
16 | @include paragraph-medium();
17 | color: $color-neutral-700;
18 | padding-top: 2px;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/table/status-table-cell/status-table-cell.tsx:
--------------------------------------------------------------------------------
1 | import { StatusMarker } from 'design-system/components/status/status-marker/status-marker'
2 | import { Tooltip } from 'design-system/components/tooltip/tooltip'
3 | import styles from './status-table-cell.module.scss'
4 |
5 | interface StatusTableCellProps {
6 | color: string
7 | details?: string
8 | label: string
9 | }
10 |
11 | export const StatusTableCell = ({
12 | color,
13 | details,
14 | label,
15 | }: StatusTableCellProps) => (
16 |
17 |
18 |
19 |
20 | {label}
21 |
22 |
23 |
24 | )
25 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/tooltip/basic-tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { Tooltip } from 'nova-ui-kit'
2 | import { ReactNode } from 'react'
3 |
4 | interface TooltipProps {
5 | asChild?: boolean
6 | children: ReactNode
7 | content?: string
8 | onTriggerClick?: () => void
9 | }
10 |
11 | export const BasicTooltip = ({
12 | asChild,
13 | children,
14 | content,
15 | onTriggerClick,
16 | }: TooltipProps) => {
17 | if (!content?.length) {
18 | return <>{children}>
19 | }
20 |
21 | return (
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 | {content}
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/tooltip/tooltip.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .tooltipContent {
5 | padding: 4px 12px;
6 | border-radius: 4px;
7 | background-color: $color-generic-white;
8 | box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1);
9 | color: $color-neutral-700;
10 | @include paragraph-x-small();
11 | user-select: none;
12 | white-space: normal;
13 | text-align: center;
14 | z-index: 3;
15 | max-width: 320px;
16 | word-break: break-word;
17 |
18 | .tooltipArrow {
19 | fill: $color-generic-white;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ui/src/design-system/components/wizard/status-bullet/status-bullet.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .status {
5 | display: flex;
6 | align-items: center;
7 | justify-content: center;
8 | width: 24px;
9 | height: 24px;
10 | border-radius: 12px;
11 | background-color: $color-primary-2-600;
12 | padding-top: 2px;
13 | @include paragraph-x-small();
14 | font-weight: 600;
15 | color: $color-generic-white;
16 |
17 | &.success {
18 | background-color: $color-success-500;
19 | }
20 |
21 | &.neutral {
22 | background-color: $color-neutral-300;
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/design-system/map/config.ts:
--------------------------------------------------------------------------------
1 | import * as L from 'leaflet'
2 | import 'leaflet/dist/leaflet.css'
3 | import pin from './pin.svg'
4 |
5 | export const ATTRIBUTION =
6 | '© OpenStreetMap '
7 |
8 | export const TILE_LAYER_URL =
9 | 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
10 |
11 | export const DEFAULT_ZOOM = 10
12 |
13 | export const MIN_ZOOM = 1
14 |
15 | export const MAX_BOUNDS = new L.LatLngBounds([-90, -180], [90, 180])
16 |
17 | const DefaultIcon = L.icon({
18 | iconUrl: pin,
19 | iconSize: [27, 32],
20 | iconAnchor: [27 / 2, 32],
21 | })
22 |
23 | export const setup = () => {
24 | L.Marker.prototype.options.icon = DefaultIcon
25 | }
26 |
--------------------------------------------------------------------------------
/ui/src/design-system/map/styles.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 |
3 | .mapContainer {
4 | display: flex;
5 | align-items: center;
6 | justify-content: center;
7 | width: 100%;
8 | height: 320px;
9 | border: 1px solid $color-neutral-100;
10 | border-radius: 6px;
11 | overflow: hidden;
12 | position: relative;
13 | z-index: 0;
14 | }
15 |
--------------------------------------------------------------------------------
/ui/src/design-system/map/types.ts:
--------------------------------------------------------------------------------
1 | export { LatLng as MarkerPosition, Map } from 'leaflet'
2 |
--------------------------------------------------------------------------------
/ui/src/design-system/variables/variables.scss:
--------------------------------------------------------------------------------
1 | /*
2 | * Screen size definitions:
3 | * Small screen: <= 720px
4 | * Medium screen: > 720px && <= 1024px
5 | * Large screen: > 1024px && <= 1440px
6 | * Large+ screen: > 1440px
7 | */
8 |
9 | $small-screen-breakpoint: 720px;
10 | $medium-screen-breakpoint: 1024px;
11 | $large-screen-breakpoint: 1440px;
12 |
13 | /* Other */
14 | $page-min-width: 320px;
15 |
16 | $dialog-header-height: 72px;
17 | $dialog-header-height-small: 64px;
18 |
--------------------------------------------------------------------------------
/ui/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | margin: 0;
7 | font-family: 'Mazzard', sans-serif;
8 | -webkit-font-smoothing: antialiased;
9 | -moz-osx-font-smoothing: grayscale;
10 | }
11 |
12 | fieldset,
13 | li,
14 | ul,
15 | ol {
16 | all: unset;
17 | }
18 |
19 | a {
20 | display: inline-block;
21 | outline: none;
22 | color: inherit;
23 | text-decoration: none;
24 | }
25 | a:hover {
26 | cursor: pointer;
27 | opacity: 0.7;
28 | }
29 | a:focus-visible {
30 | box-shadow: 0 0 0 2px #000000;
31 | }
32 |
33 | @layer base {
34 | body {
35 | @apply bg-background text-foreground;
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/ui/src/index.tsx:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/react'
2 | import 'nova-ui-kit/dist/style.css'
3 | import ReactDOM from 'react-dom/client'
4 | import { BrowserRouter } from 'react-router-dom'
5 | import { App } from './app'
6 | import './index.css'
7 |
8 | if (process.env.NODE_ENV !== 'development') {
9 | Sentry.init({
10 | dsn: 'https://bdacb11d18ccce7135e11de82e017632@o4503927026876416.ingest.sentry.io/4505909755838464',
11 | environment: process.env.NODE_ENV,
12 | })
13 | }
14 |
15 | const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
16 | root.render(
17 |
18 |
19 |
20 | )
21 |
--------------------------------------------------------------------------------
/ui/src/pages/algorithm-details/styles.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .content {
5 | width: 720px;
6 | max-width: 100%;
7 | box-sizing: border-box;
8 | }
9 |
10 | .dialogTrigger {
11 | @include paragraph-medium();
12 | color: $color-primary-1-600;
13 | font-weight: 600;
14 |
15 | &:hover {
16 | cursor: pointer;
17 | opacity: 0.7;
18 | }
19 |
20 | &:focus-visible {
21 | box-shadow: 0 0 0 2px $color-generic-black;
22 | }
23 | }
24 |
25 | .tableContainer {
26 | max-width: 100%;
27 | overflow: hidden;
28 | }
29 |
30 | .errorContent {
31 | padding: 32px;
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/pages/auth/auth.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import styles from './auth.module.scss'
3 |
4 | export const Auth = ({ children }: { children?: ReactNode }) => (
5 |
6 |
7 |
8 |
12 |
13 |
14 |
{children}
15 |
16 | )
17 |
--------------------------------------------------------------------------------
/ui/src/pages/deployment-details/deployment-details-form/section-location/geo-search/types.ts:
--------------------------------------------------------------------------------
1 | import { MarkerPosition } from 'design-system/map/types'
2 |
3 | export interface ServerSearchResult {
4 | osm_id: number
5 | display_name: string
6 | lat: string
7 | lon: string
8 | }
9 |
10 | export interface SearchResult {
11 | osmId: number
12 | displayName: string
13 | position: MarkerPosition
14 | }
15 |
--------------------------------------------------------------------------------
/ui/src/pages/deployment-details/deployment-details-form/section-location/location-map/location-map.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/variables.scss';
2 | @import 'src/design-system/variables/colors.scss';
3 | @import 'src/design-system/variables/typography.scss';
4 |
5 | .wrapper {
6 | display: flex;
7 | flex-direction: column;
8 | gap: 16px;
9 | }
10 |
11 | .mapControls {
12 | display: flex;
13 | gap: 16px;
14 | justify-content: space-between;
15 | }
16 |
17 | .buttonContainer {
18 | display: flex;
19 | align-items: center;
20 | justify-content: flex-end;
21 | gap: 16px;
22 | }
23 |
24 | @media only screen and (max-width: $small-screen-breakpoint) {
25 | .mapControls {
26 | flex-direction: column;
27 | align-items: flex-start;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ui/src/pages/deployment-details/deployment-details-form/section-source-images/actions/styles.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/variables.scss';
2 | @import 'src/design-system/variables/colors.scss';
3 | @import 'src/design-system/variables/typography.scss';
4 |
5 | .wrapper {
6 | display: flex;
7 | gap: 8px;
8 | margin-top: 8px;
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/pages/deployment-details/deployment-details-form/types.ts:
--------------------------------------------------------------------------------
1 | export enum Section {
2 | General = 'general',
3 | Location = 'location',
4 | SourceImages = 'source-images',
5 | }
6 |
--------------------------------------------------------------------------------
/ui/src/pages/deployment-details/styles.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/variables.scss';
2 | @import 'src/design-system/variables/colors.scss';
3 | @import 'src/design-system/variables/typography.scss';
4 |
5 | .buttonWrapper {
6 | display: flex;
7 | gap: 16px;
8 | position: relative;
9 | }
10 |
11 | .content {
12 | width: 720px;
13 | max-width: 100%;
14 | box-sizing: border-box;
15 |
16 | &.compact {
17 | width: 320px;
18 | }
19 | }
20 |
21 | .section {
22 | margin: 32px 0;
23 | }
24 |
25 | .image {
26 | width: fit-content;
27 | border-radius: 4px;
28 | border: 1px solid $color-neutral-100;
29 | overflow: hidden;
30 |
31 | img {
32 | display: block;
33 | max-width: 100%;
34 | }
35 | }
36 |
37 | @media only screen and (max-width: $small-screen-breakpoint) {
38 | .section {
39 | margin: 16px 0;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/ui/src/pages/deployments/deployments.module.scss:
--------------------------------------------------------------------------------
1 | .deploymentActions {
2 | display: flex;
3 | align-items: center;
4 | justify-content: flex-end;
5 | gap: 8px;
6 | padding: 16px;
7 | }
8 |
--------------------------------------------------------------------------------
/ui/src/pages/export-details/styles.module.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | width: 720px;
3 | max-width: 100%;
4 | box-sizing: border-box;
5 | }
6 |
--------------------------------------------------------------------------------
/ui/src/pages/job-details/job-actions/cancel-job.tsx:
--------------------------------------------------------------------------------
1 | import { useCancelJob } from 'data-services/hooks/jobs/useCancelJob'
2 | import { Button } from 'design-system/components/button/button'
3 | import { IconType } from 'design-system/components/icon/icon'
4 | import { STRING, translate } from 'utils/language'
5 |
6 | export const CancelJob = ({ jobId }: { jobId: string }) => {
7 | const { cancelJob, isLoading, isSuccess } = useCancelJob()
8 |
9 | if (isSuccess) {
10 | return (
11 |
12 | )
13 | }
14 |
15 | return (
16 | cancelJob(jobId)}
20 | />
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/ui/src/pages/job-details/job-actions/queue-job.tsx:
--------------------------------------------------------------------------------
1 | import { useQueueJob } from 'data-services/hooks/jobs/useQueueJob'
2 | import { Button, ButtonTheme } from 'design-system/components/button/button'
3 | import { IconType } from 'design-system/components/icon/icon'
4 | import { STRING, translate } from 'utils/language'
5 |
6 | export const QueueJob = ({ jobId }: { jobId: string }) => {
7 | const { queueJob, isLoading, isSuccess } = useQueueJob()
8 |
9 | if (isSuccess) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | return (
20 | queueJob(jobId)}
25 | />
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/ui/src/pages/job-details/job-actions/retry-job.tsx:
--------------------------------------------------------------------------------
1 | import { useRetryJob } from 'data-services/hooks/jobs/useRetryJob'
2 | import { Button, ButtonTheme } from 'design-system/components/button/button'
3 | import { IconType } from 'design-system/components/icon/icon'
4 | import { STRING, translate } from 'utils/language'
5 |
6 | export const RetryJob = ({ jobId }: { jobId: string }) => {
7 | const { retryJob, isLoading, isSuccess } = useRetryJob()
8 |
9 | if (isSuccess) {
10 | return (
11 |
16 | )
17 | }
18 |
19 | return (
20 | retryJob(jobId)}
25 | />
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/ui/src/pages/job-details/job-details-form/pipelines-picker.tsx:
--------------------------------------------------------------------------------
1 | import { usePipelines } from 'data-services/hooks/pipelines/usePipelines'
2 | import { Select } from 'design-system/components/select/select'
3 | import { useParams } from 'react-router-dom'
4 |
5 | export const PipelinesPicker = ({
6 | value,
7 | onValueChange,
8 | }: {
9 | value?: string
10 | onValueChange: (value?: string) => void
11 | }) => {
12 | const { projectId } = useParams()
13 | const { pipelines = [], isLoading } = usePipelines({
14 | projectId: projectId as string,
15 | })
16 |
17 | return (
18 | ({
21 | value: p.id,
22 | label: p.name,
23 | }))}
24 | value={value}
25 | onValueChange={onValueChange}
26 | />
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/ui/src/pages/job-details/job-details.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/variables.scss';
2 | @import 'src/design-system/variables/colors.scss';
3 | @import 'src/design-system/variables/typography.scss';
4 |
5 | .headerContent {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | gap: 8px;
10 |
11 | .fetchInfoWrapper {
12 | height: 32px;
13 | }
14 | }
15 |
16 | .content {
17 | width: 720px;
18 | max-width: 100%;
19 | }
20 |
21 | .status {
22 | grid-column: span 2;
23 | }
24 |
25 | .jobStageLabel {
26 | position: absolute;
27 | right: 16px;
28 | top: 16px;
29 | z-index: 1;
30 | }
31 |
32 | @media only screen and (max-width: $small-screen-breakpoint) {
33 | .status {
34 | grid-column: auto;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/ui/src/pages/job-details/job-stage-label/job-stage-label.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/variables.scss';
2 | @import 'src/design-system/variables/colors.scss';
3 | @import 'src/design-system/variables/typography.scss';
4 |
5 | .container {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | gap: 8px;
10 | padding: 4px 8px 2px;
11 | border-radius: 2px;
12 | background-color: $color-primary-2-50;
13 | @include paragraph-x-small();
14 | color: $color-neutral-700;
15 | }
16 |
--------------------------------------------------------------------------------
/ui/src/pages/job-details/job-stage-label/job-stage-label.tsx:
--------------------------------------------------------------------------------
1 | import { StatusMarker } from 'design-system/components/status/status-marker/status-marker'
2 | import { Tooltip } from 'design-system/components/tooltip/tooltip'
3 | import styles from './job-stage-label.module.scss'
4 |
5 | export const JobStageLabel = ({
6 | color,
7 | details,
8 | label,
9 | }: {
10 | color: string
11 | details?: string
12 | label: string
13 | }) => (
14 |
15 |
{label}
16 | {details?.length ? (
17 |
18 |
19 |
20 |
21 |
22 | ) : (
23 |
24 | )}
25 |
26 | )
27 |
--------------------------------------------------------------------------------
/ui/src/pages/jobs/jobs.module.scss:
--------------------------------------------------------------------------------
1 | .jobActions {
2 | display: flex;
3 | align-items: center;
4 | justify-content: flex-end;
5 | gap: 8px;
6 | padding: 16px;
7 | }
8 |
9 | .dialogContent {
10 | width: 320px;
11 | max-width: 100%;
12 | box-sizing: border-box;
13 | }
14 |
--------------------------------------------------------------------------------
/ui/src/pages/occurrence-details/identification-card/machine-avatar.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/ui/src/pages/occurrence-details/reject-id/constants.ts:
--------------------------------------------------------------------------------
1 | export const REJECT_OPTIONS = [
2 | { label: 'Not Lepidoptera', value: '11613' },
3 | { label: 'Not identifiable', value: '11614' },
4 | ]
5 |
--------------------------------------------------------------------------------
/ui/src/pages/occurrence-details/reject-id/utils.ts:
--------------------------------------------------------------------------------
1 | import { Taxon } from 'data-services/models/taxa'
2 | import { REJECT_OPTIONS } from './constants'
3 |
4 | export const getCommonRanks = (occurrencesTaxons: Taxon[]) => {
5 | const ranks = occurrencesTaxons.map((occurrenceTaxons) =>
6 | [
7 | ...occurrenceTaxons.ranks,
8 | {
9 | id: occurrenceTaxons.id,
10 | name: occurrenceTaxons.name,
11 | rank: occurrenceTaxons.rank,
12 | },
13 | ].reverse()
14 | )
15 |
16 | const commonRanks = ranks.shift()?.filter((rank1) => {
17 | if (REJECT_OPTIONS.some((o) => o.value === rank1.id)) {
18 | // Filter out options of type reject
19 | return false
20 | }
21 |
22 | return ranks.every((list) => list.some((rank2) => rank1.id === rank2.id))
23 | })
24 |
25 | return commonRanks ?? []
26 | }
27 |
--------------------------------------------------------------------------------
/ui/src/pages/occurrence-details/status-label/status-label.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/variables.scss';
2 | @import 'src/design-system/variables/colors.scss';
3 | @import 'src/design-system/variables/typography.scss';
4 |
5 | .statusLabel {
6 | position: absolute;
7 | top: 8px;
8 | right: 8px;
9 | padding: 4px 8px 2px;
10 | background-color: $color-success-100;
11 | color: $color-success-700;
12 | @include paragraph-xx-small();
13 | font-weight: 600;
14 | text-transform: uppercase;
15 | }
16 |
--------------------------------------------------------------------------------
/ui/src/pages/occurrence-details/status-label/status-label.tsx:
--------------------------------------------------------------------------------
1 | import styles from './status-label.module.scss'
2 |
3 | export const StatusLabel = ({ label }: { label: string }) => (
4 | {label}
5 | )
6 |
--------------------------------------------------------------------------------
/ui/src/pages/occurrence-details/taxonomy-info/taxonomy-info.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/variables.scss';
2 | @import 'src/design-system/variables/colors.scss';
3 | @import 'src/design-system/variables/typography.scss';
4 |
5 | .container {
6 | width: 480px;
7 | overflow: hidden;
8 |
9 | img {
10 | width: 100%;
11 | vertical-align: middle;
12 | border-radius: 4px 4px 0 0;
13 | }
14 | }
15 |
16 | .content {
17 | padding: 24px;
18 |
19 | p {
20 | @include paragraph-small();
21 | color: $color-neutral-700;
22 | margin: 0;
23 | }
24 | }
25 |
26 | @media only screen and (max-width: $small-screen-breakpoint) {
27 | .container {
28 | width: 320px;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/ui/src/pages/occurrence-details/taxonomy-info/taxonomy-info.tsx:
--------------------------------------------------------------------------------
1 | import styles from './taxonomy-info.module.scss'
2 |
3 | const taxonomy = {
4 | image:
5 | 'https://upload.wikimedia.org/wikipedia/commons/thumb/6/60/Meropleon_diversicolor_-_Multicolored_Sedgeminer_Moth_%289810621354%29.jpg/440px-Meropleon_diversicolor_-_Multicolored_Sedgeminer_Moth_%289810621354%29.jpg',
6 | description:
7 | 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum sodales cursus porta. Proin nec quam turpis.',
8 | }
9 |
10 | export const TaxonomyInfo = () => (
11 |
12 |
13 |
14 |
{taxonomy.description}
15 |
16 |
17 | )
18 |
--------------------------------------------------------------------------------
/ui/src/pages/occurrences/occurrences.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .taxonCell {
5 | .taxonCellContent {
6 | display: flex;
7 | flex-direction: column;
8 | gap: 8px;
9 | }
10 |
11 | .taxonActions {
12 | grid-column: 2;
13 | display: flex;
14 | align-items: center;
15 | justify-content: flex-start;
16 | gap: 8px;
17 | }
18 | }
19 |
20 | .scoreCell {
21 | .scoreCellContent {
22 | display: flex;
23 | align-items: center;
24 | justify-content: flex-start;
25 | gap: 12px;
26 | }
27 |
28 | .scoreCellLabel {
29 | display: block;
30 | @include paragraph-medium();
31 | color: $color-neutral-700;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/ui/src/pages/pipeline-details/styles.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .content {
5 | width: 720px;
6 | max-width: 100%;
7 | box-sizing: border-box;
8 | }
9 |
10 | .dialogTrigger {
11 | @include paragraph-medium();
12 | color: $color-primary-1-600;
13 | font-weight: 600;
14 |
15 | &:hover {
16 | cursor: pointer;
17 | opacity: 0.7;
18 | }
19 |
20 | &:focus-visible {
21 | box-shadow: 0 0 0 2px $color-generic-black;
22 | }
23 | }
24 |
25 | .tableContainer {
26 | max-width: 100%;
27 | overflow: hidden;
28 | }
29 |
30 | .errorContent {
31 | padding: 32px;
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/pages/processing-service-details/styles.module.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | width: 720px;
3 | max-width: 100%;
4 | box-sizing: border-box;
5 | }
6 |
7 | .tableContainer {
8 | max-width: 100%;
9 | overflow: hidden;
10 | }
11 |
--------------------------------------------------------------------------------
/ui/src/pages/project-details/styles.module.scss:
--------------------------------------------------------------------------------
1 | .content {
2 | width: 720px;
3 | max-width: 100%;
4 | box-sizing: border-box;
5 |
6 | &.compact {
7 | width: 320px;
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/pages/project/collections/constants.tsx:
--------------------------------------------------------------------------------
1 | // Only some sampling methods are editable from the UI
2 | export const editableSamplingMethods = ['common_combined']
3 |
--------------------------------------------------------------------------------
/ui/src/pages/project/entities/details-form/constants.ts:
--------------------------------------------------------------------------------
1 | import { CollectionDetailsForm } from './collection-details-form'
2 | import { ExportDetailsForm } from './export-details-form'
3 | import { ProcessingServiceDetailsForm } from './processing-service-details-form'
4 | import { StorageDetailsForm } from './storage-details-form'
5 | import { DetailsFormProps } from './types'
6 |
7 | export const customFormMap: {
8 | [key: string]: (props: DetailsFormProps) => JSX.Element
9 | } = {
10 | export: ExportDetailsForm,
11 | storage: StorageDetailsForm,
12 | collection: CollectionDetailsForm,
13 | service: ProcessingServiceDetailsForm,
14 | }
15 |
--------------------------------------------------------------------------------
/ui/src/pages/project/entities/details-form/types.ts:
--------------------------------------------------------------------------------
1 | import { Entity } from 'data-services/models/entity'
2 |
3 | export type DetailsFormProps = {
4 | entity?: Entity
5 | error?: unknown
6 | isLoading?: boolean
7 | isSuccess?: boolean
8 | onSubmit: (
9 | data: FormValues & {
10 | customFields?: { [key: string]: string | number | object | undefined }
11 | }
12 | ) => void
13 | }
14 |
15 | export type FormValues = {
16 | name: string
17 | description?: string
18 | }
19 |
--------------------------------------------------------------------------------
/ui/src/pages/project/entities/devices.tsx:
--------------------------------------------------------------------------------
1 | import { API_ROUTES } from 'data-services/constants'
2 | import { STRING, translate } from 'utils/language'
3 | import { Entities } from './entities'
4 |
5 | export const Devices = () => (
6 |
12 | )
13 |
--------------------------------------------------------------------------------
/ui/src/pages/project/entities/sites.tsx:
--------------------------------------------------------------------------------
1 | import { API_ROUTES } from 'data-services/constants'
2 | import { STRING, translate } from 'utils/language'
3 | import { Entities } from './entities'
4 |
5 | export const Sites = () => (
6 |
12 | )
13 |
--------------------------------------------------------------------------------
/ui/src/pages/project/entities/styles.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .entityActions {
5 | display: flex;
6 | align-items: center;
7 | justify-content: flex-end;
8 | gap: 8px;
9 | padding: 16px;
10 | }
11 |
12 | .dialogContent {
13 | width: 720px;
14 | max-width: 100%;
15 | box-sizing: border-box;
16 |
17 | &.compact {
18 | width: 420px;
19 | }
20 | }
21 |
22 | .dialogTrigger {
23 | @include paragraph-medium();
24 | color: $color-primary-1-600;
25 |
26 | &:hover {
27 | cursor: pointer;
28 | opacity: 0.7;
29 | }
30 |
31 | &:focus-visible {
32 | box-shadow: 0 0 0 2px $color-generic-black;
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/ui/src/pages/project/processing-services/processing-services.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .connectionStatus {
5 | .wizardRoot {
6 | border-width: 1px !important;
7 | }
8 |
9 | .wizardTrigger {
10 | padding: 16px 64px;
11 | background-color: $color-neutral-50;
12 | @include paragraph-small();
13 | font-weight: 600;
14 | }
15 |
16 | .wizardContent {
17 | border-top: 1px solid $color-neutral-70;
18 | padding: 32px 64px;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/ui/src/pages/project/processing-services/status-info/types.ts:
--------------------------------------------------------------------------------
1 | export enum Status {
2 | NotConnected,
3 | Connecting,
4 | Connected,
5 | }
6 |
--------------------------------------------------------------------------------
/ui/src/pages/project/storage/status-info/types.ts:
--------------------------------------------------------------------------------
1 | export enum Status {
2 | NotConnected,
3 | Connecting,
4 | Connected,
5 | }
6 |
--------------------------------------------------------------------------------
/ui/src/pages/project/storage/storage.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .connectionStatus {
5 | .wizardRoot {
6 | border-width: 1px !important;
7 | }
8 |
9 | .wizardTrigger {
10 | padding: 16px 64px;
11 | background-color: $color-neutral-50;
12 | @include paragraph-small();
13 | font-weight: 600;
14 | }
15 |
16 | .wizardContent {
17 | border-top: 1px solid $color-neutral-70;
18 | padding: 32px 64px;
19 | }
20 |
21 | .firstFileFound {
22 | width: 100%;
23 | aspect-ratio: 16/9;
24 | background-color: $color-primary-2-50;
25 | border: 1px solid $color-neutral-100;
26 | border-radius: 4px;
27 | object-fit: contain;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/activity-plot/lazy-activity-plot.module.scss:
--------------------------------------------------------------------------------
1 | .loadingWrapper {
2 | height: 100px;
3 | display: flex;
4 | align-items: center;
5 | justify-content: center;
6 | }
7 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/activity-plot/lazy-activity-plot.tsx:
--------------------------------------------------------------------------------
1 | import { ErrorBoundary } from 'components/error-boundary/error-boundary'
2 | import { LoadingSpinner } from 'design-system/components/loading-spinner/loading-spinner'
3 | import React, { Suspense } from 'react'
4 | import styles from './lazy-activity-plot.module.scss'
5 | import { ActivityPlotProps } from './types'
6 |
7 | const _ActivityPlot = React.lazy(() => import('./activity-plot'))
8 |
9 | export const ActivityPlot = (props: ActivityPlotProps) => (
10 |
13 |
14 |
15 | }
16 | >
17 |
18 | <_ActivityPlot {...props} />
19 |
20 |
21 | )
22 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/activity-plot/types.ts:
--------------------------------------------------------------------------------
1 | import { SessionDetails } from 'data-services/models/session-details'
2 | import { TimelineTick } from 'data-services/models/timeline-tick'
3 |
4 | export interface ActivityPlotProps {
5 | session: SessionDetails
6 | snapToDetections?: boolean
7 | timeline: TimelineTick[]
8 | setActiveCaptureId: (captureId: string) => void
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/activity-plot/useDynamicPlotWidth.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect, useState } from 'react'
2 |
3 | export const useDynamicPlotWidth = (containerRef: RefObject) => {
4 | const [width, setWidth] = useState()
5 |
6 | useEffect(() => {
7 | const updateState = () => {
8 | const container = containerRef.current
9 |
10 | if (container) {
11 | setWidth(container.clientWidth)
12 | }
13 | }
14 |
15 | updateState()
16 |
17 | window.addEventListener('resize', updateState)
18 |
19 | return () => {
20 | window.removeEventListener('resize', updateState)
21 | }
22 | }, [containerRef])
23 |
24 | return width
25 | }
26 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/capture-details/capture-job/capture-job.tsx:
--------------------------------------------------------------------------------
1 | import { CaptureDetails } from 'data-services/models/capture-details'
2 | import { CaptureJobDialog } from './capture-job-dialog'
3 | import { ProcessNow } from './process-now'
4 |
5 | export const CaptureJob = ({
6 | capture,
7 | pipelineId,
8 | }: {
9 | capture?: CaptureDetails
10 | pipelineId?: string
11 | }) => (
12 | <>
13 |
16 | {capture?.jobs.length ? : null}
17 | >
18 | )
19 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/capture-navigation/capture-navigation.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 | @import 'src/design-system/variables/variables.scss';
4 |
5 | .wrapper {
6 | display: flex;
7 | align-items: center;
8 | justify-content: center;
9 | align-self: flex-start;
10 | height: 48px;
11 | gap: 32px;
12 | padding: 0 8px;
13 | border: 1px solid $color-neutral-600;
14 | border-radius: 24px;
15 | box-sizing: border-box;
16 |
17 | span {
18 | @include label();
19 | color: $color-generic-white;
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/frame/types.ts:
--------------------------------------------------------------------------------
1 | export interface BoxStyle {
2 | width: string
3 | height: string
4 | top: string
5 | left: string
6 | }
7 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/useActiveCapture.ts:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'react-router-dom'
2 |
3 | const SEARCH_PARAM_KEY = 'capture'
4 |
5 | export const useActiveCaptureId = (defaultValue?: string) => {
6 | const [searchParams, setSearchParams] = useSearchParams()
7 |
8 | const activeCaptureId = searchParams.get(SEARCH_PARAM_KEY) ?? defaultValue
9 |
10 | const setActiveCaptureId = (captureId: string) => {
11 | searchParams.delete(SEARCH_PARAM_KEY)
12 | searchParams.set(SEARCH_PARAM_KEY, captureId)
13 | setSearchParams(searchParams, { replace: true })
14 | }
15 |
16 | return { activeCaptureId, setActiveCaptureId }
17 | }
18 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/playback/useActiveOccurrences.ts:
--------------------------------------------------------------------------------
1 | import { useCallback } from 'react'
2 | import { useSearchParams } from 'react-router-dom'
3 |
4 | const SEARCH_PARAM_KEY = 'occurrence'
5 |
6 | export const useActiveOccurrences = () => {
7 | const [searchParams, setSearchParams] = useSearchParams()
8 |
9 | const activeOccurrences = searchParams.getAll(SEARCH_PARAM_KEY)
10 |
11 | const setActiveOccurrences = useCallback(
12 | (occurrences: string[]) => {
13 | searchParams.delete(SEARCH_PARAM_KEY)
14 | occurrences.forEach((o) => searchParams.append(SEARCH_PARAM_KEY, o))
15 | setSearchParams(searchParams, { replace: true })
16 | },
17 | [searchParams, setSearchParams]
18 | )
19 |
20 | return { activeOccurrences, setActiveOccurrences }
21 | }
22 |
--------------------------------------------------------------------------------
/ui/src/pages/session-details/session-info/session-info.module.scss:
--------------------------------------------------------------------------------
1 | @import 'src/design-system/variables/colors.scss';
2 | @import 'src/design-system/variables/typography.scss';
3 |
4 | .content {
5 | width: 100%;
6 | display: grid;
7 | grid-template-columns: 1fr 1fr;
8 | column-gap: 16px;
9 | }
10 |
11 | .title {
12 | @include paragraph-large();
13 | font-weight: 600;
14 | color: $color-neutral-700;
15 | margin: 0 0 16px 0;
16 | }
17 |
--------------------------------------------------------------------------------
/ui/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | import '@testing-library/jest-dom'
2 | import { LocalStorageMock } from 'utils/testHelpers'
3 |
4 | Object.defineProperty(window, 'localStorage', { value: new LocalStorageMock() })
5 |
6 | jest.mock('axios')
7 |
--------------------------------------------------------------------------------
/ui/src/utils/cookieConsent/constants.ts:
--------------------------------------------------------------------------------
1 | import { CookieCategory, CookieSettings } from './types'
2 |
3 | export const COOKIE_CONSENT_STORAGE_KEY = 'ami-cookie-consent'
4 |
5 | export const DEFAULT_SETTINGS: CookieSettings = {
6 | [CookieCategory.Necessary]: true,
7 | [CookieCategory.Functionality]: false,
8 | [CookieCategory.Performance]: false,
9 | }
10 |
--------------------------------------------------------------------------------
/ui/src/utils/cookieConsent/types.ts:
--------------------------------------------------------------------------------
1 | export enum CookieCategory {
2 | Necessary = 'necessary',
3 | Functionality = 'functionality',
4 | Performance = 'performance',
5 | }
6 |
7 | export type CookieSettings = { [key in CookieCategory]: boolean }
8 |
9 | export interface CookieConsent {
10 | settings: CookieSettings
11 | accepted?: string
12 | }
13 |
14 | export interface CookieConsentContextValues {
15 | accepted?: string
16 | settings: CookieSettings
17 | setSettings: (settings: CookieSettings) => void
18 | }
19 |
--------------------------------------------------------------------------------
/ui/src/utils/date/getCompactTimespanString/getCompactTimespanString.ts:
--------------------------------------------------------------------------------
1 | import { getFormatedTimeString } from '../getFormatedTimeString/getFormatedTimeString'
2 |
3 | export const getCompactTimespanString = ({
4 | date1,
5 | date2,
6 | locale,
7 | options = {},
8 | }: {
9 | date1: Date
10 | date2: Date
11 | locale?: string
12 | options?: {
13 | second?: boolean
14 | }
15 | }) => {
16 | const time1 = getFormatedTimeString({ date: date1, locale, options })
17 | const time2 = getFormatedTimeString({ date: date2, locale, options })
18 |
19 | if (time1 === time2) {
20 | return time1
21 | }
22 |
23 | return `${time1} - ${time2}`
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/utils/date/getFormatedDateString/getFormatedDateString.ts:
--------------------------------------------------------------------------------
1 | const OPTIONS: Intl.DateTimeFormatOptions = {
2 | day: '2-digit',
3 | month: 'short',
4 | year: 'numeric',
5 | }
6 |
7 | export const getFormatedDateString = ({
8 | date,
9 | locale,
10 | }: {
11 | date: Date
12 | locale?: string
13 | }) => date.toLocaleDateString(locale, OPTIONS)
14 |
--------------------------------------------------------------------------------
/ui/src/utils/date/getFormatedDateTimeString/getFormatedDateTimeString.ts:
--------------------------------------------------------------------------------
1 | import { getFormatedDateString } from '../getFormatedDateString/getFormatedDateString'
2 | import { getFormatedTimeString } from '../getFormatedTimeString/getFormatedTimeString'
3 |
4 | export const getFormatedDateTimeString = ({
5 | date,
6 | locale,
7 | options = {},
8 | }: {
9 | date: Date
10 | locale?: string
11 | options?: {
12 | second?: boolean
13 | }
14 | }) => {
15 | const dateString = getFormatedDateString({ date, locale })
16 | const timeString = getFormatedTimeString({ date, locale, options })
17 |
18 | return `${dateString} ${timeString}`
19 | }
20 |
--------------------------------------------------------------------------------
/ui/src/utils/date/getFormatedTimeString/getFormatedTimeString.ts:
--------------------------------------------------------------------------------
1 | const OPTIONS: Intl.DateTimeFormatOptions = {
2 | hour: '2-digit',
3 | minute: '2-digit',
4 | }
5 |
6 | export const getFormatedTimeString = ({
7 | date,
8 | locale,
9 | options = {},
10 | }: {
11 | date: Date
12 | locale?: string
13 | options?: {
14 | second?: boolean
15 | }
16 | }) =>
17 | date.toLocaleTimeString(locale, {
18 | ...OPTIONS,
19 | ...(options.second ? { second: '2-digit' } : {}),
20 | })
21 |
--------------------------------------------------------------------------------
/ui/src/utils/formContext/types.ts:
--------------------------------------------------------------------------------
1 | import { RefObject } from 'react'
2 |
3 | export type FormState = {
4 | [section: string]: {
5 | isDirty?: boolean
6 | isValid?: boolean
7 | values?: any
8 | }
9 | }
10 |
11 | export interface FormContextValues {
12 | currentSection?: string
13 | formSectionRef?: RefObject
14 | formState: FormState
15 | setCurrentSection: (section: string) => void
16 | setFormSectionStatus: (
17 | section: string,
18 | status: { isValid: boolean; isDirty: boolean }
19 | ) => void
20 | setFormSectionValues: (section: string, values: any) => void
21 | submitFormSection: () => void
22 | }
23 |
--------------------------------------------------------------------------------
/ui/src/utils/isEmpty/isEmpty.test.ts:
--------------------------------------------------------------------------------
1 | import { isEmpty } from './isEmpty'
2 |
3 | describe('isEmpty', () => {
4 | test('returns true if value is empty string, null, or undefined', () => {
5 | const emptyTestCases = ['', ' ', null, undefined]
6 | const results = emptyTestCases.map((testCase) => isEmpty(testCase))
7 | expect(results).not.toContain(false)
8 | })
9 |
10 | test('returns false if value is not empty string, null, or undefined', () => {
11 | const notEmptyTestCases = ['cat', 1, [], {}, false, 0, -1, NaN]
12 | const results = notEmptyTestCases.map((testCase) => isEmpty(testCase))
13 | expect(results).not.toContain(true)
14 | })
15 | })
16 |
--------------------------------------------------------------------------------
/ui/src/utils/isEmpty/isEmpty.ts:
--------------------------------------------------------------------------------
1 | export const isEmpty = (value?: any) =>
2 | value == null || (typeof value === 'string' && value.trim().length === 0)
3 |
--------------------------------------------------------------------------------
/ui/src/utils/numberFormats/index.ts:
--------------------------------------------------------------------------------
1 | export const bytesToMB = (bytes: number) => bytes / (1024 * 1024)
2 |
3 | export const getTotalLabel = (
4 | sampleLength: number,
5 | knownTotal: number | undefined
6 | ) => {
7 | // If the known total provided and is more than the sample length, show the sample length followed by a '+'
8 | return knownTotal
9 | ? `${sampleLength}${sampleLength < knownTotal ? '+' : ''}`
10 | : sampleLength
11 | }
12 |
--------------------------------------------------------------------------------
/ui/src/utils/numberFormats/numberFormats.test.ts:
--------------------------------------------------------------------------------
1 | import { bytesToMB, getTotalLabel } from '.'
2 |
3 | describe('bytesToMB', () => {
4 | test('will convert bytes to MB', () => {
5 | const sizeBytes = 1024 * 1024 * 30
6 | const sizeMB = 30
7 | const result = bytesToMB(sizeBytes)
8 | expect(result).toEqual(sizeMB)
9 | })
10 | })
11 |
12 | describe('getTotalLabel', () => {
13 | test(`will show the sample length followed by a '+' if the known total is more than the sample length`, () => {
14 | const result = getTotalLabel(10, 100)
15 | expect(result).toEqual('10+')
16 | })
17 |
18 | test('will show the sample length if the known total is equal to the sample length', () => {
19 | const result = getTotalLabel(10, 10)
20 | expect(result).toEqual('10')
21 | })
22 | })
23 |
--------------------------------------------------------------------------------
/ui/src/utils/parseServerError/parseServerError.ts:
--------------------------------------------------------------------------------
1 | export const parseServerError = (error: any) => {
2 | let message = ''
3 | const fieldErrors: { key: string; message: string }[] = []
4 |
5 | if (error.response?.data && typeof error.response.data === 'object') {
6 | Object.entries(error.response.data).forEach(([key, details]) => {
7 | if (key && details) {
8 | if (key === 'non_field_errors' || key === 'detail') {
9 | message = details as string
10 | } else {
11 | fieldErrors.push({ key, message: `${(details as string[])[0]}` })
12 | }
13 | }
14 | })
15 | }
16 |
17 | if (!message.length) {
18 | message = error.message ?? 'Something went wrong.'
19 | }
20 |
21 | return { message, fieldErrors }
22 | }
23 |
--------------------------------------------------------------------------------
/ui/src/utils/snakeCaseToSentenceCase.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 |
3 | export const snakeCaseToSentenceCase = (input: string) =>
4 | _.capitalize(input.split('_').join(' '))
5 |
--------------------------------------------------------------------------------
/ui/src/utils/useClientSideSort.ts:
--------------------------------------------------------------------------------
1 | import { TableSortSettings } from 'design-system/components/table/types'
2 | import _ from 'lodash'
3 | import { useEffect, useState } from 'react'
4 |
5 | export const useClientSideSort = ({
6 | items,
7 | defaultSort,
8 | }: {
9 | items?: T[]
10 | defaultSort?: TableSortSettings
11 | }) => {
12 | const [sortedItems, setSortedItems] = useState(items ?? [])
13 | const [sort, setSort] = useState(defaultSort)
14 |
15 | useEffect(() => {
16 | if (sort) {
17 | setSortedItems(_.orderBy(items, sort.field, sort.order))
18 | } else {
19 | setSortedItems(items ?? [])
20 | }
21 | }, [items, sort])
22 |
23 | return { sortedItems, sort, setSort }
24 | }
25 |
--------------------------------------------------------------------------------
/ui/src/utils/useColumnSettings.tsx:
--------------------------------------------------------------------------------
1 | import { useUserPreferences } from './userPreferences/userPreferencesContext'
2 |
3 | export const useColumnSettings = (
4 | tableKey: string,
5 | defaultSettings: { [columnKey: string]: boolean }
6 | ) => {
7 | const { userPreferences, setUserPreferences } = useUserPreferences()
8 |
9 | return {
10 | columnSettings: userPreferences.columnSettings[tableKey] ?? defaultSettings,
11 | setColumnSettings: (settings: { [columnKey: string]: boolean }) => {
12 | setUserPreferences({
13 | ...userPreferences,
14 | columnSettings: {
15 | ...(userPreferences.columnSettings ?? {}),
16 | [tableKey]: settings,
17 | },
18 | })
19 | },
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/ui/src/utils/useDebounce.ts:
--------------------------------------------------------------------------------
1 | import _ from 'lodash'
2 | import { useCallback, useState } from 'react'
3 |
4 | export const useDebounce = (value: T, delay: number): T => {
5 | const [debouncedValue, _setDebouncedValue] = useState(value)
6 | const setDebouncedValue = useCallback(_.debounce(_setDebouncedValue, delay), [
7 | _setDebouncedValue,
8 | ])
9 | setDebouncedValue(value)
10 |
11 | return debouncedValue
12 | }
13 |
--------------------------------------------------------------------------------
/ui/src/utils/usePageBreadcrumb.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from 'react'
2 | import { Breadcrumb, BreadcrumbContext } from './breadcrumbContext'
3 |
4 | export const usePageBreadcrumb = (breadcrumb: Breadcrumb) => {
5 | const { setPageBreadcrumb } = useContext(BreadcrumbContext)
6 |
7 | useEffect(() => {
8 | setPageBreadcrumb(breadcrumb)
9 |
10 | return () => {
11 | setPageBreadcrumb(undefined)
12 | }
13 | }, [])
14 | }
15 |
--------------------------------------------------------------------------------
/ui/src/utils/useSelectedView.ts:
--------------------------------------------------------------------------------
1 | import { useSearchParams } from 'react-router-dom'
2 |
3 | const SEARCH_PARAM_KEY_VIEW = 'view'
4 |
5 | export const useSelectedView = (
6 | defaultValue: string,
7 | key: string = SEARCH_PARAM_KEY_VIEW
8 | ) => {
9 | const [searchParams, setSearchParams] = useSearchParams()
10 | const selectedView = searchParams.get(key) ?? undefined
11 |
12 | const setSelectedView = (selectedView?: string) => {
13 | searchParams.delete(key)
14 |
15 | if (selectedView && selectedView !== defaultValue) {
16 | searchParams.set(key, selectedView ?? defaultValue)
17 | }
18 |
19 | setSearchParams(searchParams)
20 | }
21 |
22 | return {
23 | selectedView: selectedView ?? defaultValue,
24 | setSelectedView,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/ui/src/utils/useSyncSectionStatus.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from 'react'
2 | import { Control, useFormState } from 'react-hook-form'
3 | import { FormContext } from 'utils/formContext/formContext'
4 |
5 | export const useSyncSectionStatus = (
6 | section: string,
7 | control: Control
8 | ) => {
9 | const { isDirty, isValid } = useFormState({ control })
10 | const { setFormSectionStatus } = useContext(FormContext)
11 |
12 | useEffect(() => {
13 | setFormSectionStatus(section, { isDirty, isValid })
14 | }, [isDirty, isValid])
15 | }
16 |
--------------------------------------------------------------------------------
/ui/src/utils/useWindowSize.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | export const useWindowSize = () => {
4 | const [windowSize, setWindowSize] = useState([
5 | Math.max(document.documentElement.clientWidth, window.innerWidth),
6 | Math.max(document.documentElement.clientHeight, window.innerHeight),
7 | ])
8 |
9 | useEffect(() => {
10 | const windowSizeHandler = () =>
11 | setWindowSize([window.innerWidth, window.innerHeight])
12 |
13 | window.addEventListener('resize', windowSizeHandler)
14 |
15 | return () => {
16 | window.removeEventListener('resize', windowSizeHandler)
17 | }
18 | }, [])
19 |
20 | return windowSize
21 | }
22 |
--------------------------------------------------------------------------------
/ui/src/utils/user/constants.ts:
--------------------------------------------------------------------------------
1 | export const AUTH_TOKEN_STORAGE_KEY = 'ami-auth-token'
2 |
--------------------------------------------------------------------------------
/ui/src/utils/user/types.ts:
--------------------------------------------------------------------------------
1 | export type User = {
2 | loggedIn: boolean
3 | token?: string
4 | }
5 |
6 | export type UserInfo = {
7 | email?: string
8 | id: string
9 | image?: string
10 | name?: string
11 | }
12 |
13 | export enum UserPermission {
14 | Cancel = 'cancel', // Custom job permission
15 | Create = 'create',
16 | Delete = 'delete',
17 | Populate = 'populate', // Custom collection permission
18 | Retry = 'retry', // Custom job permission
19 | Run = 'run', // Custom job permission
20 | Star = 'star',
21 | Update = 'update', // Custom capture permission
22 | }
23 |
24 | export interface UserContextValues {
25 | clearToken: () => void
26 | setToken: (token: string, from?: string) => void
27 | user: User
28 | }
29 |
30 | export interface UserInfoContextValues {
31 | userInfo?: UserInfo
32 | }
33 |
--------------------------------------------------------------------------------
/ui/src/utils/user/userInfoContext.tsx:
--------------------------------------------------------------------------------
1 | import { useUserInfo as _useUserInfo } from 'data-services/hooks/auth/useUserInfo'
2 | import { ReactNode, createContext, useContext } from 'react'
3 | import { UserInfoContextValues } from './types'
4 |
5 | export const UserInfoContext = createContext({})
6 |
7 | export const UserInfoContextProvider = ({
8 | children,
9 | }: {
10 | children: ReactNode
11 | }) => {
12 | const { userInfo } = _useUserInfo()
13 |
14 | return (
15 |
20 | {children}
21 |
22 | )
23 | }
24 |
25 | export const useUserInfo = () => useContext(UserInfoContext)
26 |
--------------------------------------------------------------------------------
/ui/src/utils/userPreferences/constants.ts:
--------------------------------------------------------------------------------
1 | import { UserPreferences } from './types'
2 |
3 | export const USER_PREFERENCES_STORAGE_KEY = 'ami-user-preferences'
4 |
5 | export const DEFAULT_PREFERENCES: UserPreferences = {
6 | columnSettings: {},
7 | recentIdentifications: [],
8 | scoreThreshold: 0.6,
9 | termsMessageSeen: false,
10 | }
11 |
--------------------------------------------------------------------------------
/ui/src/utils/userPreferences/types.ts:
--------------------------------------------------------------------------------
1 | export interface UserPreferences {
2 | columnSettings: { [tableKey: string]: { [columnKey: string]: boolean } }
3 | recentIdentifications: {
4 | details?: string
5 | label: string
6 | value: string
7 | }[]
8 | scoreThreshold: number
9 | termsMessageSeen?: boolean
10 | }
11 |
12 | export interface UserPreferencesContextValues {
13 | userPreferences: UserPreferences
14 | setUserPreferences: (userPreferences: UserPreferences) => void
15 | }
16 |
--------------------------------------------------------------------------------
/ui/tailwind.config.js:
--------------------------------------------------------------------------------
1 | import { CONSTANTS } from 'nova-ui-kit'
2 | import { BREAKPOINTS } from './src/components/constants'
3 |
4 | /** @type {import('tailwindcss').Config} */
5 | module.exports = {
6 | content: ['./src/**/*.{ts,tsx}', './node_modules/nova-ui-kit/**/*.js'],
7 | theme: {
8 | colors: CONSTANTS.COLORS,
9 | screens: {
10 | sm: `${BREAKPOINTS.SM}px`,
11 | md: `${BREAKPOINTS.MD}px`,
12 | lg: `${BREAKPOINTS.LG}px`,
13 | xl: `${BREAKPOINTS.XL}px`,
14 | },
15 | extend: {
16 | backgroundImage: CONSTANTS.GRADIENTS,
17 | colors: CONSTANTS.COLOR_THEME,
18 | },
19 | },
20 | plugins: [],
21 | }
22 |
--------------------------------------------------------------------------------
/ui/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "lib": ["dom", "dom.iterable", "esnext"],
5 | "allowJs": true,
6 | "skipLibCheck": true,
7 | "esModuleInterop": true,
8 | "allowSyntheticDefaultImports": true,
9 | "strict": true,
10 | "forceConsistentCasingInFileNames": true,
11 | "noFallthroughCasesInSwitch": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": true,
17 | "jsx": "react-jsx",
18 | "baseUrl": "src",
19 | "types": ["vite/client", "vite-plugin-svgr/client", "node"]
20 | },
21 | "include": ["src", "vite-env.d.ts"]
22 | }
23 |
--------------------------------------------------------------------------------
/ui/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare const __COMMIT_HASH__: string
4 |
5 | declare module '*.md'
6 |
--------------------------------------------------------------------------------