chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
[bandit]
|
||||
# B101 : assert_used
|
||||
# B102 : exec_used
|
||||
# B404 : import_subprocess
|
||||
# B406 : import_xml_sax
|
||||
skips: B101,B102,B404,B406
|
||||
exclude: **/tests/**,tests
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
[run]
|
||||
branch = true
|
||||
|
||||
source =
|
||||
cvat/apps/
|
||||
cvat-sdk/
|
||||
cvat-cli/
|
||||
utils/dataset_manifest
|
||||
|
||||
omit =
|
||||
cvat/settings/*
|
||||
*/tests/*
|
||||
*/test_*
|
||||
*/_test_*
|
||||
*/migrations/*
|
||||
|
||||
[report]
|
||||
# Regexes for lines to exclude from consideration
|
||||
exclude_lines =
|
||||
# Have to re-enable the standard pragma
|
||||
pragma: no cover
|
||||
|
||||
# Don't complain about missing debug-only code:
|
||||
def __repr__
|
||||
if\s+[\w\.()]+\.isEnabledFor\(log\.DEBUG\):
|
||||
|
||||
# Don't complain if tests don't hit defensive assertion code:
|
||||
raise AssertionError
|
||||
raise NotImplementedError
|
||||
|
||||
# Don't complain if non-runnable code isn't run:
|
||||
if 0:
|
||||
if __name__ == .__main__.:
|
||||
|
||||
# don't fail on the code that can be found
|
||||
ignore_errors = true
|
||||
|
||||
skip_empty = true
|
||||
@@ -0,0 +1,18 @@
|
||||
/.git
|
||||
/.env
|
||||
/.vscode
|
||||
/.github
|
||||
/.husky
|
||||
/share
|
||||
/data
|
||||
/media
|
||||
/keys
|
||||
**/node_modules
|
||||
/static
|
||||
/serverless
|
||||
/site
|
||||
/helm-chart
|
||||
/dev
|
||||
/changelog.d
|
||||
/cvat-cli
|
||||
/profiles
|
||||
@@ -0,0 +1,21 @@
|
||||
# EditorConfig helps developers define and maintain consistent
|
||||
# coding styles between different editors and IDEs
|
||||
# editorconfig.org
|
||||
|
||||
root = true
|
||||
|
||||
[*]
|
||||
|
||||
# Change these settings to your own preference
|
||||
indent_style = space
|
||||
indent_size = 4
|
||||
|
||||
# We recommend you to keep these unchanged
|
||||
end_of_line = lf
|
||||
charset = utf-8
|
||||
trim_trailing_whitespace = true
|
||||
insert_final_newline = true
|
||||
|
||||
[*.{yml,yaml}]
|
||||
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright (C) 2018-2022 Intel Corporation
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
browser: true,
|
||||
es2020: true,
|
||||
},
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
parser: '@typescript-eslint/parser',
|
||||
},
|
||||
ignorePatterns: [
|
||||
'.eslintrc.cjs',
|
||||
'lint-staged.config.js',
|
||||
'site/**',
|
||||
'webpack.config.cjs',
|
||||
],
|
||||
plugins: ['@typescript-eslint', '@stylistic', 'security', 'no-unsanitized', 'import'],
|
||||
extends: [
|
||||
'eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM',
|
||||
'airbnb-base', 'plugin:import/errors', 'plugin:import/warnings',
|
||||
'plugin:import/typescript', 'plugin:@typescript-eslint/recommended', 'airbnb-typescript/base',
|
||||
],
|
||||
rules: {
|
||||
// 'header/header': [2, 'line', [{
|
||||
// pattern: ' {1}Copyright \\(C\\) (?:20\\d{2}-)?2022 Intel Corporation',
|
||||
// template: ' Copyright (C) 2022 Intel Corporation'
|
||||
// }, '', ' SPDX-License-Identifier: MIT']],
|
||||
'no-plusplus': 0,
|
||||
'no-continue': 0,
|
||||
'no-console': 0,
|
||||
'no-restricted-syntax': [0, { selector: 'ForOfStatement' }],
|
||||
'no-await-in-loop': 0,
|
||||
'@stylistic/indent': ['error', 4, { 'SwitchCase': 1 }],
|
||||
'max-len': ['error', { code: 120, ignoreStrings: true }],
|
||||
'func-names': 0,
|
||||
'valid-typeof': 0,
|
||||
'quotes': ['error', 'single', { "avoidEscape": true }],
|
||||
'lines-between-class-members': 'off',
|
||||
'@stylistic/lines-between-class-members': 0,
|
||||
'@typescript-eslint/lines-between-class-members': 'off',
|
||||
'class-methods-use-this': 0,
|
||||
'no-underscore-dangle': ['error', { allowAfterThis: true }],
|
||||
'max-classes-per-file': 0,
|
||||
'operator-linebreak': ['error', 'after'],
|
||||
'newline-per-chained-call': 0,
|
||||
'global-require': 0,
|
||||
'arrow-parens': ['error', 'always'],
|
||||
'security/detect-object-injection': 0, // the rule is relevant for user input data on the node.js environment
|
||||
'import/order': ['error', {'groups': ['builtin', 'external', 'internal']}],
|
||||
'import/no-unresolved': 'off',
|
||||
'import/prefer-default-export': 0, // works incorrect with interfaces
|
||||
'no-useless-assignment': 'off',
|
||||
'preserve-caught-error': 'off',
|
||||
|
||||
'react/jsx-indent-props': 0, // new rule, breaks current styling
|
||||
'react/jsx-indent': 0, // new rule, conflicts with eslint@typescript-eslint/indent eslint@indent, breaks current styling
|
||||
'function-paren-newline': 0, // new rule, breaks current styling
|
||||
'@typescript-eslint/default-param-last': 0, // does not really work with redux reducers
|
||||
'@typescript-eslint/ban-ts-comment': 0,
|
||||
'@typescript-eslint/no-explicit-any': 0,
|
||||
'@typescript-eslint/explicit-function-return-type': ['warn', { allowExpressions: true }],
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-empty-object-type': [
|
||||
'error',
|
||||
{
|
||||
allowInterfaces: 'always',
|
||||
allowObjectTypes: 'never',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unsafe-function-type': 'error',
|
||||
'@typescript-eslint/no-restricted-types': [
|
||||
'error',
|
||||
{
|
||||
types: {
|
||||
object: {
|
||||
message: 'Use a more specific object shape, Record<string, unknown>, or unknown instead of object.',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
* text=auto whitespace=trailing-space,space-before-tab,-indent-with-non-tab,tab-in-indent,tabwidth=4
|
||||
|
||||
.git* text export-ignore
|
||||
|
||||
*.txt text
|
||||
*.htm text
|
||||
*.html text
|
||||
*.js text
|
||||
*.py text
|
||||
*.css text
|
||||
*.md text
|
||||
*.yml text
|
||||
Dockerfile text
|
||||
LICENSE text
|
||||
*.conf text
|
||||
*.mimetypes text
|
||||
*.sh text eol=lf
|
||||
|
||||
*.avi binary
|
||||
*.bmp binary
|
||||
*.exr binary
|
||||
*.ico binary
|
||||
*.jpeg binary
|
||||
*.jpg binary
|
||||
*.png binary
|
||||
*.gif binary
|
||||
*.ttf binary
|
||||
*.pdf binary
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
# https://help.github.com/en/github/creating-cloning-and-archiving-repositories/about-code-owners
|
||||
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence, they will
|
||||
# be requested for review when someone opens a pull request.
|
||||
* @nmanovic
|
||||
|
||||
# Order is important; the last matching pattern takes the most
|
||||
# precedence. When someone opens a pull request that only
|
||||
# modifies components below, only the list of owners and not
|
||||
# the global owner(s) will be requested for a review.
|
||||
|
||||
# Component: Server
|
||||
/cvat/ @SpecLad
|
||||
/cvat/apps/dataset_manager/ @zhiltsov-max
|
||||
/cvat/apps/consensus/ @zhiltsov-max
|
||||
/cvat/apps/quality_control/ @zhiltsov-max
|
||||
|
||||
# Component: CVAT SDK/CLI
|
||||
/cvat-sdk/ @SpecLad
|
||||
/cvat/schema.yml @SpecLad
|
||||
/cvat-cli/ @SpecLad
|
||||
|
||||
# Component: Documentation
|
||||
/site/ @bsekachev
|
||||
/CHANGELOG.md @bsekachev
|
||||
/README.md @bsekachev
|
||||
|
||||
# Component: CVAT UI
|
||||
/cvat-ui/ @bsekachev
|
||||
/cvat-data/ @bsekachev
|
||||
/cvat-canvas/ @bsekachev
|
||||
/cvat-canvas3d/ @bsekachev
|
||||
/cvat-core/ @bsekachev
|
||||
|
||||
# Advanced components (e.g. analytics)
|
||||
/components/ @azhavoro
|
||||
|
||||
# Component: Tests
|
||||
/tests/ @bsekachev
|
||||
/tests/python/ @zhiltsov-max
|
||||
|
||||
# Component: Serverless functions
|
||||
/serverless/ @bsekachev
|
||||
|
||||
# Infrastructure
|
||||
Dockerfile* @azhavoro
|
||||
docker-compose* @azhavoro
|
||||
.* @azhavoro
|
||||
*.js @bsekachev
|
||||
*.conf @azhavoro
|
||||
*.sh @azhavoro
|
||||
/supervisord/ @azhavoro
|
||||
/helm-chart/ @azhavoro
|
||||
/utils/ @SpecLad
|
||||
/.github/ @SpecLad
|
||||
/dev/ @SpecLad
|
||||
.regal/ @SpecLad
|
||||
/LICENSE @nmanovic
|
||||
@@ -0,0 +1,65 @@
|
||||
name: "\U0001F41E Bug Report"
|
||||
description: Create a bug report to help us fix it
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Actions before raising this issue
|
||||
options:
|
||||
- label: I searched the existing issues and did not find anything similar.
|
||||
required: true
|
||||
- label: I read/searched [the docs](https://docs.cvat.ai/docs/)
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Provide a link to a live example or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant.
|
||||
placeholder: |
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: A concise description of what you expected to happen.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Possible Solution
|
||||
description: |
|
||||
Not obligatory, but suggest a fix/reason for the bug, or ideas on how to implement the addition or change
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Context
|
||||
description: |
|
||||
How has this issue affected you? What are you trying to accomplish?
|
||||
Providing context helps us come up with a solution that is most useful in the real world!
|
||||
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Environment
|
||||
description: |
|
||||
Include relevant details about the environment you experienced
|
||||
placeholder: |
|
||||
- Git hash commit (`git log -1`):
|
||||
- Docker version `docker version` (e.g. Docker 17.0.05):
|
||||
- Are you using Docker Swarm or Kubernetes?
|
||||
- Operating System and version (e.g. Linux, Windows, MacOS):
|
||||
- Code example or link to GitHub repo or gist to reproduce problem:
|
||||
- Other diagnostic information / logs:
|
||||
<details>
|
||||
<summary>Logs from `cvat` container</summary>
|
||||
</details>
|
||||
render: Markdown
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,41 @@
|
||||
name: "\U0001F680 Feature Request"
|
||||
description: Suggest an idea for this project
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Actions before raising this issue
|
||||
options:
|
||||
- label: I searched the existing issues and did not find anything similar.
|
||||
required: true
|
||||
- label: I read/searched [the docs](https://docs.cvat.ai/docs/)
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Is your feature request related to a problem? Please describe.
|
||||
description: A clear and concise description of what the problem is.
|
||||
placeholder: I'm always frustrated when I have to press this small button.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe the solution you'd like
|
||||
description: A clear and concise description of what you want to happen.
|
||||
placeholder: Make this button bigger
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
placeholder: I wanted to buy bigger display, but it would be too expensive.
|
||||
validations:
|
||||
required: false
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: |
|
||||
Add any other context or screenshots about the feature request here.
|
||||
Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in.
|
||||
validations:
|
||||
required: false
|
||||
@@ -0,0 +1,34 @@
|
||||
<!-- Raise an issue to propose your change (https://github.com/cvat-ai/cvat/issues).
|
||||
It helps to avoid duplication of efforts from multiple independent contributors.
|
||||
Discuss your ideas with maintainers to be sure that changes will be approved and merged.
|
||||
Read the [Contribution guide](https://docs.cvat.ai/docs/contributing/). -->
|
||||
|
||||
<!-- Provide a general summary of your changes in the Title above -->
|
||||
|
||||
### Motivation and context
|
||||
<!-- Why is this change required? What problem does it solve? If it fixes an open
|
||||
issue, please link to the issue here. Describe your changes in detail, add
|
||||
screenshots. -->
|
||||
|
||||
### How has this been tested?
|
||||
<!-- Please describe in detail how you tested your changes.
|
||||
Include details of your testing environment, and the tests you ran to
|
||||
see how your change affects other areas of the code, etc. -->
|
||||
|
||||
### Checklist
|
||||
<!-- Go over all the following points, and put an `x` in all the boxes that apply.
|
||||
If an item isn't applicable for some reason, then ~~explicitly strikethrough~~ the whole
|
||||
line. If you don't do that, GitHub will show incorrect progress for the pull request.
|
||||
If you're unsure about any of these, don't hesitate to ask. We're here to help! -->
|
||||
- [ ] I submit my changes into the `develop` branch
|
||||
- [ ] I have created a changelog fragment <!-- see top comment in CHANGELOG.md -->
|
||||
- [ ] I have updated the documentation accordingly
|
||||
- [ ] I have added tests to cover my changes
|
||||
- [ ] I have linked related issues (see [GitHub docs](
|
||||
https://help.github.com/en/github/managing-your-work-on-github/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword))
|
||||
|
||||
### License
|
||||
|
||||
- [ ] I submit _my code changes_ under the same [MIT License](
|
||||
https://github.com/cvat-ai/cvat/blob/develop/LICENSE) that covers the project.
|
||||
Feel free to contact the maintainers if that's a concern.
|
||||
@@ -0,0 +1,62 @@
|
||||
name: Setup Yarn
|
||||
description: Set up Node.js, enable Yarn via Corepack, cache dependencies, and install packages
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: Node.js version to install
|
||||
required: false
|
||||
default: 22.x
|
||||
node-modules-path:
|
||||
description: Paths to cache for node_modules
|
||||
required: false
|
||||
default: node_modules
|
||||
install-command:
|
||||
description: Command used to install dependencies
|
||||
required: false
|
||||
default: yarn install --immutable
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
|
||||
- name: Enable corepack
|
||||
shell: bash
|
||||
run: corepack enable yarn
|
||||
|
||||
- name: Get yarn cache directory path
|
||||
id: yarn-cache-dir-path
|
||||
shell: bash
|
||||
run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache yarn cache
|
||||
uses: actions/cache@v4
|
||||
id: cache-yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Get yarn version
|
||||
id: yarn-version
|
||||
shell: bash
|
||||
run: echo "version=$(node --version)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
id: cache-node-modules
|
||||
with:
|
||||
path: ${{ inputs.node-modules-path }}
|
||||
key: ${{ runner.os }}-${{ steps.yarn-version.outputs.version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-${{ steps.yarn-version.outputs.version }}-nodemodules-
|
||||
|
||||
- name: Install dependencies
|
||||
if: |
|
||||
steps.cache-yarn-cache.outputs.cache-hit != 'true' ||
|
||||
steps.cache-node-modules.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
run: ${{ inputs.install-command }}
|
||||
@@ -0,0 +1,33 @@
|
||||
comment:
|
||||
layout: "header, diff, components"
|
||||
|
||||
component_management:
|
||||
individual_components:
|
||||
- component_id: cvat-ui
|
||||
name: cvat-ui
|
||||
paths:
|
||||
- cvat-canvas/**
|
||||
- cvat-canvas3d/**
|
||||
- cvat-core/**
|
||||
- cvat-data/**
|
||||
- cvat-ui/**
|
||||
- component_id: cvat-server
|
||||
name: cvat-server
|
||||
paths:
|
||||
- cvat/**
|
||||
- cvat-cli/**
|
||||
- cvat-sdk/**
|
||||
- utils/**
|
||||
|
||||
codecov:
|
||||
require_ci_to_pass: yes
|
||||
notify:
|
||||
wait_for_ci: yes
|
||||
|
||||
coverage:
|
||||
status:
|
||||
patch: false
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 5%
|
||||
@@ -0,0 +1,35 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
- package-ecosystem: "docker"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
|
||||
# Python dependencies in cvat/requirements are managed by pip-compile-multi.
|
||||
# Dependabot should update source *.in files; generated *.txt files are
|
||||
# regenerated by a separate workflow.
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/utils/dataset_manifest"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
exclude-paths:
|
||||
- "*.txt"
|
||||
|
||||
# Dependabot resolves the full CVAT requirements graph for each candidate
|
||||
# update, while generated lock files are refreshed by the workflow below.
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/cvat/requirements"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
open-pull-requests-limit: 10
|
||||
exclude-paths:
|
||||
- "*.txt"
|
||||
- "all.in"
|
||||
@@ -0,0 +1,63 @@
|
||||
name: Cache
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'develop'
|
||||
|
||||
jobs:
|
||||
get-sha:
|
||||
uses: ./.github/workflows/search-cache.yml
|
||||
|
||||
Caching_CVAT:
|
||||
needs: get-sha
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/cache@v6
|
||||
id: server-cache-action
|
||||
with:
|
||||
path: /tmp/cvat_cache_server
|
||||
key: ${{ runner.os }}-build-server-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-server-${{ needs.get-sha.outputs.sha }}
|
||||
${{ runner.os }}-build-server-
|
||||
|
||||
- uses: actions/cache@v6
|
||||
id: ui-cache-action
|
||||
with:
|
||||
path: /tmp/cvat_cache_ui
|
||||
key: ${{ runner.os }}-build-ui-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-ui-${{ needs.get-sha.outputs.sha }}
|
||||
${{ runner.os }}-build-ui-
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Caching CVAT Server
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
cache-from: type=local,src=/tmp/cvat_cache_server
|
||||
cache-to: type=local,dest=/tmp/cvat_cache_server-new
|
||||
|
||||
- name: Caching CVAT UI
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile.ui
|
||||
cache-from: type=local,src=/tmp/cvat_cache_ui
|
||||
cache-to: type=local,dest=/tmp/cvat_cache_ui-new
|
||||
|
||||
- name: Moving cache
|
||||
run: |
|
||||
rm -rf /tmp/cvat_cache_server
|
||||
mv /tmp/cvat_cache_server-new /tmp/cvat_cache_server
|
||||
|
||||
rm -rf /tmp/cvat_cache_ui
|
||||
mv /tmp/cvat_cache_ui-new /tmp/cvat_cache_ui
|
||||
@@ -0,0 +1,72 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "develop", master, release-* ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "develop" ]
|
||||
schedule:
|
||||
- cron: '27 19 * * 4'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript', 'python' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v7
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v4
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
|
||||
# Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs
|
||||
# queries: security-extended,security-and-quality
|
||||
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@v4
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||
|
||||
# If the Autobuild fails above, remove it and uncomment the following three lines.
|
||||
# modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance.
|
||||
|
||||
# - run: |
|
||||
# echo "Run, Build Application using script"
|
||||
# ./location_of_script_within_repo/buildscript.sh
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v4
|
||||
@@ -0,0 +1,89 @@
|
||||
name: Comment
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
env:
|
||||
WORKFLOW_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
|
||||
jobs:
|
||||
verify_author:
|
||||
if: contains(github.event.issue.html_url, '/pull') &&
|
||||
contains(github.event.comment.body, '/check')
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
ref: ${{ steps.get-ref.outputs.ref }}
|
||||
cid: ${{ steps.send-status.outputs.cid }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Check author of comment
|
||||
id: check-author
|
||||
run: |
|
||||
PERM=$(gh api repos/${{ github.repository }}/collaborators/${{ github.event.comment.user.login }}/permission | jq -r '.permission')
|
||||
if [[ $PERM == "write" || $PERM == "maintain" || $PERM == "admin" ]];
|
||||
then
|
||||
ALLOW="true"
|
||||
fi
|
||||
echo "allow=${ALLOW}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Verify that author of comment is collaborator
|
||||
if: steps.check-author.outputs.allow == ''
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
core.setFailed('User that send comment with /check command is not collaborator')
|
||||
|
||||
- name: Get branch name
|
||||
id: get-ref
|
||||
run: |
|
||||
SHA=$(gh api /repos/${{ github.repository }}/pulls/${{ github.event.issue.number }} | jq -r '.head.sha')
|
||||
echo "ref=${SHA}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Send comment. Test are executing
|
||||
id: send-status
|
||||
run: |
|
||||
BODY=":hourglass: Tests are executing, see more information [here](${{ env.WORKFLOW_RUN_URL }})"
|
||||
BODY=$BODY"\n :warning: Cancel [this](${{ env.WORKFLOW_RUN_URL }}) workflow manually first, if you want to restart full check"
|
||||
BODY=$(echo -e $BODY)
|
||||
|
||||
COMMENT_ID=$(gh api --method POST \
|
||||
/repos/${{ github.repository }}/issues/${{ github.event.issue.number }}/comments \
|
||||
-f body="${BODY}" | jq '.id')
|
||||
|
||||
echo "cid=${COMMENT_ID}" >> $GITHUB_OUTPUT
|
||||
|
||||
run-full:
|
||||
needs: verify_author
|
||||
uses: ./.github/workflows/full.yml
|
||||
with:
|
||||
ref: ${{ needs.verify_author.outputs.ref }}
|
||||
|
||||
send_status:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [run-full, verify_author]
|
||||
if: needs.run-full.result != 'skipped' && always()
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
steps:
|
||||
- name: Send status in comments
|
||||
run: |
|
||||
BODY=""
|
||||
|
||||
if [[ "${{ needs.run-full.result }}" == "failure" ]]
|
||||
then
|
||||
BODY=":x: Some checks failed"
|
||||
elif [[ "${{ needs.run-full.result }}" == "success" ]]
|
||||
then
|
||||
BODY=":heavy_check_mark: All checks completed successfully"
|
||||
elif [[ "${{ needs.run-full.result }}" == "cancelled" ]]
|
||||
then
|
||||
BODY=":no_entry_sign: Workflows has been canceled"
|
||||
fi
|
||||
|
||||
BODY=$BODY"\n :page_facing_up: See logs [here](${WORKFLOW_RUN_URL})"
|
||||
BODY=$(echo -e $BODY)
|
||||
|
||||
gh api --method PATCH \
|
||||
/repos/${{ github.repository }}/issues/comments/${{ needs.verify_author.outputs.cid }} \
|
||||
-f body="${BODY}"
|
||||
@@ -0,0 +1,65 @@
|
||||
name: Docs
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'develop'
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
generate_github_pages:
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
submodules: recursive
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Generate CVAT SDK
|
||||
run: |
|
||||
pip3 install --user -r cvat-sdk/gen/requirements.txt
|
||||
./cvat-sdk/gen/generate.sh
|
||||
|
||||
- name: Setup Hugo
|
||||
run: |
|
||||
wget https://github.com/gohugoio/hugo/releases/download/v0.110.0/hugo_extended_0.110.0_Linux-64bit.tar.gz
|
||||
(mkdir hugo_extended_0.110.0_Linux-64bit && tar -xf hugo_extended_0.110.0_Linux-64bit.tar.gz -C hugo_extended_0.110.0_Linux-64bit)
|
||||
|
||||
mkdir hugo
|
||||
cp hugo_extended_0.110.0_Linux-64bit/hugo hugo/hugo-0.110
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- name: Install npm packages
|
||||
working-directory: ./site
|
||||
run: |
|
||||
npm ci
|
||||
|
||||
- name: Build docs
|
||||
run: |
|
||||
pip install -r site/requirements.txt
|
||||
python site/process_sdk_docs.py
|
||||
PATH="$PWD/hugo:$PATH" python site/build_docs.py
|
||||
env:
|
||||
BASE_URL: https://docs.cvat.ai
|
||||
HUGO_ENV: production
|
||||
GOOGLE_TAG_ID: G-GVSBK1DNK5
|
||||
|
||||
- name: Deploy
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./public
|
||||
force_orphan: true
|
||||
cname: docs.cvat.ai
|
||||
@@ -0,0 +1,109 @@
|
||||
name: Finalize release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Discover pending release
|
||||
id: discover
|
||||
env:
|
||||
GH_TOKEN: "${{ github.token }}"
|
||||
run: |
|
||||
gh --repo="${{ github.repository }}" \
|
||||
pr list --base master --state open --json number,headRefName \
|
||||
--jq 'map(select(.headRefName | startswith("release-")))' \
|
||||
> /tmp/release-prs.json
|
||||
|
||||
if jq -e 'length < 1' /tmp/release-prs.json > /dev/null; then
|
||||
echo "No open release pull requests found."
|
||||
exit 1
|
||||
elif jq -e 'length > 1' /tmp/release-prs.json > /dev/null; then
|
||||
echo "Multiple open release pull requests found:"
|
||||
jq -r '.[] | "https://github.com/${{ github.repository }}/pull/\(.number)"' /tmp/release-prs.json
|
||||
exit 1
|
||||
fi
|
||||
|
||||
jq -r '.[] | "prNumber=\(.number)", "version=\(.headRefName | ltrimstr("release-"))"' \
|
||||
/tmp/release-prs.json >> "$GITHUB_OUTPUT"
|
||||
|
||||
# When you use the default github.token to make changes in the repository,
|
||||
# it does not trigger further GitHub pipelines. We want to trigger CI for
|
||||
# the pull request and artifact building for the release, so we have to use
|
||||
# an app token.
|
||||
- name: Generate authentication token
|
||||
id: gen-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
client-id: "${{ secrets.CVAT_BOT_CLIENT_ID }}"
|
||||
private-key: "${{ secrets.CVAT_BOT_PRIVATE_KEY }}"
|
||||
|
||||
- name: Install dependencies
|
||||
run:
|
||||
sudo apt-get install -y pandoc
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: "release-${{ steps.discover.outputs.version }}"
|
||||
|
||||
- name: Verify that the release is new
|
||||
env:
|
||||
NEW_VERSION: "${{ steps.discover.outputs.version }}"
|
||||
run: |
|
||||
if git ls-remote --exit-code origin "refs/tags/v$NEW_VERSION" > /dev/null; then
|
||||
echo "Release v$NEW_VERSION already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Do post-release tasks before publishing the release. If anything goes wrong,
|
||||
# the dev-release-* branch can be deleted, and the whole process restarted again;
|
||||
# whereas we can't unmerge the release PR.
|
||||
|
||||
- name: Create post-release branch
|
||||
run:
|
||||
git checkout -b "dev-release-${{ steps.discover.outputs.version }}"
|
||||
|
||||
- name: Bump version
|
||||
run:
|
||||
./dev/update_version.py --patch
|
||||
|
||||
- name: Commit post-release changes
|
||||
run: |
|
||||
git -c user.name='cvat-bot[bot]' -c user.email='147643061+cvat-bot[bot]@users.noreply.github.com' \
|
||||
commit -a -m "Update ${{ github.ref_name }} after v${{ steps.discover.outputs.version }}"
|
||||
|
||||
- name: Push post-release branch
|
||||
run:
|
||||
git push -u origin "dev-release-${{ steps.discover.outputs.version }}"
|
||||
|
||||
- name: Create post-release pull request
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
|
||||
run: |
|
||||
gh pr create \
|
||||
--base="${{ github.ref_name }}" \
|
||||
--title="Update ${{ github.ref_name }} after v${{ steps.discover.outputs.version }}" \
|
||||
--body=""
|
||||
|
||||
# Now publish the release.
|
||||
|
||||
- name: Merge release pull request
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
|
||||
run:
|
||||
gh pr merge --merge "${{ steps.discover.outputs.prNumber }}" --delete-branch
|
||||
|
||||
- name: Create release
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
|
||||
NEW_VERSION: "${{ steps.discover.outputs.version }}"
|
||||
run: |
|
||||
# We could grab the release notes from the PR description, but it could
|
||||
# be outdated if any changes were made on the release branch. So instead,
|
||||
# just re-extract them from the changelog again.
|
||||
|
||||
./dev/gh_release_notes.sh \
|
||||
| gh release create "v$NEW_VERSION" \
|
||||
--target=master \
|
||||
--title="v$NEW_VERSION" \
|
||||
--notes-file=-
|
||||
@@ -0,0 +1,354 @@
|
||||
name: Full
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: true
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
ref:
|
||||
type: string
|
||||
required: true
|
||||
|
||||
env:
|
||||
WORKFLOW_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
CYPRESS_VERIFY_TIMEOUT: 180000 # https://docs.cypress.io/guides/guides/command-line#cypress-verify
|
||||
CVAT_VERSION: "local"
|
||||
|
||||
jobs:
|
||||
search_cache:
|
||||
uses: ./.github/workflows/search-cache.yml
|
||||
|
||||
build:
|
||||
needs: search_cache
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: CVAT server. Getting cache from the default branch
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: /tmp/cvat_cache_server
|
||||
key: ${{ runner.os }}-build-server-${{ needs.search_cache.outputs.sha }}
|
||||
|
||||
- name: CVAT UI. Getting cache from the default branch
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: /tmp/cvat_cache_ui
|
||||
key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Create artifact directories
|
||||
run: |
|
||||
mkdir /tmp/cvat_server
|
||||
mkdir /tmp/cvat_ui
|
||||
mkdir /tmp/cvat_sdk
|
||||
|
||||
- name: CVAT server. Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
cache-from: type=local,src=/tmp/cvat_cache_server
|
||||
context: .
|
||||
file: Dockerfile
|
||||
tags: cvat/server:${{ env.CVAT_VERSION }}
|
||||
outputs: type=docker,dest=/tmp/cvat_server/image.tar
|
||||
|
||||
- name: CVAT UI. Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
cache-from: type=local,src=/tmp/cvat_cache_ui
|
||||
context: .
|
||||
file: Dockerfile.ui
|
||||
tags: cvat/ui:${{ env.CVAT_VERSION }}
|
||||
outputs: type=docker,dest=/tmp/cvat_ui/image.tar
|
||||
|
||||
- name: CVAT SDK. Build
|
||||
run: |
|
||||
pip3 install --user -r cvat-sdk/gen/requirements.txt
|
||||
./cvat-sdk/gen/generate.sh
|
||||
|
||||
cp -r cvat-sdk/* /tmp/cvat_sdk/
|
||||
|
||||
- name: Upload CVAT server artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/image.tar
|
||||
|
||||
- name: Upload CVAT UI artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/image.tar
|
||||
|
||||
- name: Upload CVAT SDK artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_sdk
|
||||
path: /tmp/cvat_sdk/
|
||||
|
||||
rest_api_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Download CVAT UI images
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/
|
||||
|
||||
- name: Download CVAT SDK package
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_sdk
|
||||
path: /tmp/cvat_sdk/
|
||||
|
||||
- name: Load Docker images
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker load --input /tmp/cvat_ui/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- name: Verify API schema
|
||||
id: verify_schema
|
||||
run: |
|
||||
docker run --rm cvat/server:${CVAT_VERSION} bash \
|
||||
-c 'python manage.py spectacular' > cvat/schema-expected.yml
|
||||
|
||||
if ! git diff --no-index cvat/schema.yml cvat/schema-expected.yml; then
|
||||
echo
|
||||
echo 'API schema has changed! Please update cvat/schema.yml:'
|
||||
echo
|
||||
echo ' docker run --rm cvat/server:dev bash \'
|
||||
echo " -c 'python manage.py spectacular' > cvat/schema.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify migrations
|
||||
run: |
|
||||
docker run --rm cvat/server:${CVAT_VERSION} bash \
|
||||
-c 'python manage.py makemigrations --check'
|
||||
|
||||
- name: Generate SDK
|
||||
run: |
|
||||
pip3 install -r cvat-sdk/gen/requirements.txt
|
||||
./cvat-sdk/gen/generate.sh
|
||||
|
||||
- name: Install SDK
|
||||
run: |
|
||||
pip3 install -r ./tests/python/requirements.txt \
|
||||
-e './cvat-sdk[masks,pytorch]' -e ./cvat-cli \
|
||||
--extra-index-url https://download.pytorch.org/whl/cpu
|
||||
|
||||
- name: Running REST API and SDK tests
|
||||
id: run_tests
|
||||
run: |
|
||||
pytest tests/python/
|
||||
CVAT_ALLOW_STATIC_CACHE="true" pytest -k "TestTaskData" tests/python
|
||||
|
||||
- name: Creating a log file from cvat containers
|
||||
if: failure() && steps.run_tests.conclusion == 'failure'
|
||||
env:
|
||||
LOGS_DIR: "${{ github.workspace }}/rest_api_testing"
|
||||
run: |
|
||||
mkdir $LOGS_DIR
|
||||
docker logs test_cvat_server_1 > $LOGS_DIR/cvat_server.log
|
||||
docker logs test_cvat_worker_export_1 > $LOGS_DIR/cvat_worker_export.log
|
||||
docker logs test_cvat_worker_import_1 > $LOGS_DIR/cvat_worker_import.log
|
||||
docker logs test_cvat_opa_1 2> $LOGS_DIR/cvat_opa.log
|
||||
|
||||
- name: Uploading "cvat" container logs as an artifact
|
||||
if: failure() && steps.run_tests.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: rest_api_container_logs
|
||||
path: "${{ github.workspace }}/rest_api_testing"
|
||||
|
||||
unit_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Load Docker server image
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- name: Running OPA tests
|
||||
run: |
|
||||
python cvat/apps/iam/rules/tests/generate_tests.py
|
||||
|
||||
docker compose run --rm -v "$PWD:/mnt/src:ro" -w /mnt/src \
|
||||
cvat_opa test cvat/apps/*/rules
|
||||
|
||||
- name: Running unit tests
|
||||
env:
|
||||
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
|
||||
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
|
||||
run: |
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d cvat_opa cvat_server cvat_db
|
||||
|
||||
max_tries=12
|
||||
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml \
|
||||
run --env PYTHONDEVMODE=1 cvat_ci /bin/bash \
|
||||
-c 'python manage.py test cvat/apps -v 2'
|
||||
|
||||
- name: Creating a log file from cvat containers
|
||||
if: failure()
|
||||
env:
|
||||
LOGS_DIR: "${{ github.workspace }}/unit_testing"
|
||||
run: |
|
||||
mkdir $LOGS_DIR
|
||||
docker logs cvat_server > $LOGS_DIR/cvat_server.log
|
||||
docker logs cvat_opa 2> $LOGS_DIR/cvat_opa.log
|
||||
|
||||
- name: Uploading "cvat" container logs as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unit_tests_container_logs
|
||||
path: "${{ github.workspace }}/unit_testing"
|
||||
|
||||
e2e_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_tasks4',
|
||||
'actions_objects', 'actions_objects2', 'actions_users',
|
||||
'actions_projects_models', 'canvas3d_functionality',
|
||||
'issues_prs', 'issues_prs2', 'features', 'features2',
|
||||
'audio_workspace']
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ inputs.ref }}
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Download CVAT UI image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/
|
||||
|
||||
- name: Load Docker images
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker load --input /tmp/cvat_ui/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- name: Run CVAT instance
|
||||
run: |
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.dev.yml \
|
||||
-f components/serverless/docker-compose.serverless.yml \
|
||||
-f tests/docker-compose.minio.yml \
|
||||
-f tests/docker-compose.file_share.yml up -d
|
||||
|
||||
- name: Waiting for server
|
||||
env:
|
||||
API_ABOUT_PAGE: "localhost:8080/api/server/about"
|
||||
run: |
|
||||
max_tries=60
|
||||
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
|
||||
while [[ $status_code != "200" && max_tries -gt 0 ]]
|
||||
do
|
||||
echo Number of attempts left: $max_tries
|
||||
echo Status code of response: $status_code
|
||||
|
||||
sleep 5
|
||||
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
|
||||
(( max_tries-- ))
|
||||
done
|
||||
|
||||
- name: Run E2E tests
|
||||
env:
|
||||
DJANGO_SU_NAME: 'admin'
|
||||
DJANGO_SU_EMAIL: 'admin@localhost.company'
|
||||
DJANGO_SU_PASSWORD: '12qwaszx'
|
||||
run: |
|
||||
docker exec -i cvat_server /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
|
||||
cd ./tests
|
||||
corepack enable yarn
|
||||
yarn --immutable
|
||||
|
||||
if [[ ${{ matrix.specs }} == canvas3d_* ]]; then
|
||||
npx cypress run \
|
||||
--headed \
|
||||
--browser chrome \
|
||||
--env coverage=false \
|
||||
--config-file cypress_canvas3d.config.js \
|
||||
--spec 'cypress/e2e/setup/setup_canvas3d.js,cypress/e2e/canvas3d_*/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
|
||||
elif [[ ${{ matrix.specs }} == audio_* ]]; then
|
||||
npx cypress run \
|
||||
--headed \
|
||||
--browser chrome \
|
||||
--env coverage=false \
|
||||
--config-file cypress_audio.config.js \
|
||||
--spec 'cypress/e2e/setup/setup_audio.js,cypress/e2e/audio_workspace/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
|
||||
else
|
||||
npx cypress run \
|
||||
--browser chrome \
|
||||
--env coverage=false \
|
||||
--spec 'cypress/e2e/setup/setup.js,cypress/e2e/setup/setup_project.js,cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
|
||||
fi
|
||||
|
||||
- name: Creating a log file from "cvat" container logs
|
||||
if: failure()
|
||||
run: |
|
||||
docker logs cvat_server > ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
|
||||
|
||||
- name: Uploading "cvat" container logs as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: e2e_container_logs_${{ matrix.specs }}
|
||||
path: ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
|
||||
|
||||
- name: Uploading cypress screenshots as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cypress_screenshots_${{ matrix.specs }}
|
||||
path: ${{ github.workspace }}/tests/cypress/screenshots
|
||||
@@ -0,0 +1,68 @@
|
||||
name: Generate Allure Report
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
secrets:
|
||||
AWS_ALLURE_REPORTS_ROLE:
|
||||
required: true
|
||||
env:
|
||||
S3_BUCKET: cvat-allure-reports
|
||||
|
||||
jobs:
|
||||
generate_report:
|
||||
name: Generate Allure Report and Upload to S3
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v7
|
||||
|
||||
- name: Install Allure CLI (.deb)
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y default-jre-headless
|
||||
wget https://github.com/allure-framework/allure2/releases/download/2.34.0/allure_2.34.0-1_all.deb
|
||||
sudo dpkg -i allure_2.34.0-1_all.deb
|
||||
|
||||
- name: Configure AWS Credentials
|
||||
uses: aws-actions/configure-aws-credentials@v6
|
||||
with:
|
||||
role-to-assume: ${{ secrets.AWS_ALLURE_REPORTS_ROLE }}
|
||||
aws-region: eu-west-1
|
||||
|
||||
- name: Set a timestamp
|
||||
id: timestampid
|
||||
run: |
|
||||
echo "timestamp=$(date --utc +%Y%m%d_%H%M%SZ)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Download Allure results from artifacts
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
pattern: allure-results*
|
||||
merge-multiple: true
|
||||
path: merged-allure-results
|
||||
|
||||
- name: Download previous history from S3
|
||||
run: aws s3 cp s3://${{ env.S3_BUCKET }}/history/ ./merged-allure-results/history --recursive || echo "No history found"
|
||||
|
||||
- name: Generate Allure Report
|
||||
run: allure generate ./merged-allure-results --clean -o allure-report
|
||||
|
||||
- name: Backup updated history to S3
|
||||
run: aws s3 cp ./allure-report/history s3://${{ env.S3_BUCKET }}/history/ --recursive
|
||||
|
||||
- name: Deploy report to S3 (timestamped folder)
|
||||
run: aws s3 cp ./allure-report s3://${{ env.S3_BUCKET }}/report/${{ steps.timestampid.outputs.timestamp }}/ --recursive
|
||||
|
||||
- name: Update latest report alias
|
||||
run: |
|
||||
aws s3 rm s3://${{ env.S3_BUCKET }}/report/latest/ --recursive || true
|
||||
aws s3 cp ./allure-report s3://${{ env.S3_BUCKET }}/report/latest/ --recursive
|
||||
|
||||
- name: Write report URL to summary
|
||||
run: |
|
||||
echo "### 🧪 [Allure Report](http://${{ env.S3_BUCKET }}.s3-website.eu-west-1.amazonaws.com/report/${{ steps.timestampid.outputs.timestamp }}/index.html)" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -0,0 +1,189 @@
|
||||
name: linters
|
||||
on: pull_request
|
||||
jobs:
|
||||
bandit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
pipx install $(grep "^bandit" ./dev/requirements.txt)
|
||||
|
||||
echo "Bandit version: "$(bandit --version | head -1)
|
||||
bandit -a file --ini .bandit --recursive .
|
||||
|
||||
black:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
pipx install $(grep "^black" ./dev/requirements.txt)
|
||||
|
||||
echo "Black version: $(black --version)"
|
||||
|
||||
black --check --diff .
|
||||
|
||||
hadolint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Run checks
|
||||
env:
|
||||
HADOLINT: '${{ github.workspace }}/hadolint'
|
||||
HADOLINT_VER: 2.12.0
|
||||
VERIFICATION_LEVEL: error
|
||||
run: |
|
||||
curl -sLo "$HADOLINT" "https://github.com/hadolint/hadolint/releases/download/v$HADOLINT_VER/hadolint-Linux-x86_64"
|
||||
chmod +x "$HADOLINT"
|
||||
echo "hadolint version: $("$HADOLINT" --version)"
|
||||
git ls-files -z 'Dockerfile*' '*/Dockerfile*' | xargs -0 "$HADOLINT" -t "$VERIFICATION_LEVEL"
|
||||
|
||||
isort:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
pipx install $(grep "^isort" ./dev/requirements.txt)
|
||||
|
||||
echo "isort version: $(isort --version-number)"
|
||||
|
||||
isort --check --diff --resolve-all-configs .
|
||||
|
||||
pylint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- id: setup-pipx
|
||||
shell: bash
|
||||
run: |
|
||||
# custom cached dir
|
||||
cache_home_dir="$HOME/.local/pipx"
|
||||
cache_bin_dir="$HOME/.local/pipx_bin"
|
||||
|
||||
# add cached dir to PATH
|
||||
echo "$cache_bin_dir" >> $GITHUB_PATH
|
||||
|
||||
echo "cache_home_dir=$cache_home_dir" >> $GITHUB_OUTPUT
|
||||
echo "cache_bin_dir=$cache_bin_dir" >> $GITHUB_OUTPUT
|
||||
echo "version=$(pipx --version)" >> $GITHUB_OUTPUT
|
||||
echo "python_version=$(python3 --version)" >> $GITHUB_OUTPUT
|
||||
|
||||
- id: cache-pipx
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
${{ steps.setup-pipx.outputs.cache_home_dir }}
|
||||
${{ steps.setup-pipx.outputs.cache_bin_dir }}
|
||||
# cache based on pipx version, python version, and both requirements files
|
||||
# nb! we need to keep python version in the key because packages might be not compatible between python versions
|
||||
key: pipx-pylint-${{ steps.setup-pipx.outputs.version}}-${{ steps.setup-pipx.outputs.python_version}}-${{ hashFiles('./dev/requirements.txt', './cvat/requirements/base.txt') }}
|
||||
|
||||
- name: Install pylint
|
||||
if: steps.cache-pipx.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
env:
|
||||
# nb! install in our custom cached dir, using /opt will not work because of permission issues
|
||||
PIPX_HOME: ${{ steps.setup-pipx.outputs.cache_home_dir }}
|
||||
PIPX_BIN_DIR: ${{ steps.setup-pipx.outputs.cache_bin_dir }}
|
||||
run: |
|
||||
pipx install $(grep "^pylint==" ./dev/requirements.txt)
|
||||
|
||||
pipx inject pylint \
|
||||
$(grep "^pylint-.\+==" ./dev/requirements.txt) \
|
||||
$(grep "^django==" ./cvat/requirements/base.txt)
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
echo "Pylint version: "$(pylint --version | head -1)
|
||||
pylint -j0 .
|
||||
|
||||
regal:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup Regal
|
||||
uses: open-policy-agent/setup-regal@761188c3b435761fa254beca508a44875619648f # v2.0.0
|
||||
with:
|
||||
version: v0.21.3
|
||||
|
||||
- run: regal lint --format=github cvat/apps/*/rules
|
||||
|
||||
spellcheck:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
pipx install $(grep "^typos==" ./dev/requirements.txt)
|
||||
|
||||
echo "Typos version: $(typos --version )"
|
||||
typos
|
||||
|
||||
eslint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: ./.github/actions/setup-yarn
|
||||
with:
|
||||
node-version: 22.x
|
||||
node-modules-path: |
|
||||
node_modules
|
||||
tests/node_modules
|
||||
install-command: |
|
||||
yarn install --immutable
|
||||
(cd tests && yarn install --immutable)
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
echo "ESLint version: "$(yarn run eslint --version)
|
||||
yarn run eslint .
|
||||
|
||||
typescript:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: ./.github/actions/setup-yarn
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Run TypeScript checks for cvat frontend code
|
||||
run: yarn run type-check
|
||||
|
||||
remark:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: ./.github/actions/setup-yarn
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
echo "Remark version: "`npx remark --version`
|
||||
npx remark --quiet --frail -i .remarkignore .
|
||||
|
||||
stylelint:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: ./.github/actions/setup-yarn
|
||||
with:
|
||||
node-version: 22.x
|
||||
|
||||
- name: Run checks
|
||||
run: |
|
||||
echo "StyleLint version: "$(yarn run stylelint --version)
|
||||
yarn run stylelint '**/*.css' '**/*.scss'
|
||||
@@ -0,0 +1,625 @@
|
||||
name: CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'develop'
|
||||
pull_request:
|
||||
types: [ready_for_review, opened, synchronize, reopened]
|
||||
paths-ignore:
|
||||
- 'site/**'
|
||||
- '**/*.md'
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CYPRESS_VERIFY_TIMEOUT: 180000 # https://docs.cypress.io/guides/guides/command-line#cypress-verify
|
||||
CVAT_VERSION: "local"
|
||||
|
||||
jobs:
|
||||
search_cache:
|
||||
if: |
|
||||
github.event.pull_request.draft == false &&
|
||||
!startsWith(github.event.pull_request.title, '[WIP]') &&
|
||||
!startsWith(github.event.pull_request.title, '[Dependent]')
|
||||
uses: ./.github/workflows/search-cache.yml
|
||||
|
||||
build:
|
||||
needs: search_cache
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Verify version consistency
|
||||
run: ./dev/update_version.py --verify-current
|
||||
|
||||
- name: Check changelog fragments
|
||||
run: ./dev/check_changelog_fragments.py
|
||||
|
||||
- name: CVAT server. Getting cache from the default branch
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: /tmp/cvat_cache_server
|
||||
key: ${{ runner.os }}-build-server-${{ needs.search_cache.outputs.sha }}
|
||||
|
||||
- name: CVAT UI. Getting cache from the default branch
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: /tmp/cvat_cache_ui
|
||||
key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Create artifact directories
|
||||
run: |
|
||||
mkdir /tmp/cvat_server
|
||||
mkdir /tmp/cvat_ui
|
||||
mkdir /tmp/cvat_sdk
|
||||
|
||||
- name: CVAT server. Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
build-args: |
|
||||
"COVERAGE_PROCESS_START=.coveragerc"
|
||||
cache-from: type=local,src=/tmp/cvat_cache_server
|
||||
context: .
|
||||
file: Dockerfile
|
||||
tags: cvat/server:${{ env.CVAT_VERSION }}
|
||||
outputs: type=docker,dest=/tmp/cvat_server/image.tar
|
||||
|
||||
- name: Instrumentation of the code then rebuilding the CVAT UI
|
||||
run: |
|
||||
corepack enable yarn
|
||||
yarn --immutable
|
||||
yarn run coverage
|
||||
|
||||
- name: CVAT UI. Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
cache-from: type=local,src=/tmp/cvat_cache_ui
|
||||
context: .
|
||||
file: Dockerfile.ui
|
||||
tags: cvat/ui:${{ env.CVAT_VERSION }}
|
||||
outputs: type=docker,dest=/tmp/cvat_ui/image.tar
|
||||
|
||||
- name: CVAT SDK. Build
|
||||
run: |
|
||||
pip3 install --user -r cvat-sdk/gen/requirements.txt
|
||||
./cvat-sdk/gen/generate.sh
|
||||
|
||||
cp -r cvat-sdk/* /tmp/cvat_sdk/
|
||||
|
||||
- name: Verify API schema
|
||||
id: verify_schema
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker run --rm "cvat/server:${CVAT_VERSION}" bash \
|
||||
-c 'python manage.py spectacular' > cvat/schema-expected.yml
|
||||
|
||||
if ! git diff --no-index cvat/schema.yml cvat/schema-expected.yml; then
|
||||
echo
|
||||
echo 'API schema has changed! Please update cvat/schema.yml:'
|
||||
echo
|
||||
echo ' docker run --rm cvat/server:dev bash \'
|
||||
echo " -c 'python manage.py spectacular' > cvat/schema.yml"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Verify migrations
|
||||
run: |
|
||||
docker run --rm "cvat/server:${CVAT_VERSION}" bash \
|
||||
-c 'python manage.py makemigrations --check'
|
||||
|
||||
- name: Verify redis migration filenames
|
||||
run: |
|
||||
docker run --rm "cvat/server:${CVAT_VERSION}" bash \
|
||||
-c 'python manage.py checkredismigrations'
|
||||
|
||||
- name: Upload CVAT server artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/image.tar
|
||||
|
||||
- name: Upload CVAT UI artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/image.tar
|
||||
|
||||
- name: Upload CVAT SDK artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_sdk
|
||||
path: /tmp/cvat_sdk/
|
||||
|
||||
rest_api_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- pattern: "test_task_data"
|
||||
allow_cache: 'true'
|
||||
- pattern: "test_task_data"
|
||||
allow_cache: 'no'
|
||||
- pattern: "not test_task_data"
|
||||
allow_cache: 'no'
|
||||
- pattern: "not test_task_data"
|
||||
allow_cache: 'true'
|
||||
name: rest_api_testing [${{ matrix.pattern }}, cache-${{ matrix.allow_cache }}]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Download CVAT UI images
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/
|
||||
|
||||
- name: Load Docker images
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker load --input /tmp/cvat_ui/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- name: Generate SDK
|
||||
run: |
|
||||
pip3 install -r cvat-sdk/gen/requirements.txt
|
||||
./cvat-sdk/gen/generate.sh
|
||||
|
||||
- name: Install SDK
|
||||
run: |
|
||||
pip3 install -r ./tests/python/requirements.txt \
|
||||
-e './cvat-sdk[masks,pytorch]' -e ./cvat-cli \
|
||||
--extra-index-url https://download.pytorch.org/whl/cpu
|
||||
|
||||
- name: Run REST API and SDK tests
|
||||
id: run_tests
|
||||
env:
|
||||
COVERAGE_PROCESS_START: ".coveragerc"
|
||||
CVAT_ALLOW_STATIC_CACHE: ${{ matrix.allow_cache }}
|
||||
run: |
|
||||
pytest tests/python/ -k "${{ matrix.pattern }}" --cov --cov-report=json --alluredir=tests/python/allure-results
|
||||
for COVERAGE_FILE in `find -name "coverage*.json" -type f -printf "%f\n"`; do mv ${COVERAGE_FILE} "${COVERAGE_FILE%%.*}_0.json"; done
|
||||
|
||||
|
||||
- name: Uploading code coverage results as an artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{format(
|
||||
'coverage_results_rest_api-{0}-cache-{1}',
|
||||
matrix.pattern, matrix.allow_cache
|
||||
)}}
|
||||
path: |
|
||||
coverage*.json
|
||||
|
||||
- name: Creating a log file from cvat containers
|
||||
if: failure() && steps.run_tests.conclusion == 'failure'
|
||||
env:
|
||||
LOGS_DIR: "${{ github.workspace }}/rest_api_testing"
|
||||
run: |
|
||||
mkdir $LOGS_DIR
|
||||
docker logs test_cvat_server_1 > $LOGS_DIR/cvat_server.log
|
||||
docker logs test_cvat_worker_export_1 > $LOGS_DIR/cvat_worker_export.log
|
||||
docker logs test_cvat_worker_import_1 > $LOGS_DIR/cvat_worker_import.log
|
||||
docker logs test_cvat_worker_annotation_1 > $LOGS_DIR/cvat_worker_annotation.log
|
||||
docker logs test_cvat_opa_1 2> $LOGS_DIR/cvat_opa.log
|
||||
# all remaining workers (any one can fail)
|
||||
docker logs test_cvat_worker_utils_1 > $LOGS_DIR/cvat_worker_utils.log
|
||||
docker logs test_cvat_worker_annotation_1 > $LOGS_DIR/cvat_worker_annotation.log
|
||||
docker logs test_cvat_worker_quality_reports_1 > $LOGS_DIR/cvat_worker_quality_reports.log
|
||||
docker logs test_cvat_worker_consensus_1 > $LOGS_DIR/cvat_worker_consensus.log
|
||||
docker logs test_cvat_worker_webhooks_1 > $LOGS_DIR/cvat_worker_webhooks.log
|
||||
docker logs test_cvat_worker_export_1 > $LOGS_DIR/cvat_worker_export.log
|
||||
docker logs test_cvat_redis_inmem_1 > $LOGS_DIR/cvat_redis_inmem.log
|
||||
docker logs test_cvat_redis_ondisk_1 > $LOGS_DIR/cvat_redis_ondisk.log
|
||||
|
||||
|
||||
|
||||
- name: Creating a log file from cvat containers
|
||||
if: failure() && steps.run_tests.conclusion == 'failure'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{format(
|
||||
'rest_api_container_logs-{0}-cache-{1}',
|
||||
matrix.pattern, matrix.allow_cache
|
||||
)}}
|
||||
path: "${{ github.workspace }}/rest_api_testing"
|
||||
|
||||
- name: Upload allure results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: ${{format(
|
||||
'allure-results-tests-api-{0}-cache-{1}',
|
||||
matrix.pattern,matrix.allow_cache
|
||||
)}}
|
||||
path: tests/python/allure-results
|
||||
|
||||
unit_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Load Docker server image
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- name: Running OPA tests
|
||||
run: |
|
||||
python cvat/apps/iam/rules/tests/generate_tests.py
|
||||
|
||||
docker compose run --rm -v "$PWD:/mnt/src:ro" -w /mnt/src \
|
||||
cvat_opa test cvat/apps/*/rules
|
||||
|
||||
- name: Running unit tests
|
||||
env:
|
||||
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
|
||||
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
|
||||
run: |
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d cvat_opa cvat_server cvat_db
|
||||
|
||||
max_tries=12
|
||||
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml \
|
||||
run --env PYTHONDEVMODE=1 cvat_ci /bin/bash \
|
||||
-c 'coverage run -a manage.py test -v 2 cvat/apps && coverage json && mv coverage.json ${CONTAINER_COVERAGE_DATA_DIR}/unit_tests_coverage.json'
|
||||
|
||||
- name: Uploading code coverage results as an artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage_results_unit_tests
|
||||
path: |
|
||||
${{ github.workspace }}/coverage-final.json
|
||||
${{ github.workspace }}/unit_tests_coverage.json
|
||||
|
||||
- name: Creating a log file from cvat containers
|
||||
if: failure()
|
||||
env:
|
||||
LOGS_DIR: "${{ github.workspace }}/unit_testing"
|
||||
run: |
|
||||
mkdir $LOGS_DIR
|
||||
docker logs cvat_server > $LOGS_DIR/cvat_server.log
|
||||
docker logs cvat_opa 2> $LOGS_DIR/cvat_opa.log
|
||||
|
||||
- name: Uploading "cvat" container logs as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: unit_tests_container_logs
|
||||
path: "${{ github.workspace }}/unit_testing"
|
||||
|
||||
e2e_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_tasks4',
|
||||
'actions_objects', 'actions_objects2', 'actions_users',
|
||||
'actions_projects_models', 'canvas3d_functionality',
|
||||
'issues_prs', 'issues_prs2', 'features', 'features2',
|
||||
'audio_workspace']
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
# NOTE: corepack is not bundled after Node 25
|
||||
# https://nodejs.org/docs/v22.18.0/api/corepack.html#corepack
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Download CVAT UI image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/
|
||||
|
||||
- name: Load Docker images
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker load --input /tmp/cvat_ui/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- name: Run CVAT instance
|
||||
run: |
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.dev.yml \
|
||||
-f components/serverless/docker-compose.serverless.yml \
|
||||
-f tests/docker-compose.minio.yml \
|
||||
-f tests/docker-compose.file_share.yml up -d
|
||||
|
||||
- name: Waiting for server
|
||||
env:
|
||||
API_ABOUT_PAGE: "localhost:8080/api/server/about"
|
||||
run: |
|
||||
max_tries=60
|
||||
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
|
||||
while [[ $status_code != "200" && max_tries -gt 0 ]]
|
||||
do
|
||||
echo Number of attempts left: $max_tries
|
||||
echo Status code of response: $status_code
|
||||
|
||||
sleep 5
|
||||
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
|
||||
(( max_tries-- ))
|
||||
done
|
||||
|
||||
- name: Run E2E tests
|
||||
env:
|
||||
DJANGO_SU_NAME: 'admin'
|
||||
DJANGO_SU_EMAIL: 'admin@localhost.company'
|
||||
DJANGO_SU_PASSWORD: '12qwaszx'
|
||||
|
||||
run: |
|
||||
docker exec -i cvat_server /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
|
||||
cd ./tests
|
||||
corepack enable yarn
|
||||
yarn --immutable
|
||||
|
||||
if [[ ${{ matrix.specs }} == canvas3d_* ]]; then
|
||||
npx cypress run \
|
||||
--headed \
|
||||
--browser chrome \
|
||||
--config-file cypress_canvas3d.config.js \
|
||||
--spec 'cypress/e2e/setup/setup_canvas3d.js,cypress/e2e/canvas3d_*/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
|
||||
elif [[ ${{ matrix.specs }} == audio_* ]]; then
|
||||
npx cypress run \
|
||||
--headed \
|
||||
--browser chrome \
|
||||
--config-file cypress_audio.config.js \
|
||||
--spec 'cypress/e2e/setup/setup_audio.js,cypress/e2e/audio_workspace/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
|
||||
else
|
||||
npx cypress run \
|
||||
--browser chrome \
|
||||
--spec 'cypress/e2e/setup/setup.js,cypress/e2e/setup/setup_project.js,cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
|
||||
fi
|
||||
mv coverage/coverage-final.json coverage/${{ matrix.specs }}_coverage.json
|
||||
|
||||
- name: Uploading code coverage results as an artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: coverage_results_e2e_${{ matrix.specs }}
|
||||
path: |
|
||||
tests/coverage/${{ matrix.specs }}_coverage.json
|
||||
|
||||
- name: Creating a log file from "cvat" container logs
|
||||
if: failure()
|
||||
run: |
|
||||
docker logs cvat_server > ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
|
||||
docker logs cvat_worker_export > ${{ github.workspace }}/tests/cvat_worker_export_${{ matrix.specs }}.log
|
||||
docker logs cvat_worker_import > ${{ github.workspace }}/tests/cvat_worker_import_${{ matrix.specs }}.log
|
||||
|
||||
- name: Uploading "cvat" container logs as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: e2e_container_logs_${{ matrix.specs }}
|
||||
path: ${{ github.workspace }}/tests/cvat_${{ matrix.specs }}.log
|
||||
|
||||
- name: Uploading cypress screenshots as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cypress_screenshots_${{ matrix.specs }}
|
||||
path: ${{ github.workspace }}/tests/cypress/screenshots
|
||||
|
||||
- name: Uploading cypress videos as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cypress_videos_${{ matrix.specs }}
|
||||
path: ${{ github.workspace }}/tests/cypress/videos
|
||||
|
||||
- name: Upload Allure results as an artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: allure-results-e2e-${{ matrix.specs }}
|
||||
path: tests/allure-results-e2e
|
||||
|
||||
helm_rest_api_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Start minikube
|
||||
uses: medyagh/setup-minikube@latest
|
||||
with:
|
||||
cpus: max
|
||||
memory: max
|
||||
|
||||
- name: Try the cluster
|
||||
run: kubectl get pods -A
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Download CVAT UI images
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/
|
||||
|
||||
- name: Load images
|
||||
run: |
|
||||
eval $(minikube -p minikube docker-env)
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker load --input /tmp/cvat_ui/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- uses: azure/setup-helm@v5
|
||||
|
||||
- name: Update Helm chart dependencies
|
||||
working-directory: helm-chart
|
||||
run: |
|
||||
helm dependency update
|
||||
|
||||
- name: Deploy to minikube
|
||||
run: |
|
||||
printf " service:\n externalIPs:\n - $(minikube ip)\n" >> helm-chart/test.values.yaml
|
||||
helm upgrade release-${{ github.run_id }}-${{ github.run_attempt }} --install helm-chart \
|
||||
-f helm-chart/cvat.values.yaml \
|
||||
-f helm-chart/test.values.yaml \
|
||||
--set cvat.backend.tag=${{ env.CVAT_VERSION }} \
|
||||
--set cvat.frontend.tag=${{ env.CVAT_VERSION }}
|
||||
|
||||
- name: Update test config
|
||||
run: |
|
||||
sed -i -e 's$http://localhost:8080$http://cvat.local:80$g' tests/python/shared/utils/config.py
|
||||
find tests/python/shared/assets/ -type f -name '*.json' | xargs sed -i -e 's$http://localhost:8080$http://cvat.local$g'
|
||||
echo "$(minikube ip) cvat.local" | sudo tee -a /etc/hosts
|
||||
|
||||
- name: Generate SDK
|
||||
run: |
|
||||
pip3 install --user -r cvat-sdk/gen/requirements.txt
|
||||
./cvat-sdk/gen/generate.sh
|
||||
|
||||
- name: Install test requirements
|
||||
run: |
|
||||
pip3 install --user cvat-sdk/ cvat-cli/ -r tests/python/requirements.txt
|
||||
|
||||
- name: Wait for CVAT to be ready
|
||||
run: |
|
||||
max_tries=60
|
||||
while [[ $(kubectl get pods -l component=server -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" && max_tries -gt 0 ]]; do echo "waiting for CVAT pod" && (( max_tries-- )) && sleep 5; done
|
||||
while [[ $(kubectl get pods -l app.kubernetes.io/name=postgresql -o 'jsonpath={..status.conditions[?(@.type=="Ready")].status}') != "True" && max_tries -gt 0 ]]; do echo "waiting for DB pod" && (( max_tries-- )) && sleep 5; done
|
||||
while [[ $(curl -s -o /tmp/server_response -w "%{http_code}" cvat.local/api/server/about) != "200" && max_tries -gt 0 ]]; do echo "waiting for CVAT" && (( max_tries-- )) && sleep 5; done
|
||||
kubectl get pods
|
||||
kubectl logs $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}')
|
||||
|
||||
- name: REST API and SDK tests
|
||||
# We don't have external services in Helm tests, so we ignore corresponding cases
|
||||
# They are still tested without Helm
|
||||
run: |
|
||||
kubectl cp tests/mounted_file_share $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}'):/home/django/share/
|
||||
kubectl exec $(kubectl get pods -l component=server -o jsonpath='{.items[0].metadata.name}') -- bash -c "mv /home/django/share/mounted_file_share/* /home/django/share/ && rm -r /home/django/share/mounted_file_share"
|
||||
pytest --timeout 30 --platform=kube -m "not with_external_services" tests/python --log-level INFO
|
||||
|
||||
- name: Creating a log file from "cvat" container logs
|
||||
if: failure()
|
||||
env:
|
||||
LOGS_DIR: "${{ github.workspace }}/rest_api_testing"
|
||||
run: |
|
||||
mkdir ${LOGS_DIR}
|
||||
for backend_pod in $(kubectl get pods -l tier=backend -o 'jsonpath={.items[*].metadata.name}'); do
|
||||
kubectl logs ${backend_pod} >${LOGS_DIR}/${backend_pod}.log
|
||||
done
|
||||
kubectl logs $(kubectl get pods -l app.kubernetes.io/name=traefik -o 'jsonpath={.items[0].metadata.name}') >${LOGS_DIR}/traefik.log
|
||||
|
||||
- name: Uploading "cvat" container logs as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: helm_rest_api_container_logs
|
||||
path: "${{ github.workspace }}/rest_api_testing"
|
||||
|
||||
publish_dev_images:
|
||||
if: github.ref == 'refs/heads/develop'
|
||||
needs: [rest_api_testing, unit_testing, e2e_testing]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Download CVAT server images
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Download CVAT UI images
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/
|
||||
|
||||
- name: Load Docker images
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker load --input /tmp/cvat_ui/image.tar
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Push to Docker Hub
|
||||
env:
|
||||
SERVER_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/server
|
||||
UI_IMAGE_REPO: ${{ secrets.DOCKERHUB_WORKSPACE }}/ui
|
||||
run: |
|
||||
docker tag "cvat/server:${CVAT_VERSION}" "${SERVER_IMAGE_REPO}:dev"
|
||||
docker push "${SERVER_IMAGE_REPO}:dev"
|
||||
|
||||
docker tag "cvat/ui:${CVAT_VERSION}" "${UI_IMAGE_REPO}:dev"
|
||||
docker push "${UI_IMAGE_REPO}:dev"
|
||||
|
||||
generate_report:
|
||||
name: Generate Allure Report and Upload to S3
|
||||
needs: [rest_api_testing, e2e_testing]
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/develop' && github.repository == 'cvat.ai/cvat'
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read
|
||||
uses: ./.github/workflows/generate-allure-report.yml
|
||||
secrets:
|
||||
AWS_ALLURE_REPORTS_ROLE: ${{ secrets.AWS_ALLURE_REPORTS_ROLE }}
|
||||
|
||||
codecov:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [unit_testing, e2e_testing, rest_api_testing]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Merge coverage artifacts
|
||||
uses: actions/upload-artifact/merge@v7
|
||||
with:
|
||||
name: coverage_results
|
||||
pattern: coverage_results_*
|
||||
delete-merged: true
|
||||
|
||||
- name: Downloading coverage results
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: coverage_results
|
||||
|
||||
- name: Upload coverage reports to Codecov with GitHub Action
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
@@ -0,0 +1,76 @@
|
||||
name: Prepare release
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
newVersion:
|
||||
description: "Version number for the new release"
|
||||
required: true
|
||||
default: X.Y.Z
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Validate version number
|
||||
env:
|
||||
NEW_VERSION: "${{ inputs.newVersion }}"
|
||||
run: |
|
||||
if ! [[ "$NEW_VERSION" =~ [0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
||||
echo "Invalid version number"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# When you use the default github.token to make changes in the repository,
|
||||
# it does not trigger further GitHub pipelines. We want to trigger CI for
|
||||
# the pull request, so we have to use an app token.
|
||||
- name: Generate authentication token
|
||||
id: gen-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
client-id: "${{ secrets.CVAT_BOT_CLIENT_ID }}"
|
||||
private-key: "${{ secrets.CVAT_BOT_PRIVATE_KEY }}"
|
||||
|
||||
- name: Install dependencies
|
||||
run:
|
||||
sudo apt-get install -y pandoc
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Verify that the release is new
|
||||
run: |
|
||||
if git ls-remote --exit-code origin refs/tags/v${{ inputs.newVersion }} > /dev/null; then
|
||||
echo "Release v${{ inputs.newVersion }} already exists"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create release branch
|
||||
run:
|
||||
git checkout -b "release-${{ inputs.newVersion }}"
|
||||
|
||||
- name: Collect changelog
|
||||
run: |
|
||||
if ls changelog.d/*.md >/dev/null 2>&1; then
|
||||
pipx run scriv collect --version='${{ inputs.newVersion }}'
|
||||
fi
|
||||
|
||||
- name: Set the new version
|
||||
run:
|
||||
./dev/update_version.py --set="${{ inputs.newVersion }}"
|
||||
|
||||
- name: Commit release preparation changes
|
||||
run: |
|
||||
git -c user.name='cvat-bot[bot]' -c user.email='147643061+cvat-bot[bot]@users.noreply.github.com' \
|
||||
commit -a -m "Prepare release v${{ inputs.newVersion }}"
|
||||
|
||||
- name: Push release branch
|
||||
run:
|
||||
git push -u origin "release-${{ inputs.newVersion }}"
|
||||
|
||||
- name: Create release pull request
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
|
||||
run: |
|
||||
./dev/gh_release_notes.sh \
|
||||
| gh pr create \
|
||||
--base=master \
|
||||
--title="Release v${{ inputs.newVersion }}" \
|
||||
--body-file=-
|
||||
@@ -0,0 +1,84 @@
|
||||
name: Publish artifacts
|
||||
on:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
docker-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Build images
|
||||
run: |
|
||||
CVAT_VERSION=latest CLAM_AV=yes docker compose -f docker-compose.yml -f docker-compose.dev.yml build
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Push to Docker Hub
|
||||
env:
|
||||
DOCKERHUB_WORKSPACE: ${{ secrets.DOCKERHUB_WORKSPACE }}
|
||||
SERVER_IMAGE_REPO: 'server'
|
||||
UI_IMAGE_REPO: 'ui'
|
||||
run: |
|
||||
docker tag "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
|
||||
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:${{ github.event.release.tag_name }}"
|
||||
docker push "${DOCKERHUB_WORKSPACE}/${SERVER_IMAGE_REPO}:latest"
|
||||
|
||||
docker tag "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:latest" "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:${{ github.event.release.tag_name }}"
|
||||
docker push "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:${{ github.event.release.tag_name }}"
|
||||
docker push "${DOCKERHUB_WORKSPACE}/${UI_IMAGE_REPO}:latest"
|
||||
|
||||
python-packages:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Generate SDK
|
||||
run: |
|
||||
pip3 install --user -r cvat-sdk/gen/requirements.txt
|
||||
./cvat-sdk/gen/generate.sh
|
||||
|
||||
- name: Build packages
|
||||
run: |
|
||||
for d in cvat-sdk cvat-cli; do
|
||||
pipx run --spec=build pyproject-build --outdir=dist "$d"
|
||||
done
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
|
||||
helm:
|
||||
runs-on: ubuntu-latest
|
||||
needs: docker-images
|
||||
env:
|
||||
DOCKERHUB_WORKSPACE: ${{ secrets.DOCKERHUB_WORKSPACE }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Set up Helm
|
||||
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
|
||||
with:
|
||||
version: v3.19.0
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Package Helm chart
|
||||
working-directory: helm-chart
|
||||
run: |
|
||||
helm package . --dependency-update --destination ../
|
||||
|
||||
- name: Push Helm chart to Docker Hub
|
||||
run: |
|
||||
helm push cvat-*.tgz oci://registry-1.docker.io/${DOCKERHUB_WORKSPACE}
|
||||
@@ -0,0 +1,317 @@
|
||||
name: CI-nightly
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 22 * * *'
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
CYPRESS_VERIFY_TIMEOUT: 180000 # https://docs.cypress.io/guides/guides/command-line#cypress-verify
|
||||
CVAT_VERSION: "local"
|
||||
|
||||
jobs:
|
||||
check_updates:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
REPO: ${{ github.repository }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
outputs:
|
||||
last_commit_time: ${{ steps.check_updates.outputs.last_commit_time }}
|
||||
last_night_time: ${{ steps.check_updates.outputs.last_night_time }}
|
||||
steps:
|
||||
- id: check_updates
|
||||
run: |
|
||||
default_branch=$(gh api /repos/$REPO | jq -r '.default_branch')
|
||||
|
||||
last_commit_date=$(gh api /repos/${REPO}/branches/${default_branch} | jq -r '.commit.commit.author.date')
|
||||
|
||||
last_night_date=$(gh api /repos/${REPO}/actions/workflows/schedule.yml/runs | \
|
||||
jq -r '.workflow_runs[]? | select((.status == "completed")) | .updated_at' \
|
||||
| sort | tail -1)
|
||||
|
||||
last_night_time=$(date +%s -d $last_night_date)
|
||||
last_commit_time=$(date +%s -d $last_commit_date)
|
||||
|
||||
echo Last CI-nightly workflow run time: $last_night_date
|
||||
echo Last commit time in develop branch: $last_commit_date
|
||||
|
||||
echo "last_commit_time=${last_commit_time}" >> $GITHUB_OUTPUT
|
||||
echo "last_night_time=${last_night_time}" >> $GITHUB_OUTPUT
|
||||
|
||||
search_cache:
|
||||
needs: check_updates
|
||||
uses: ./.github/workflows/search-cache.yml
|
||||
|
||||
build:
|
||||
needs: search_cache
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: CVAT server. Getting cache from the default branch
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: /tmp/cvat_cache_server
|
||||
key: ${{ runner.os }}-build-server-${{ needs.search_cache.outputs.sha }}
|
||||
|
||||
- name: CVAT UI. Getting cache from the default branch
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: /tmp/cvat_cache_ui
|
||||
key: ${{ runner.os }}-build-ui-${{ needs.search_cache.outputs.sha }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Create artifact directories
|
||||
run: |
|
||||
mkdir /tmp/cvat_server
|
||||
mkdir /tmp/cvat_ui
|
||||
mkdir /tmp/cvat_sdk
|
||||
|
||||
- name: CVAT server. Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
cache-from: type=local,src=/tmp/cvat_cache_server
|
||||
context: .
|
||||
file: Dockerfile
|
||||
tags: cvat/server:${{ env.CVAT_VERSION }}
|
||||
outputs: type=docker,dest=/tmp/cvat_server/image.tar
|
||||
|
||||
- name: CVAT UI. Build and push
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
cache-from: type=local,src=/tmp/cvat_cache_ui
|
||||
context: .
|
||||
file: Dockerfile.ui
|
||||
tags: cvat/ui:${{ env.CVAT_VERSION }}
|
||||
outputs: type=docker,dest=/tmp/cvat_ui/image.tar
|
||||
|
||||
- name: Upload CVAT server artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/image.tar
|
||||
|
||||
- name: Upload CVAT UI artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/image.tar
|
||||
|
||||
unit_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Download CVAT UI images
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/
|
||||
|
||||
- name: Load Docker images
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker load --input /tmp/cvat_ui/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- name: OPA tests
|
||||
run: |
|
||||
python cvat/apps/iam/rules/tests/generate_tests.py
|
||||
|
||||
docker compose run --rm -v "$PWD:/mnt/src:ro" -w /mnt/src \
|
||||
cvat_opa test cvat/apps/*/rules
|
||||
|
||||
- name: Generate SDK
|
||||
run: |
|
||||
pip3 install -r cvat-sdk/gen/requirements.txt
|
||||
./cvat-sdk/gen/generate.sh
|
||||
|
||||
- name: Install test requirements
|
||||
run: |
|
||||
pip3 install -e ./cvat-sdk -e ./cvat-cli -r ./tests/python/requirements.txt
|
||||
|
||||
- name: REST API and SDK tests
|
||||
run: |
|
||||
echo 'Run 1: without cache'
|
||||
pytest tests/python/ --alluredir=tests/python/allure-results
|
||||
pytest tests/python/ --stop-services
|
||||
|
||||
echo 'Run 2: with cache'
|
||||
CVAT_ALLOW_STATIC_CACHE="true" pytest tests/python
|
||||
pytest tests/python/ --stop-services
|
||||
|
||||
- name: Upload allure results for REST API tests
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: allure-results-rest-api
|
||||
path: tests/python/allure-results
|
||||
|
||||
- name: Unit tests
|
||||
env:
|
||||
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}
|
||||
CONTAINER_COVERAGE_DATA_DIR: "/coverage_data"
|
||||
run: |
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d cvat_opa cvat_server cvat_db
|
||||
max_tries=12
|
||||
while [[ $(curl -s -o /dev/null -w "%{http_code}" localhost:8181/health?bundles) != "200" && max_tries -gt 0 ]]; do (( max_tries-- )); sleep 5; done
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml \
|
||||
run --env PYTHONDEVMODE=1 cvat_ci /bin/bash \
|
||||
-c 'python manage.py test cvat/apps -v 2'
|
||||
|
||||
docker compose -f docker-compose.yml -f docker-compose.dev.yml -f docker-compose.ci.yml down -v
|
||||
|
||||
e2e_testing:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
specs: ['actions_tasks', 'actions_tasks2', 'actions_tasks3', 'actions_tasks4',
|
||||
'actions_objects', 'actions_objects2', 'actions_users',
|
||||
'actions_projects_models', 'canvas3d_functionality',
|
||||
'issues_prs', 'issues_prs2', 'features']
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: '22.x'
|
||||
|
||||
- name: Download CVAT server image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_server
|
||||
path: /tmp/cvat_server/
|
||||
|
||||
- name: Download CVAT UI image
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: cvat_ui
|
||||
path: /tmp/cvat_ui/
|
||||
|
||||
- name: Load Docker images
|
||||
run: |
|
||||
docker load --input /tmp/cvat_server/image.tar
|
||||
docker load --input /tmp/cvat_ui/image.tar
|
||||
docker image ls -a
|
||||
|
||||
- name: Run CVAT instance
|
||||
run: |
|
||||
docker compose \
|
||||
-f docker-compose.yml \
|
||||
-f docker-compose.dev.yml \
|
||||
-f tests/docker-compose.file_share.yml \
|
||||
-f tests/docker-compose.minio.yml \
|
||||
-f components/serverless/docker-compose.serverless.yml up -d
|
||||
|
||||
- name: Waiting for server
|
||||
id: wait-server
|
||||
env:
|
||||
API_ABOUT_PAGE: "localhost:8080/api/server/about"
|
||||
run: |
|
||||
max_tries=60
|
||||
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
|
||||
while [[ $status_code != "200" && max_tries -gt 0 ]]
|
||||
do
|
||||
echo Number of attempts left: $max_tries
|
||||
echo Status code of response: $status_code
|
||||
|
||||
sleep 5
|
||||
status_code=$(curl -s -o /tmp/server_response -w "%{http_code}" ${API_ABOUT_PAGE})
|
||||
(( max_tries-- ))
|
||||
done
|
||||
|
||||
if [[ $status_code != "200" ]]; then
|
||||
echo Response from server is incorrect, output:
|
||||
cat /tmp/server_response
|
||||
fi
|
||||
echo "status_code=${status_code}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Fail on bad response from server
|
||||
if: steps.wait-server.outputs.status_code != '200'
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
core.setFailed('Workflow failed: incorrect response from server. See logs artifact to get more info')
|
||||
|
||||
- name: Add user for tests
|
||||
env:
|
||||
DJANGO_SU_NAME: "admin"
|
||||
DJANGO_SU_EMAIL: "admin@localhost.company"
|
||||
DJANGO_SU_PASSWORD: "12qwaszx"
|
||||
run: |
|
||||
docker exec -i cvat_server /bin/bash -c "echo \"from django.contrib.auth.models import User; User.objects.create_superuser('${DJANGO_SU_NAME}', '${DJANGO_SU_EMAIL}', '${DJANGO_SU_PASSWORD}')\" | python3 ~/manage.py shell"
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
cd ./tests
|
||||
corepack enable yarn
|
||||
yarn --immutable
|
||||
|
||||
shopt -s extglob
|
||||
if [[ ${{ matrix.specs }} == canvas3d_* ]]; then
|
||||
npx cypress run \
|
||||
--headed \
|
||||
--browser chrome \
|
||||
--config-file cypress_canvas3d.config.js \
|
||||
--spec 'cypress/e2e/setup/setup_canvas3d.js,cypress/e2e/canvas3d_*/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
|
||||
else
|
||||
npx cypress run \
|
||||
--browser chrome \
|
||||
--spec 'cypress/e2e/setup/setup.js,cypress/e2e/setup/setup_project.js,cypress/e2e/${{ matrix.specs }}/**/*.js,cypress/e2e/remove_users_tasks_projects_organizations.js'
|
||||
fi
|
||||
|
||||
- name: Creating a log file from "cvat" container logs
|
||||
if: failure()
|
||||
run: |
|
||||
docker logs cvat_server > ${{ github.workspace }}/tests/cvat.log
|
||||
|
||||
- name: Uploading cypress screenshots as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cypress_screenshots_${{ matrix.specs }}
|
||||
path: ${{ github.workspace }}/tests/cypress/screenshots
|
||||
|
||||
- name: Uploading cypress videos as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cypress_videos_${{ matrix.specs }}
|
||||
path: ${{ github.workspace }}/tests/cypress/videos
|
||||
|
||||
- name: Uploading "cvat" container logs as an artifact
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: cvat_container_logs_${{ matrix.specs }}
|
||||
path: ${{ github.workspace }}/tests/cvat.log
|
||||
- name: Upload Allure results as an artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: allure-results-e2e-${{ matrix.specs }}
|
||||
path: tests/allure-results-e2e
|
||||
|
||||
generate_report:
|
||||
name: Generate Allure Report and Upload to S3
|
||||
needs: [unit_testing, e2e_testing]
|
||||
if: always()
|
||||
|
||||
uses: ./.github/workflows/generate-allure-report.yml
|
||||
secrets:
|
||||
AWS_ALLURE_REPORTS_ROLE: ${{ secrets.AWS_ALLURE_REPORTS_ROLE }}
|
||||
@@ -0,0 +1,40 @@
|
||||
name: Search Cache
|
||||
on:
|
||||
workflow_call:
|
||||
outputs:
|
||||
sha:
|
||||
value: ${{ jobs.search_cache.outputs.sha }}
|
||||
|
||||
# if: |
|
||||
# github.event.pull_request.draft == false &&
|
||||
# !startsWith(github.event.pull_request.title, '[WIP]') &&
|
||||
# !startsWith(github.event.pull_request.title, '[Dependent]')
|
||||
|
||||
jobs:
|
||||
search_cache:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
sha: ${{ steps.get-sha.outputs.sha}}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
REPO: ${{ github.repository }}
|
||||
steps:
|
||||
- name: Getting SHA with cache from the default branch
|
||||
id: get-sha
|
||||
run: |
|
||||
DEFAULT_BRANCH=$(gh api /repos/$REPO | jq -r '.default_branch')
|
||||
for sha in $(gh api "/repos/$REPO/commits?per_page=100&sha=$DEFAULT_BRANCH" | jq -r '.[].sha');
|
||||
do
|
||||
RUN_status=$(gh api /repos/${REPO}/actions/workflows/cache.yml/runs | \
|
||||
jq -r ".workflow_runs[]? | select((.head_sha == \"${sha}\") and (.conclusion == \"success\")) | .status")
|
||||
|
||||
if [[ ${RUN_status} == "completed" ]]; then
|
||||
SHA=$sha
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo Default branch is ${DEFAULT_BRANCH}
|
||||
echo Workflow will try to get cache from commit: ${SHA}
|
||||
|
||||
echo "sha=${SHA}" >> $GITHUB_OUTPUT
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Trigger Agent Image Builds
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
release:
|
||||
types: [released]
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate authentication token
|
||||
id: gen-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
client-id: ${{ secrets.CVAT_BOT_CLIENT_ID }}
|
||||
private-key: ${{ secrets.CVAT_BOT_PRIVATE_KEY }}
|
||||
repositories: "cvat-models"
|
||||
|
||||
- name: Trigger build in cvat-models
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
|
||||
run: |
|
||||
curl -L \
|
||||
-X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer $GH_TOKEN" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
https://api.github.com/repos/cvat-ai/cvat-models/dispatches \
|
||||
-d '{"event_type":"build-agent-images","client_payload":{"tag_name":"${{ github.event.release.tag_name }}"}}'
|
||||
@@ -0,0 +1,30 @@
|
||||
name: Trigger dependent repo update workflow
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches: [ develop ]
|
||||
|
||||
jobs:
|
||||
trigger:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Generate authentication token
|
||||
id: gen-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
client-id: ${{ secrets.CVAT_BOT_CLIENT_ID }}
|
||||
private-key: ${{ secrets.CVAT_BOT_PRIVATE_KEY }}
|
||||
repositories: ${{ secrets.CVAT_DEPENDENT_REPO }}
|
||||
|
||||
- name: Trigger private repository workflow
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
|
||||
run: |
|
||||
gh api \
|
||||
--method POST \
|
||||
/repos/cvat-ai/${{ secrets.CVAT_DEPENDENT_REPO }}/dispatches \
|
||||
-f 'event_type=upstream_updated' \
|
||||
-F "client_payload[triggeredBy]=${{ github.repository }}" \
|
||||
-F "client_payload[commitSha]=${{ github.sha }}" \
|
||||
-F "client_payload[branch]=${{ github.ref }}"
|
||||
@@ -0,0 +1,91 @@
|
||||
name: Update Python requirements
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
paths:
|
||||
- "cvat/requirements/*.in"
|
||||
- "utils/dataset_manifest/requirements.in"
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: read
|
||||
|
||||
jobs:
|
||||
update:
|
||||
# Only run on Dependabot branches from this repository. Regenerating
|
||||
# requirements can execute package build code, so fork PRs must not run this
|
||||
# workflow with access to the CVAT bot token.
|
||||
if: >
|
||||
github.event.pull_request.user.login == 'dependabot[bot]' &&
|
||||
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||
startsWith(github.event.pull_request.head.ref, 'dependabot/pip/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.ref }}
|
||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.12"
|
||||
|
||||
- name: Install system dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y \
|
||||
cargo \
|
||||
g++ \
|
||||
gcc \
|
||||
libhdf5-dev \
|
||||
libldap2-dev \
|
||||
libmp3lame-dev \
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
make \
|
||||
nasm \
|
||||
pkg-config \
|
||||
python3-dev
|
||||
|
||||
- name: Install pip-compile-multi
|
||||
run: python -m pip install pip-compile-multi
|
||||
|
||||
- name: Regenerate Python requirements
|
||||
run: cvat/requirements/regenerate.sh --no-upgrade
|
||||
|
||||
- name: Check generated requirements changes
|
||||
id: changes
|
||||
run: |
|
||||
if git diff --quiet -- cvat/requirements/*.txt utils/dataset_manifest/requirements.txt; then
|
||||
echo "No generated requirements changes"
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
# The default github.token does not trigger follow-up CI on push. Use the
|
||||
# same app token as the release workflow so the updated Dependabot PR runs
|
||||
# the normal validation pipeline.
|
||||
- name: Generate authentication token
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
id: gen-token
|
||||
uses: actions/create-github-app-token@v3
|
||||
with:
|
||||
client-id: "${{ secrets.CVAT_BOT_CLIENT_ID }}"
|
||||
private-key: "${{ secrets.CVAT_BOT_PRIVATE_KEY }}"
|
||||
|
||||
- name: Commit regenerated requirements
|
||||
if: steps.changes.outputs.changed == 'true'
|
||||
env:
|
||||
GH_TOKEN: "${{ steps.gen-token.outputs.token }}"
|
||||
run: |
|
||||
git config user.name "cvat-bot[bot]"
|
||||
git config user.email "147643061+cvat-bot[bot]@users.noreply.github.com"
|
||||
git add cvat/requirements/*.txt utils/dataset_manifest/requirements.txt
|
||||
git commit -m "Regenerate Python requirements"
|
||||
git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git"
|
||||
git push origin HEAD:${{ github.event.pull_request.head.ref }}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
# Project Specific
|
||||
/data/
|
||||
/events/
|
||||
/share/
|
||||
/static/
|
||||
/db.sqlite3
|
||||
/keys
|
||||
/logs
|
||||
/profiles
|
||||
|
||||
# Ignore temporary files
|
||||
docker-compose.override.yml
|
||||
__pycache__
|
||||
*.pyc
|
||||
._*
|
||||
.coverage
|
||||
.husky/
|
||||
.python-version
|
||||
/tmp*cvat/
|
||||
/temp*/
|
||||
|
||||
# Ignore generated test files
|
||||
docker-compose.tests.yml
|
||||
|
||||
# Ignore npm logs file
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
.DS_Store
|
||||
|
||||
#Ignore Cypress tests temp files
|
||||
/tests/cypress/fixtures
|
||||
/tests/cypress/screenshots
|
||||
.idea/
|
||||
|
||||
#Ignore helm-related files
|
||||
/helm-chart/Chart.lock
|
||||
/helm-chart/values.*.yaml
|
||||
/helm-chart/charts/*
|
||||
|
||||
#Ignore website temp files
|
||||
/site/public/
|
||||
/site/resources/
|
||||
/site/node_modules/
|
||||
/site/tech-doc-hugo
|
||||
/site/.hugo_build.lock
|
||||
|
||||
# Ignore all the installed packages
|
||||
node_modules
|
||||
/*env*/
|
||||
/.*env*
|
||||
perfkit.egg-info/
|
||||
|
||||
# Ignore all js dists
|
||||
cvat-data/dist
|
||||
cvat-core/dist
|
||||
cvat-canvas/dist
|
||||
cvat-canvas3d/dist
|
||||
cvat-ui/dist
|
||||
|
||||
# produced by prepare in the root package.json script
|
||||
.husky
|
||||
|
||||
# produced by cvat/apps/iam/rules/tests/generate_tests.py
|
||||
/cvat/apps/*/rules/*_test.gen.rego
|
||||
|
||||
# produced by allure report
|
||||
/**/allure-report*
|
||||
/**/allure-results*
|
||||
|
||||
# https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored
|
||||
**/.pnp.*
|
||||
**/.yarn/*
|
||||
**/!.yarn/patches
|
||||
**/!.yarn/plugins
|
||||
**/!.yarn/releases
|
||||
**/!.yarn/sdks
|
||||
**/!.yarn/versions
|
||||
@@ -0,0 +1,3 @@
|
||||
[submodule "site/themes/docsy"]
|
||||
path = site/themes/docsy
|
||||
url = https://github.com/google/docsy
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"all": true,
|
||||
"compact": false,
|
||||
"extension": [
|
||||
".js",
|
||||
".jsx",
|
||||
".ts",
|
||||
".tsx"
|
||||
],
|
||||
"exclude": [
|
||||
"**/3rdparty/*",
|
||||
"**/tests/*",
|
||||
"cvat-ui/src/actions/boundaries-actions.ts",
|
||||
"cvat-ui/src/utils/platform-checker.ts"
|
||||
],
|
||||
"parser-plugins": [
|
||||
"typescript"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
.*/
|
||||
3rdparty/
|
||||
node_modules/
|
||||
dist/
|
||||
data/
|
||||
datumaro/
|
||||
keys/
|
||||
logs/
|
||||
static/
|
||||
templates/
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"embeddedLanguageFormatting": "auto",
|
||||
"htmlWhitespaceSensitivity": "css",
|
||||
"insertPragma": false,
|
||||
"jsxBracketSameLine": false,
|
||||
"jsxSingleQuote": true,
|
||||
"printWidth": 120,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"requirePragma": false,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"vueIndentScriptAndStyle": false,
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["*.json", "*.yml", "*.yaml", "*.md"],
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,986 @@
|
||||
[MAIN]
|
||||
|
||||
# Analyse import fallback blocks. This can be used to support both Python 2 and
|
||||
# 3 compatible code, which means that the block might have code that exists
|
||||
# only in one or another interpreter, leading to false positives when analysed.
|
||||
analyse-fallback-blocks=no
|
||||
|
||||
# Load and enable all available extensions. Use --list-extensions to see a list
|
||||
# all available extensions.
|
||||
#enable-all-extensions=
|
||||
|
||||
# In error mode, messages with a category besides ERROR or FATAL are
|
||||
# suppressed, and no reports are done by default. Error mode is compatible with
|
||||
# disabling specific errors.
|
||||
#errors-only=
|
||||
|
||||
# Always return a 0 (non-error) status code, even if lint errors are found.
|
||||
# This is primarily useful in continuous integration scripts.
|
||||
#exit-zero=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code.
|
||||
extension-pkg-allow-list=
|
||||
|
||||
# A comma-separated list of package or module names from where C extensions may
|
||||
# be loaded. Extensions are loading into the active Python interpreter and may
|
||||
# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
|
||||
# for backward compatibility.)
|
||||
extension-pkg-whitelist=
|
||||
|
||||
# Return non-zero exit code if any of these messages/categories are detected,
|
||||
# even if score is above --fail-under value. Syntax same as enable. Messages
|
||||
# specified are enabled, while categories only check already-enabled messages.
|
||||
fail-on=
|
||||
|
||||
# Specify a score threshold to be exceeded before program exits with error.
|
||||
fail-under=10
|
||||
|
||||
# Interpret the stdin as a python script, whose filename needs to be passed as
|
||||
# the module_or_package argument.
|
||||
#from-stdin=
|
||||
|
||||
# Files or directories to be skipped. They should be base names, not paths.
|
||||
ignore=CVS
|
||||
|
||||
# Add files or directories matching the regex patterns to the ignore-list. The
|
||||
# regex matches against paths and can be in Posix or Windows format.
|
||||
ignore-paths=
|
||||
cvat-sdk/cvat_sdk/api_client|cvat-sdk/build|node_modules
|
||||
|
||||
|
||||
# Files or directories matching the regex patterns are skipped. The regex
|
||||
# matches against base names, not paths. The default value ignores Emacs file
|
||||
# locks
|
||||
ignore-patterns=
|
||||
|
||||
# List of module names for which member attributes should not be checked
|
||||
# (useful for modules/projects where namespaces are manipulated during runtime
|
||||
# and thus existing member attributes cannot be deduced by static analysis). It
|
||||
# supports qualified module names, as well as Unix pattern matching.
|
||||
ignored-modules=
|
||||
|
||||
# Python code to execute, usually for sys.path manipulation such as
|
||||
# pygtk.require().
|
||||
#init-hook=
|
||||
|
||||
# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
|
||||
# number of processors available to use, and will cap the count on Windows to
|
||||
# avoid hangs.
|
||||
jobs=0
|
||||
|
||||
# Control the amount of potential inferred values when inferring a single
|
||||
# object. This can help the performance when dealing with large functions or
|
||||
# complex, nested conditions.
|
||||
limit-inference-results=100
|
||||
|
||||
# List of plugins (as comma separated values of python module names) to load,
|
||||
# usually to register additional checkers.
|
||||
load-plugins=pylint_django
|
||||
|
||||
# Pickle collected data for later comparisons.
|
||||
persistent=yes
|
||||
|
||||
# Minimum Python version to use for version dependent checks. Will default to
|
||||
# the version used to run pylint.
|
||||
py-version=3.10
|
||||
|
||||
# Discover python modules and packages in the file system subtree.
|
||||
recursive=no
|
||||
|
||||
# When enabled, pylint would attempt to guess common misconfiguration and emit
|
||||
# user-friendly hints instead of false-positive error messages.
|
||||
suggestion-mode=yes
|
||||
|
||||
# Allow loading of arbitrary C extensions. Extensions are imported into the
|
||||
# active Python interpreter and may run arbitrary code.
|
||||
unsafe-load-any-extension=no
|
||||
|
||||
# In verbose mode, extra non-checker-related info will be displayed.
|
||||
#verbose=
|
||||
|
||||
|
||||
[REPORTS]
|
||||
|
||||
# Python expression which should return a score less than or equal to 10. You
|
||||
# have access to the variables 'fatal', 'error', 'warning', 'refactor',
|
||||
# 'convention', and 'info' which contain the number of messages in each
|
||||
# category, as well as 'statement' which is the total number of statements
|
||||
# analyzed. This score is used by the global evaluation report (RP0004).
|
||||
evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
|
||||
|
||||
# Template used to display messages. This is a python new-style format string
|
||||
# used to format the message information. See doc for all details.
|
||||
msg-template=
|
||||
|
||||
# Set the output format. Available formats are text, parseable, colorized, json
|
||||
# and msvs (visual studio). You can also give a reporter class, e.g.
|
||||
# mypackage.mymodule.MyReporterClass.
|
||||
#output-format=
|
||||
|
||||
# Tells whether to display a full report or only the messages.
|
||||
reports=no
|
||||
|
||||
# Activate the evaluation score.
|
||||
score=yes
|
||||
|
||||
|
||||
[MESSAGES CONTROL]
|
||||
|
||||
# Only show warnings with the listed confidence levels. Leave empty to show
|
||||
# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE,
|
||||
# UNDEFINED.
|
||||
confidence=HIGH,
|
||||
CONTROL_FLOW,
|
||||
INFERENCE,
|
||||
INFERENCE_FAILURE,
|
||||
UNDEFINED
|
||||
|
||||
# Disable the message, report, category or checker with the given id(s). You
|
||||
# can either give multiple identifiers separated by comma (,) or put this
|
||||
# option multiple times (only on the command line, not in the configuration
|
||||
# file where it should appear only once). You can also use "--disable=all" to
|
||||
# disable everything first and then re-enable specific checks. For example, if
|
||||
# you want to run only the similarities checker, you can use "--disable=all
|
||||
# --enable=similarities". If you want to run only the classes checker, but have
|
||||
# no Warning level messages displayed, use "--disable=all --enable=classes
|
||||
# --disable=W".
|
||||
disable=abstract-method,
|
||||
arguments-out-of-order,
|
||||
arguments-renamed,
|
||||
assert-on-string-literal,
|
||||
assigning-non-slot,
|
||||
assignment-from-none,
|
||||
astroid-error,
|
||||
attribute-defined-outside-init,
|
||||
await-outside-async,
|
||||
bad-classmethod-argument,
|
||||
bad-configuration-section,
|
||||
bad-file-encoding,
|
||||
bad-inline-option,
|
||||
bad-mcs-classmethod-argument,
|
||||
bad-mcs-method-argument,
|
||||
bad-open-mode,
|
||||
bad-plugin-value,
|
||||
bad-reversed-sequence,
|
||||
bad-staticmethod-argument,
|
||||
bad-str-strip-call,
|
||||
bad-string-format-type,
|
||||
bad-thread-instantiation,
|
||||
bidirectional-unicode,
|
||||
boolean-datetime,
|
||||
broad-exception-caught,
|
||||
broad-exception-raised,
|
||||
c-extension-no-member,
|
||||
cell-var-from-loop,
|
||||
chained-comparison,
|
||||
class-variable-slots-conflict,
|
||||
comparison-of-constants,
|
||||
comparison-with-callable,
|
||||
comparison-with-itself,
|
||||
condition-evals-to-constant,
|
||||
config-parse-error,
|
||||
consider-iterating-dictionary,
|
||||
consider-math-not-float,
|
||||
consider-merging-isinstance,
|
||||
consider-swap-variables,
|
||||
consider-using-dict-comprehension,
|
||||
consider-using-dict-items,
|
||||
consider-using-f-string,
|
||||
consider-using-from-import,
|
||||
consider-using-generator,
|
||||
consider-using-get,
|
||||
consider-using-in,
|
||||
consider-using-join,
|
||||
consider-using-max-builtin,
|
||||
consider-using-min-builtin,
|
||||
consider-using-set-comprehension,
|
||||
consider-using-sys-exit,
|
||||
consider-using-ternary,
|
||||
consider-using-with,
|
||||
cyclic-import,
|
||||
deprecated-argument,
|
||||
deprecated-class,
|
||||
deprecated-decorator,
|
||||
deprecated-method,
|
||||
deprecated-module,
|
||||
deprecated-pragma,
|
||||
dict-iter-missing-items,
|
||||
disallowed-name,
|
||||
django-not-available-placeholder,
|
||||
django-not-available,
|
||||
django-not-configured,
|
||||
django-settings-module-not-found,
|
||||
duplicate-code,
|
||||
duplicate-string-formatting-argument,
|
||||
duplicate-value,
|
||||
empty-docstring,
|
||||
eval-used,
|
||||
f-string-without-interpolation,
|
||||
fatal,
|
||||
file-ignored,
|
||||
fixme,
|
||||
forgotten-debug-statement,
|
||||
format-string-without-interpolation,
|
||||
global-statement,
|
||||
hard-coded-auth-user,
|
||||
http-response-with-content-type-json,
|
||||
http-response-with-json-dumps,
|
||||
implicit-str-concat,
|
||||
import-error,
|
||||
import-outside-toplevel,
|
||||
import-self,
|
||||
imported-auth-user,
|
||||
inconsistent-quotes,
|
||||
inconsistent-return-statements,
|
||||
invalid-all-format,
|
||||
invalid-bool-returned,
|
||||
invalid-bytes-returned,
|
||||
invalid-character-backspace,
|
||||
invalid-character-carriage-return,
|
||||
invalid-character-esc,
|
||||
invalid-character-nul,
|
||||
invalid-character-sub,
|
||||
invalid-character-zero-width-space,
|
||||
invalid-characters-in-docstring,
|
||||
invalid-class-object,
|
||||
invalid-enum-extension,
|
||||
invalid-envvar-default,
|
||||
invalid-envvar-value,
|
||||
invalid-format-returned,
|
||||
invalid-getnewargs-ex-returned,
|
||||
invalid-getnewargs-returned,
|
||||
invalid-hash-returned,
|
||||
invalid-index-returned,
|
||||
invalid-length-hint-returned,
|
||||
invalid-length-returned,
|
||||
invalid-metaclass,
|
||||
invalid-name,
|
||||
invalid-overridden-method,
|
||||
invalid-repr-returned,
|
||||
invalid-str-returned,
|
||||
invalid-unary-operand-type,
|
||||
invalid-unicode-codec,
|
||||
isinstance-second-argument-not-valid-type,
|
||||
keyword-arg-before-vararg,
|
||||
line-too-long,
|
||||
literal-comparison,
|
||||
locally-disabled,
|
||||
logging-format-interpolation,
|
||||
logging-fstring-interpolation,
|
||||
logging-not-lazy,
|
||||
method-cache-max-size-none,
|
||||
method-check-failed,
|
||||
misplaced-format-function,
|
||||
missing-class-docstring,
|
||||
missing-function-docstring,
|
||||
missing-module-docstring,
|
||||
missing-parentheses-for-call-in-test,
|
||||
mixed-line-endings,
|
||||
model-has-unicode,
|
||||
model-missing-unicode,
|
||||
model-no-explicit-unicode,
|
||||
model-unicode-not-callable,
|
||||
modelform-uses-exclude,
|
||||
modified-iterating-dict,
|
||||
modified-iterating-list,
|
||||
modified-iterating-set,
|
||||
multiple-imports,
|
||||
multiple-statements,
|
||||
nan-comparison,
|
||||
no-else-break,
|
||||
no-else-continue,
|
||||
no-else-raise,
|
||||
no-else-return,
|
||||
no-member,
|
||||
no-name-in-module,
|
||||
no-self-argument,
|
||||
non-ascii-file-name,
|
||||
non-ascii-module-import,
|
||||
non-ascii-name,
|
||||
non-str-assignment-to-dunder-name,
|
||||
not-a-mapping,
|
||||
not-an-iterable,
|
||||
not-async-context-manager,
|
||||
not-context-manager,
|
||||
overridden-final-method,
|
||||
parse-error,
|
||||
possibly-unused-variable,
|
||||
possibly-used-before-assignment,
|
||||
potential-index-error,
|
||||
preferred-module,
|
||||
property-with-parameters,
|
||||
protected-access,
|
||||
raise-missing-from,
|
||||
raising-format-tuple,
|
||||
raw-checker-failed,
|
||||
redeclared-assigned-name,
|
||||
redefined-argument-from-local,
|
||||
redefined-outer-name,
|
||||
redefined-slots-in-subclass,
|
||||
redundant-content-type-for-json-response,
|
||||
redundant-u-string-prefix,
|
||||
redundant-unittest-assert,
|
||||
relative-beyond-top-level,
|
||||
return-arg-in-generator,
|
||||
self-assigning-variable,
|
||||
self-cls-assignment,
|
||||
shallow-copy-environ,
|
||||
simplifiable-condition,
|
||||
simplifiable-if-expression,
|
||||
simplify-boolean-expression,
|
||||
single-string-used-for-slots,
|
||||
stop-iteration-return,
|
||||
subclassed-final-class,
|
||||
subprocess-popen-preexec-fn,
|
||||
subprocess-run-check,
|
||||
super-init-not-called,
|
||||
super-with-arguments,
|
||||
super-without-brackets,
|
||||
suppressed-message,
|
||||
too-few-public-methods,
|
||||
too-many-ancestors,
|
||||
too-many-arguments,
|
||||
too-many-boolean-expressions,
|
||||
too-many-branches,
|
||||
too-many-instance-attributes,
|
||||
too-many-lines,
|
||||
too-many-locals,
|
||||
too-many-nested-blocks,
|
||||
too-many-positional-arguments,
|
||||
too-many-public-methods,
|
||||
too-many-return-statements,
|
||||
too-many-statements,
|
||||
trailing-comma-tuple,
|
||||
trailing-newlines,
|
||||
try-except-raise,
|
||||
typevar-double-variance,
|
||||
typevar-name-incorrect-variance,
|
||||
typevar-name-mismatch,
|
||||
unbalanced-tuple-unpacking,
|
||||
undefined-loop-variable,
|
||||
unexpected-line-ending-format,
|
||||
ungrouped-imports,
|
||||
unhashable-member,
|
||||
unknown-option-value,
|
||||
unnecessary-comprehension,
|
||||
unnecessary-dict-index-lookup,
|
||||
unnecessary-direct-lambda-call,
|
||||
unnecessary-dunder-call,
|
||||
unnecessary-ellipsis,
|
||||
unnecessary-lambda-assignment,
|
||||
unnecessary-list-index-lookup,
|
||||
unnecessary-negation,
|
||||
unpacking-non-sequence,
|
||||
unrecognized-inline-option,
|
||||
unrecognized-option,
|
||||
unspecified-encoding,
|
||||
unsubscriptable-object,
|
||||
unsupported-assignment-operation,
|
||||
unsupported-binary-operation,
|
||||
unsupported-delete-operation,
|
||||
unsupported-membership-test,
|
||||
unused-argument,
|
||||
unused-format-string-argument,
|
||||
unused-private-member,
|
||||
unused-wildcard-import,
|
||||
use-a-generator,
|
||||
use-dict-literal,
|
||||
use-implicit-booleaness-not-comparison,
|
||||
use-implicit-booleaness-not-len,
|
||||
use-list-literal,
|
||||
use-maxsplit-arg,
|
||||
use-sequence-for-iteration,
|
||||
use-symbolic-message-instead,
|
||||
used-prior-global-declaration,
|
||||
useless-import-alias,
|
||||
useless-option-value,
|
||||
useless-parent-delegation,
|
||||
useless-return,
|
||||
useless-suppression,
|
||||
useless-with-lock,
|
||||
using-constant-test,
|
||||
using-f-string-in-unsupported-version,
|
||||
using-final-decorator-in-unsupported-version,
|
||||
wrong-exception-operation,
|
||||
wrong-import-order,
|
||||
wrong-import-position,
|
||||
wrong-spelling-in-comment,
|
||||
wrong-spelling-in-docstring,
|
||||
yield-inside-async-function,
|
||||
|
||||
# Enable the message, report, category or checker with the given id(s). You can
|
||||
# either give multiple identifier separated by comma (,) or put this option
|
||||
# multiple time (only on the command line, not in the configuration file where
|
||||
# it should appear only once). See also the "--disable" option for examples.
|
||||
enable=abstract-class-instantiated,
|
||||
access-member-before-definition,
|
||||
anomalous-backslash-in-string,
|
||||
anomalous-unicode-escape-in-string,
|
||||
arguments-differ,
|
||||
assert-on-tuple,
|
||||
assignment-from-no-return,
|
||||
async-context-manager-with-regular-with,
|
||||
bad-except-order,
|
||||
bad-exception-cause,
|
||||
bad-format-character,
|
||||
bad-format-string-key,
|
||||
bad-format-string,
|
||||
bad-indentation,
|
||||
bad-super-call,
|
||||
bare-except,
|
||||
bare-name-capture-pattern,
|
||||
binary-op-exception,
|
||||
break-in-finally,
|
||||
catching-non-exception,
|
||||
confusing-with-statement,
|
||||
consider-using-enumerate,
|
||||
continue-in-finally,
|
||||
dangerous-default-value,
|
||||
duplicate-argument-name,
|
||||
duplicate-bases,
|
||||
duplicate-except,
|
||||
duplicate-key,
|
||||
exec-used,
|
||||
expression-not-assigned,
|
||||
format-combined-specification,
|
||||
format-needs-mapping,
|
||||
function-redefined,
|
||||
global-at-module-level,
|
||||
global-variable-not-assigned,
|
||||
global-variable-undefined,
|
||||
inconsistent-mro,
|
||||
inherit-non-class,
|
||||
init-is-generator,
|
||||
invalid-all-object,
|
||||
invalid-format-index,
|
||||
invalid-match-args-definition,
|
||||
invalid-sequence-index,
|
||||
invalid-slice-index,
|
||||
invalid-slots-object,
|
||||
invalid-slots,
|
||||
invalid-star-assignment-target,
|
||||
logging-format-truncated,
|
||||
logging-too-few-args,
|
||||
logging-too-many-args,
|
||||
logging-unsupported-format,
|
||||
lost-exception,
|
||||
match-class-bind-self,
|
||||
match-class-positional-attributes,
|
||||
method-hidden,
|
||||
misplaced-bare-raise,
|
||||
misplaced-future,
|
||||
missing-final-newline,
|
||||
missing-format-argument-key,
|
||||
missing-format-attribute,
|
||||
missing-format-string-key,
|
||||
missing-kwoa,
|
||||
mixed-format-string,
|
||||
multiple-class-sub-patterns,
|
||||
no-classmethod-decorator,
|
||||
no-method-argument,
|
||||
no-staticmethod-decorator,
|
||||
no-value-for-parameter,
|
||||
non-iterator-returned,
|
||||
non-parent-init-called,
|
||||
nonexistent-operator,
|
||||
nonlocal-and-global,
|
||||
nonlocal-without-binding,
|
||||
not-callable,
|
||||
not-in-loop,
|
||||
notimplemented-raised,
|
||||
pointless-statement,
|
||||
pointless-string-statement,
|
||||
raising-bad-type,
|
||||
raising-non-exception,
|
||||
redefined-builtin,
|
||||
redundant-keyword-arg,
|
||||
reimported,
|
||||
repeated-keyword,
|
||||
return-in-init,
|
||||
return-outside-function,
|
||||
signature-differs,
|
||||
simplifiable-if-statement,
|
||||
singleton-comparison,
|
||||
star-needs-assignment-target,
|
||||
superfluous-parens,
|
||||
syntax-error,
|
||||
too-few-format-args,
|
||||
too-many-format-args,
|
||||
too-many-function-args,
|
||||
too-many-positional-sub-patterns,
|
||||
too-many-star-expressions,
|
||||
trailing-whitespace,
|
||||
truncated-format-string,
|
||||
undefined-all-variable,
|
||||
undefined-variable,
|
||||
unexpected-keyword-arg,
|
||||
unexpected-special-method-signature,
|
||||
unidiomatic-typecheck,
|
||||
unnecessary-lambda,
|
||||
unnecessary-pass,
|
||||
unnecessary-semicolon,
|
||||
unreachable,
|
||||
unused-format-string-key,
|
||||
unused-import,
|
||||
unused-variable,
|
||||
used-before-assignment,
|
||||
useless-else-on-loop,
|
||||
useless-object-inheritance,
|
||||
wildcard-import,
|
||||
yield-outside-function,
|
||||
|
||||
|
||||
[BASIC]
|
||||
|
||||
# Naming style matching correct argument names.
|
||||
argument-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct argument names. Overrides argument-
|
||||
# naming-style. If left empty, argument names will be checked with the set
|
||||
# naming style.
|
||||
argument-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming style matching correct attribute names.
|
||||
attr-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct attribute names. Overrides attr-naming-
|
||||
# style. If left empty, attribute names will be checked with the set naming
|
||||
# style.
|
||||
attr-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Bad variable names which should always be refused, separated by a comma.
|
||||
bad-names=foo,
|
||||
bar,
|
||||
baz,
|
||||
toto,
|
||||
tutu,
|
||||
tata
|
||||
|
||||
# Bad variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be refused
|
||||
bad-names-rgxs=
|
||||
|
||||
# Naming style matching correct class attribute names.
|
||||
class-attribute-naming-style=any
|
||||
|
||||
# Regular expression matching correct class attribute names. Overrides class-
|
||||
# attribute-naming-style. If left empty, class attribute names will be checked
|
||||
# with the set naming style.
|
||||
class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$
|
||||
|
||||
# Naming style matching correct class constant names.
|
||||
class-const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct class constant names. Overrides class-
|
||||
# const-naming-style. If left empty, class constant names will be checked with
|
||||
# the set naming style.
|
||||
#class-const-rgx=
|
||||
|
||||
# Naming style matching correct class names.
|
||||
class-naming-style=PascalCase
|
||||
|
||||
# Regular expression matching correct class names. Overrides class-naming-
|
||||
# style. If left empty, class names will be checked with the set naming style.
|
||||
class-rgx=[A-Z_][a-zA-Z0-9]+$
|
||||
|
||||
# Naming style matching correct constant names.
|
||||
const-naming-style=UPPER_CASE
|
||||
|
||||
# Regular expression matching correct constant names. Overrides const-naming-
|
||||
# style. If left empty, constant names will be checked with the set naming
|
||||
# style.
|
||||
const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$
|
||||
|
||||
# Minimum line length for functions/classes that require docstrings, shorter
|
||||
# ones are exempt.
|
||||
docstring-min-length=-1
|
||||
|
||||
# Naming style matching correct function names.
|
||||
function-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct function names. Overrides function-
|
||||
# naming-style. If left empty, function names will be checked with the set
|
||||
# naming style.
|
||||
function-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Good variable names which should always be accepted, separated by a comma.
|
||||
good-names=i,
|
||||
j,
|
||||
k,
|
||||
ex,
|
||||
Run,
|
||||
_
|
||||
|
||||
# Good variable names regexes, separated by a comma. If names match any regex,
|
||||
# they will always be accepted
|
||||
good-names-rgxs=
|
||||
|
||||
# Include a hint for the correct naming format with invalid-name.
|
||||
include-naming-hint=no
|
||||
|
||||
# Naming style matching correct inline iteration names.
|
||||
inlinevar-naming-style=any
|
||||
|
||||
# Regular expression matching correct inline iteration names. Overrides
|
||||
# inlinevar-naming-style. If left empty, inline iteration names will be checked
|
||||
# with the set naming style.
|
||||
inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
|
||||
|
||||
# Naming style matching correct method names.
|
||||
method-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct method names. Overrides method-naming-
|
||||
# style. If left empty, method names will be checked with the set naming style.
|
||||
method-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
# Naming style matching correct module names.
|
||||
module-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct module names. Overrides module-naming-
|
||||
# style. If left empty, module names will be checked with the set naming style.
|
||||
module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
|
||||
|
||||
# Colon-delimited sets of names that determine each other's naming style when
|
||||
# the name regexes allow several styles.
|
||||
name-group=
|
||||
|
||||
# Regular expression which should only match function or class names that do
|
||||
# not require a docstring.
|
||||
no-docstring-rgx=^_
|
||||
|
||||
# List of decorators that produce properties, such as abc.abstractproperty. Add
|
||||
# to this list to register other decorators that produce valid properties.
|
||||
# These decorators are taken in consideration only for invalid-name.
|
||||
property-classes=abc.abstractproperty
|
||||
|
||||
# Regular expression matching correct type variable names. If left empty, type
|
||||
# variable names will be checked with the set naming style.
|
||||
#typevar-rgx=
|
||||
|
||||
# Naming style matching correct variable names.
|
||||
variable-naming-style=snake_case
|
||||
|
||||
# Regular expression matching correct variable names. Overrides variable-
|
||||
# naming-style. If left empty, variable names will be checked with the set
|
||||
# naming style.
|
||||
variable-rgx=[a-z_][a-z0-9_]{2,30}$
|
||||
|
||||
|
||||
[VARIABLES]
|
||||
|
||||
# List of additional names supposed to be defined in builtins. Remember that
|
||||
# you should avoid defining new builtins when possible.
|
||||
additional-builtins=
|
||||
|
||||
# Tells whether unused global variables should be treated as a violation.
|
||||
allow-global-unused-variables=yes
|
||||
|
||||
# List of names allowed to shadow builtins
|
||||
allowed-redefined-builtins=
|
||||
|
||||
# List of strings which can identify a callback function by name. A callback
|
||||
# name must start or end with one of those strings.
|
||||
callbacks=cb_,
|
||||
_cb
|
||||
|
||||
# A regular expression matching the name of dummy variables (i.e. expected to
|
||||
# not be used).
|
||||
dummy-variables-rgx=(_+[a-zA-Z0-9]*?$)|dummy
|
||||
|
||||
# Argument names that match this expression will be ignored. Default to name
|
||||
# with leading underscore.
|
||||
ignored-argument-names=_.*
|
||||
|
||||
# Tells whether we should check for unused import in __init__ files.
|
||||
init-import=no
|
||||
|
||||
# List of qualified module names which can have objects that can redefine
|
||||
# builtins.
|
||||
redefining-builtins-modules=six.moves,future.builtins
|
||||
|
||||
|
||||
[DESIGN]
|
||||
|
||||
# List of regular expressions of class ancestor names to ignore when counting
|
||||
# public methods (see R0903)
|
||||
exclude-too-few-public-methods=
|
||||
|
||||
# List of qualified class names to ignore when counting class parents (see
|
||||
# R0901)
|
||||
ignored-parents=
|
||||
|
||||
# Maximum number of arguments for function / method.
|
||||
max-args=5
|
||||
|
||||
# Maximum number of attributes for a class (see R0902).
|
||||
max-attributes=7
|
||||
|
||||
# Maximum number of boolean expressions in an if statement (see R0916).
|
||||
max-bool-expr=5
|
||||
|
||||
# Maximum number of branch for function / method body.
|
||||
max-branches=12
|
||||
|
||||
# Maximum number of locals for function / method body.
|
||||
max-locals=15
|
||||
|
||||
# Maximum number of parents for a class (see R0901).
|
||||
max-parents=7
|
||||
|
||||
# Maximum number of public methods for a class (see R0904).
|
||||
max-public-methods=20
|
||||
|
||||
# Maximum number of return / yield for function / method body.
|
||||
max-returns=6
|
||||
|
||||
# Maximum number of statements in function / method body.
|
||||
max-statements=50
|
||||
|
||||
# Minimum number of public methods for a class (see R0903).
|
||||
min-public-methods=2
|
||||
|
||||
|
||||
[FORMAT]
|
||||
|
||||
# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
|
||||
expected-line-ending-format=
|
||||
|
||||
# Regexp for a line that is allowed to be longer than the limit.
|
||||
ignore-long-lines=^\s*(# )?<?https?://\S+>?$
|
||||
|
||||
# Number of spaces of indent required inside a hanging or continued line.
|
||||
indent-after-paren=4
|
||||
|
||||
# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1
|
||||
# tab).
|
||||
indent-string=' '
|
||||
|
||||
# Maximum number of characters on a single line.
|
||||
max-line-length=100
|
||||
|
||||
# Maximum number of lines in a module.
|
||||
max-module-lines=1000
|
||||
|
||||
# Allow the body of a class to be on the same line as the declaration if body
|
||||
# contains single statement.
|
||||
single-line-class-stmt=no
|
||||
|
||||
# Allow the body of an if to be on the same line as the test if there is no
|
||||
# else.
|
||||
single-line-if-stmt=no
|
||||
|
||||
|
||||
[STRING]
|
||||
|
||||
# This flag controls whether inconsistent-quotes generates a warning when the
|
||||
# character used as a quote delimiter is used inconsistently within a module.
|
||||
check-quote-consistency=no
|
||||
|
||||
# This flag controls whether the implicit-str-concat should generate a warning
|
||||
# on implicit string concatenation in sequences defined over several lines.
|
||||
check-str-concat-over-line-jumps=no
|
||||
|
||||
|
||||
[MISCELLANEOUS]
|
||||
|
||||
# List of note tags to take in consideration, separated by a comma.
|
||||
notes=FIXME,
|
||||
XXX,
|
||||
TODO
|
||||
|
||||
# Regular expression of note tags to take in consideration.
|
||||
notes-rgx=
|
||||
|
||||
|
||||
[REFACTORING]
|
||||
|
||||
# Maximum number of nested blocks for function / method body
|
||||
max-nested-blocks=5
|
||||
|
||||
# Complete name of functions that never returns. When checking for
|
||||
# inconsistent-return-statements if a never returning function is called then
|
||||
# it will be considered as an explicit return statement and no message will be
|
||||
# printed.
|
||||
never-returning-functions=sys.exit,argparse.parse_error
|
||||
|
||||
|
||||
[SPELLING]
|
||||
|
||||
# Limits count of emitted suggestions for spelling mistakes.
|
||||
max-spelling-suggestions=4
|
||||
|
||||
# Spelling dictionary name. Available dictionaries: none. To make it work,
|
||||
# install the 'python-enchant' package.
|
||||
spelling-dict=
|
||||
|
||||
# List of comma separated words that should be considered directives if they
|
||||
# appear at the beginning of a comment and should not be checked.
|
||||
spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
|
||||
|
||||
# List of comma separated words that should not be checked.
|
||||
spelling-ignore-words=
|
||||
|
||||
# A path to a file that contains the private dictionary; one word per line.
|
||||
spelling-private-dict-file=
|
||||
|
||||
# Tells whether to store unknown words to the private dictionary (see the
|
||||
# --spelling-private-dict-file option) instead of raising a message.
|
||||
spelling-store-unknown-words=no
|
||||
|
||||
|
||||
[SIMILARITIES]
|
||||
|
||||
# Comments are removed from the similarity computation
|
||||
ignore-comments=yes
|
||||
|
||||
# Docstrings are removed from the similarity computation
|
||||
ignore-docstrings=yes
|
||||
|
||||
# Imports are removed from the similarity computation
|
||||
ignore-imports=no
|
||||
|
||||
# Signatures are removed from the similarity computation
|
||||
ignore-signatures=yes
|
||||
|
||||
# Minimum lines number of a similarity.
|
||||
min-similarity-lines=4
|
||||
|
||||
|
||||
[EXCEPTIONS]
|
||||
|
||||
# Exceptions that will emit a warning when caught.
|
||||
overgeneral-exceptions=builtins.Exception
|
||||
|
||||
|
||||
[IMPORTS]
|
||||
|
||||
# List of modules that can be imported at any level, not just the top level
|
||||
# one.
|
||||
allow-any-import-level=
|
||||
|
||||
# Allow wildcard imports from modules that define __all__.
|
||||
allow-wildcard-with-all=no
|
||||
|
||||
# Deprecated modules which should not be used, separated by a comma.
|
||||
deprecated-modules=optparse
|
||||
|
||||
# Output a graph (.gv or any supported image format) of external dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
ext-import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of all (i.e. internal and
|
||||
# external) dependencies to the given file (report RP0402 must not be
|
||||
# disabled).
|
||||
import-graph=
|
||||
|
||||
# Output a graph (.gv or any supported image format) of internal dependencies
|
||||
# to the given file (report RP0402 must not be disabled).
|
||||
int-import-graph=
|
||||
|
||||
# Force import order to recognize a module as part of the standard
|
||||
# compatibility libraries.
|
||||
known-standard-library=
|
||||
|
||||
# Force import order to recognize a module as part of a third party library.
|
||||
known-third-party=enchant
|
||||
|
||||
# Couples of modules and preferred modules, separated by a comma.
|
||||
preferred-modules=
|
||||
|
||||
|
||||
[CLASSES]
|
||||
|
||||
# Warn about protected attribute access inside special methods
|
||||
check-protected-access-in-special-methods=no
|
||||
|
||||
# List of method names used to declare (i.e. assign) instance attributes.
|
||||
defining-attr-methods=__init__,
|
||||
__new__,
|
||||
setUp
|
||||
|
||||
# List of member names, which should be excluded from the protected access
|
||||
# warning.
|
||||
exclude-protected=_asdict,
|
||||
_fields,
|
||||
_replace,
|
||||
_source,
|
||||
_make
|
||||
|
||||
# List of valid names for the first argument in a class method.
|
||||
valid-classmethod-first-arg=cls
|
||||
|
||||
# List of valid names for the first argument in a metaclass class method.
|
||||
valid-metaclass-classmethod-first-arg=mcs
|
||||
|
||||
|
||||
[TYPECHECK]
|
||||
|
||||
# List of decorators that produce context managers, such as
|
||||
# contextlib.contextmanager. Add to this list to register other decorators that
|
||||
# produce valid context managers.
|
||||
contextmanager-decorators=contextlib.contextmanager
|
||||
|
||||
# List of members which are set dynamically and missed by pylint inference
|
||||
# system, and so shouldn't trigger E1101 when accessed. Python regular
|
||||
# expressions are accepted.
|
||||
generated-members=
|
||||
|
||||
# Tells whether to warn about missing members when the owner of the attribute
|
||||
# is inferred to be None.
|
||||
ignore-none=yes
|
||||
|
||||
# This flag controls whether pylint should warn about no-member and similar
|
||||
# checks whenever an opaque object is returned when inferring. The inference
|
||||
# can return multiple potential results while evaluating a Python object, but
|
||||
# some branches might not be evaluated, which results in partial inference. In
|
||||
# that case, it might be useful to still emit no-member and other checks for
|
||||
# the rest of the inferred objects.
|
||||
ignore-on-opaque-inference=yes
|
||||
|
||||
# List of symbolic message names to ignore for Mixin members.
|
||||
ignored-checks-for-mixins=no-member,
|
||||
not-async-context-manager,
|
||||
not-context-manager,
|
||||
attribute-defined-outside-init
|
||||
|
||||
# List of class names for which member attributes should not be checked (useful
|
||||
# for classes with dynamically set attributes). This supports the use of
|
||||
# qualified names.
|
||||
ignored-classes=optparse.Values,thread._local,_thread._local
|
||||
|
||||
# Show a hint with possible names when a member name was not found. The aspect
|
||||
# of finding the hint is based on edit distance.
|
||||
missing-member-hint=yes
|
||||
|
||||
# The minimum edit distance a name should have in order to be considered a
|
||||
# similar match for a missing member name.
|
||||
missing-member-hint-distance=1
|
||||
|
||||
# The total number of similar names that should be taken in consideration when
|
||||
# showing a hint for a missing member.
|
||||
missing-member-max-choices=1
|
||||
|
||||
# Regex pattern to define which classes are considered mixins.
|
||||
mixin-class-rgx=.*[Mm]ixin
|
||||
|
||||
# List of decorators that change the signature of a decorated function.
|
||||
signature-mutators=
|
||||
|
||||
|
||||
[LOGGING]
|
||||
|
||||
# The type of string formatting that logging methods do. `old` means using %
|
||||
# formatting, `new` is for `{}` formatting.
|
||||
logging-format-style=old
|
||||
|
||||
# Logging modules to check that the string format arguments are in logging
|
||||
# function parameter format.
|
||||
logging-modules=logging
|
||||
|
||||
|
||||
[DJANGO FOREIGN KEYS REFERENCED BY STRINGS]
|
||||
|
||||
# A module containing Django settings to be used while linting.
|
||||
#django-settings-module=
|
||||
@@ -0,0 +1,40 @@
|
||||
ignore:
|
||||
files:
|
||||
- "*_test.gen.rego"
|
||||
rules:
|
||||
custom:
|
||||
naming-convention:
|
||||
conventions:
|
||||
- pattern: '^[a-z_]+$|^[A-Z_]+$'
|
||||
targets:
|
||||
- rule
|
||||
- function
|
||||
- variable
|
||||
level: error
|
||||
idiomatic:
|
||||
no-defined-entrypoint:
|
||||
# This would likely be the allow rule in each package
|
||||
# Not critical though, so ignoring for the time being
|
||||
level: ignore
|
||||
style:
|
||||
avoid-get-and-list-prefix:
|
||||
# Mainly a style preference
|
||||
# https://docs.styra.com/regal/rules/style/avoid-get-and-list-prefix
|
||||
level: ignore
|
||||
external-reference:
|
||||
# https://docs.styra.com/regal/rules/style/external-reference
|
||||
level: ignore
|
||||
opa-fmt:
|
||||
# https://docs.styra.com/regal/rules/style/opa-fmt
|
||||
level: ignore
|
||||
prefer-snake-case:
|
||||
# Disabled in favor of custom naming-convention rule above
|
||||
level: ignore
|
||||
rule-length:
|
||||
# Many rules longer than the default limit of 30 lines
|
||||
level: error
|
||||
max-rule-length: 60
|
||||
imports:
|
||||
unresolved-import:
|
||||
# one of "error", "warning", "ignore"
|
||||
level: error
|
||||
@@ -0,0 +1,4 @@
|
||||
cvat-sdk/docs/
|
||||
cvat-sdk/README.md
|
||||
.env/
|
||||
site/themes/
|
||||
@@ -0,0 +1,18 @@
|
||||
exports.settings = { bullet: '*', paddedTable: false };
|
||||
|
||||
exports.plugins = [
|
||||
'remark-frontmatter',
|
||||
'remark-gfm',
|
||||
'remark-preset-lint-recommended',
|
||||
'remark-preset-lint-consistent',
|
||||
['remark-lint-list-item-indent', 'one'],
|
||||
['remark-lint-no-dead-urls', false], // Does not work because of github protection system
|
||||
['remark-lint-maximum-line-length', 120],
|
||||
['remark-lint-maximum-heading-length', 120],
|
||||
['remark-lint-strong-marker', '*'],
|
||||
['remark-lint-emphasis-marker', '_'],
|
||||
['remark-lint-unordered-list-marker-style', '-'],
|
||||
['remark-lint-ordered-list-marker-style', '.'],
|
||||
['remark-lint-no-file-name-irregular-characters', false],
|
||||
['remark-lint-list-item-spacing', false],
|
||||
];
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "stylelint-config-standard-scss",
|
||||
"rules": {
|
||||
"scss/comment-no-empty": null,
|
||||
"value-keyword-case": null,
|
||||
"color-function-notation": ["legacy"],
|
||||
"scss/at-extend-no-missing-placeholder": null,
|
||||
"no-descending-specificity": null
|
||||
},
|
||||
"ignoreFiles": ["**/*.js", "**/*.ts", "**/*.py"]
|
||||
}
|
||||
Vendored
+427
@@ -0,0 +1,427 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
|
||||
{
|
||||
"name": "REST API tests: Attach to server",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9090
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/home/django/"
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/.env",
|
||||
"remoteRoot": "/opt/venv",
|
||||
}
|
||||
],
|
||||
"justMyCode": false,
|
||||
},
|
||||
{
|
||||
"name": "REST API tests: Attach to RQ annotation worker",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9091
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/home/django/"
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/.env",
|
||||
"remoteRoot": "/opt/venv",
|
||||
}
|
||||
],
|
||||
"justMyCode": false,
|
||||
},
|
||||
{
|
||||
"name": "REST API tests: Attach to RQ export worker",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9092
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/home/django/"
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/.env",
|
||||
"remoteRoot": "/opt/venv",
|
||||
}
|
||||
],
|
||||
"justMyCode": false,
|
||||
},
|
||||
{
|
||||
"name": "REST API tests: Attach to RQ import worker",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9093
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/home/django/"
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/.env",
|
||||
"remoteRoot": "/opt/venv",
|
||||
}
|
||||
],
|
||||
"justMyCode": false,
|
||||
},
|
||||
{
|
||||
"name": "REST API tests: Attach to RQ quality reports worker",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9094
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/home/django/"
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/.env",
|
||||
"remoteRoot": "/opt/venv",
|
||||
}
|
||||
],
|
||||
"justMyCode": false,
|
||||
},
|
||||
{
|
||||
"name": "REST API tests: Attach to RQ consensus worker",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 9096
|
||||
},
|
||||
"pathMappings": [
|
||||
{
|
||||
"localRoot": "${workspaceFolder}",
|
||||
"remoteRoot": "/home/django/"
|
||||
},
|
||||
{
|
||||
"localRoot": "${workspaceFolder}/.env",
|
||||
"remoteRoot": "/opt/venv",
|
||||
}
|
||||
],
|
||||
"justMyCode": false,
|
||||
},
|
||||
{
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"preLaunchTask": "npm: start - cvat-ui",
|
||||
"name": "ui.js: debug",
|
||||
"url": "http://localhost:3000",
|
||||
"webRoot": "${workspaceFolder}/cvat-ui",
|
||||
"sourceMaps": true,
|
||||
"sourceMapPathOverrides": {
|
||||
"webpack://cvat/./*": "${workspaceFolder}/cvat-core/*",
|
||||
"webpack:///./*": "${webRoot}/*",
|
||||
"webpack:///src/*": "${webRoot}/*",
|
||||
"webpack:///*": "*",
|
||||
"webpack:///./~/*": "${webRoot}/node_modules/*"
|
||||
},
|
||||
"smartStep": true,
|
||||
},
|
||||
{
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"name": "ui.js: test",
|
||||
"cwd": "${workspaceFolder}/tests",
|
||||
"runtimeExecutable": "${workspaceFolder}/tests/node_modules/.bin/cypress",
|
||||
"args": [
|
||||
"run",
|
||||
"--headless",
|
||||
"--browser",
|
||||
"chrome"
|
||||
],
|
||||
"outputCapture": "std",
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "server: django",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"stopOnEntry": false,
|
||||
"justMyCode": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"env": {
|
||||
"CVAT_SERVERLESS": "1",
|
||||
"ALLOWED_HOSTS": "*",
|
||||
"DJANGO_LOG_SERVER_HOST": "localhost",
|
||||
"DJANGO_LOG_SERVER_PORT": "8282",
|
||||
},
|
||||
"args": [
|
||||
"runserver",
|
||||
"--noreload",
|
||||
"--insecure",
|
||||
"127.0.0.1:7000"
|
||||
],
|
||||
"django": true,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "internalConsole",
|
||||
},
|
||||
{
|
||||
"name": "server: chrome",
|
||||
"type": "pwa-chrome",
|
||||
"request": "launch",
|
||||
"url": "http://localhost:7000/",
|
||||
"disableNetworkCache": true,
|
||||
"trace": true,
|
||||
"showAsyncStacks": true,
|
||||
"pathMapping": {
|
||||
"/static/engine/": "${workspaceFolder}/cvat/apps/engine/static/engine/",
|
||||
"/static/dashboard/": "${workspaceFolder}/cvat/apps/dashboard/static/dashboard/",
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "server: RQ - worker",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"stopOnEntry": false,
|
||||
"justMyCode": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"args": [
|
||||
"rqworker",
|
||||
"--worker-class=cvat.rqworker.SimpleWorker",
|
||||
// high priority
|
||||
"chunks",
|
||||
// normal priority
|
||||
"annotation",
|
||||
"consensus",
|
||||
"export",
|
||||
"import",
|
||||
"quality_reports",
|
||||
"webhooks",
|
||||
// low priority
|
||||
"cleaning",
|
||||
],
|
||||
"django": true,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {
|
||||
"DJANGO_LOG_SERVER_HOST": "localhost",
|
||||
"DJANGO_LOG_SERVER_PORT": "8282"
|
||||
},
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "server: RQ - scheduler",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"stopOnEntry": false,
|
||||
"justMyCode": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/rqscheduler.py",
|
||||
"django": true,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"args": [
|
||||
"-i", "1"
|
||||
],
|
||||
"env": {
|
||||
"DJANGO_LOG_SERVER_HOST": "localhost",
|
||||
"DJANGO_LOG_SERVER_PORT": "8282"
|
||||
},
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "server: migrate",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"args": [
|
||||
"migrate"
|
||||
],
|
||||
"django": true,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "server: sync periodic jobs",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"args": [
|
||||
"syncperiodicjobs"
|
||||
],
|
||||
"django": true,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "server: tests",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"args": [
|
||||
"test",
|
||||
"--settings",
|
||||
"cvat.settings.testing",
|
||||
"cvat/apps",
|
||||
"cvat-cli/"
|
||||
],
|
||||
"django": true,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "server: REST API tests",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"module": "pytest",
|
||||
"args": [
|
||||
"--verbose",
|
||||
"--no-cov", // vscode debugger might not work otherwise
|
||||
"tests/python/rest_api/"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "sdk: tests",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"module": "pytest",
|
||||
"args": [
|
||||
"tests/python/sdk/"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "cli: tests",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"module": "pytest",
|
||||
"args": [
|
||||
"tests/python/cli/"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "api client: Postprocess generator output",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/cvat-sdk/gen/postprocess.py",
|
||||
"args": [
|
||||
"--schema", "${workspaceFolder}/cvat/schema.yml",
|
||||
"--input-path", "${workspaceFolder}/cvat-sdk/cvat_sdk/"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "docs: Postprocess SDK docs",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/site/process_sdk_docs.py",
|
||||
"args": [
|
||||
"--input-dir", "${workspaceFolder}/cvat-sdk/docs/",
|
||||
"--site-root", "${workspaceFolder}/site/",
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "docs: Build docs",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/site/build_docs.py",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal"
|
||||
},
|
||||
{
|
||||
"name": "server: Generate REST API Schema",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"justMyCode": false,
|
||||
"stopOnEntry": false,
|
||||
"python": "${command:python.interpreterPath}",
|
||||
"program": "${workspaceFolder}/manage.py",
|
||||
"args": [
|
||||
"spectacular",
|
||||
"--file",
|
||||
"${workspaceFolder}/cvat/schema.yml"
|
||||
],
|
||||
"django": true,
|
||||
"cwd": "${workspaceFolder}",
|
||||
"env": {},
|
||||
"console": "internalConsole"
|
||||
},
|
||||
{
|
||||
"name": "core.js: debug",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"cwd": "${workspaceFolder}/cvat-core",
|
||||
"runtimeExecutable": "node",
|
||||
"runtimeArgs": [
|
||||
"--nolazy",
|
||||
"--inspect-brk=9230",
|
||||
"src/api.js"
|
||||
],
|
||||
"port": 9230
|
||||
}
|
||||
],
|
||||
"compounds": [
|
||||
{
|
||||
"name": "server: debug",
|
||||
"configurations": [
|
||||
"server: django",
|
||||
"server: RQ - worker",
|
||||
"server: RQ - scheduler",
|
||||
],
|
||||
"stopAll": true,
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"eslint.probe": [
|
||||
"javascript",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"eslint.onIgnoredFiles": "warn",
|
||||
"eslint.workingDirectories": [
|
||||
{
|
||||
"directory": "${cwd}",
|
||||
},
|
||||
{
|
||||
"pattern": "cvat-*"
|
||||
},
|
||||
{
|
||||
"directory": "tests",
|
||||
"!cwd": true
|
||||
}
|
||||
],
|
||||
"npm.exclude": "**/.env/**",
|
||||
"files.trimTrailingWhitespace": true,
|
||||
"python.analysis.exclude": [
|
||||
// VS Code defaults
|
||||
"**/node_modules",
|
||||
"**/__pycache__",
|
||||
".git",
|
||||
|
||||
"cvat-cli/build",
|
||||
"cvat-sdk/build",
|
||||
],
|
||||
"python.defaultInterpreterPath": "${workspaceFolder}/.env/",
|
||||
"python.testing.pytestArgs": [
|
||||
"--rootdir","${workspaceFolder}/tests/"
|
||||
],
|
||||
"python.testing.unittestEnabled": false,
|
||||
"python.testing.pytestEnabled": true,
|
||||
"python.testing.pytestPath": "${workspaceFolder}/.env/bin/pytest",
|
||||
"python.testing.cwd": "${workspaceFolder}/tests",
|
||||
"cSpell.words": [
|
||||
"crowdsourcing"
|
||||
]
|
||||
}
|
||||
Vendored
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
// See https://go.microsoft.com/fwlink/?LinkId=733558
|
||||
// for the documentation about the tasks.json format
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"type": "npm",
|
||||
"script": "start",
|
||||
"path": "cvat-ui/",
|
||||
"label": "npm: start - cvat-ui",
|
||||
"detail": "webpack-dev-server --env.API_URL=http://localhost:7000 --config ./webpack.config.js --mode=development",
|
||||
"promptOnClose": true,
|
||||
"isBackground": true,
|
||||
"problemMatcher": {
|
||||
"owner": "webpack",
|
||||
"severity": "error",
|
||||
"fileLocation": "absolute",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "ERROR in (.*)",
|
||||
"file": 1
|
||||
},
|
||||
{
|
||||
"regexp": "\\((\\d+),(\\d+)\\):(.*)",
|
||||
"line": 1,
|
||||
"column": 2,
|
||||
"message": 3
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": "webpack-dev-server",
|
||||
"endsPattern": "compiled"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
nodeLinker: node-modules
|
||||
+5760
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
# This CITATION.cff file was generated with cffinit.
|
||||
# Visit https://bit.ly/cffinit to generate yours today!
|
||||
|
||||
cff-version: 1.2.0
|
||||
title: Computer Vision Annotation Tool (CVAT)
|
||||
message: >-
|
||||
If you use this software, please cite it using the
|
||||
metadata from this file.
|
||||
type: software
|
||||
authors:
|
||||
- email: support+github@cvat.ai
|
||||
name: CVAT.ai Corporation
|
||||
identifiers:
|
||||
- type: doi
|
||||
value: 10.5281/zenodo.4009388
|
||||
repository-code: 'https://github.com/cvat-ai/cvat'
|
||||
url: 'https://cvat.ai/'
|
||||
abstract: >-
|
||||
Annotate better with CVAT, the industry-leading
|
||||
data engine for machine learning. Used and trusted
|
||||
by teams at any scale, for data of any scale.
|
||||
keywords:
|
||||
- image-labeling-tool
|
||||
- computer-vision-annotation
|
||||
- labeling-tool
|
||||
- image-labeling
|
||||
- semantic-segmentation
|
||||
- annotation-tool
|
||||
- object-detection
|
||||
- image-classification
|
||||
- video-annotation
|
||||
- computer-vision
|
||||
- deep-learning
|
||||
- annotation
|
||||
license: MIT
|
||||
version: 2.25.0
|
||||
date-released: '2023-11-06'
|
||||
|
||||
+209
@@ -0,0 +1,209 @@
|
||||
ARG BASE_IMAGE=ubuntu:24.04
|
||||
|
||||
FROM ${BASE_IMAGE} AS build-image-base
|
||||
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
|
||||
cargo-1.85 \
|
||||
curl \
|
||||
g++ \
|
||||
gcc \
|
||||
git \
|
||||
libhdf5-dev \
|
||||
libldap2-dev \
|
||||
libmp3lame-dev \
|
||||
libsasl2-dev \
|
||||
libxml2-dev \
|
||||
libxmlsec1-dev \
|
||||
libxmlsec1-openssl \
|
||||
make \
|
||||
nasm \
|
||||
pkg-config \
|
||||
python3-dev \
|
||||
python3-pip \
|
||||
&& update-alternatives \
|
||||
--install /usr/bin/rustc rustc /usr/bin/rustc-1.85 185 \
|
||||
--slave /usr/bin/cargo cargo /usr/bin/cargo-1.85 \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
# We build OpenH264, FFmpeg and PyAV in a separate build stage,
|
||||
# because this way Docker can do it in parallel to all the other packages.
|
||||
FROM build-image-base AS build-image-av
|
||||
|
||||
# Compile Openh264 and FFmpeg
|
||||
ARG PREFIX=/opt/ffmpeg
|
||||
ARG PKG_CONFIG_PATH=${PREFIX}/lib/pkgconfig
|
||||
|
||||
ENV FFMPEG_VERSION=8.0 \
|
||||
OPENH264_VERSION=2.6.0
|
||||
|
||||
WORKDIR /tmp/openh264
|
||||
RUN curl -sL https://github.com/cisco/openh264/archive/v${OPENH264_VERSION}.tar.gz --output - | \
|
||||
tar -zx --strip-components=1 && \
|
||||
make -j5 && make install-shared PREFIX=${PREFIX} && make clean
|
||||
|
||||
WORKDIR /tmp/ffmpeg
|
||||
RUN curl -sL https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.gz --output - | \
|
||||
tar -zx --strip-components=1 && \
|
||||
./configure --disable-nonfree --disable-gpl --enable-libopenh264 --enable-libmp3lame \
|
||||
--enable-shared --disable-static --disable-doc --disable-programs --prefix="${PREFIX}" && \
|
||||
make -j5 && make install && make clean
|
||||
|
||||
COPY utils/dataset_manifest/requirements.txt /tmp/utils/dataset_manifest/requirements.txt
|
||||
|
||||
# Since we're using pip-compile-multi, each dependency can only be listed in
|
||||
# one requirements file. In the case of PyAV, that should be
|
||||
# `dataset_manifest/requirements.txt`. Make sure it's actually there,
|
||||
# and then remove everything else.
|
||||
RUN grep -q '^av==' /tmp/utils/dataset_manifest/requirements.txt
|
||||
RUN sed -i '/^av==/!d' /tmp/utils/dataset_manifest/requirements.txt
|
||||
|
||||
RUN --mount=type=cache,target=/root/.cache/pip/http-v2 \
|
||||
python3 -m pip wheel --no-binary=av \
|
||||
-r /tmp/utils/dataset_manifest/requirements.txt \
|
||||
-w /tmp/wheelhouse
|
||||
|
||||
# This stage builds wheels for all dependencies (except PyAV)
|
||||
FROM build-image-base AS build-image
|
||||
|
||||
COPY cvat/requirements/ /tmp/cvat/requirements/
|
||||
COPY utils/dataset_manifest/requirements.txt /tmp/utils/dataset_manifest/requirements.txt
|
||||
|
||||
# Exclude av from the requirements file
|
||||
RUN sed -i '/^av==/d' /tmp/utils/dataset_manifest/requirements.txt
|
||||
|
||||
ARG CVAT_CONFIGURATION="production"
|
||||
|
||||
# https://github.com/SAML-Toolkits/python3-saml#note
|
||||
# Building from source is recommended for lxml and xmlsec to avoid libxml2 version conflicts
|
||||
RUN --mount=type=cache,target=/root/.cache/pip/http-v2 \
|
||||
DATUMARO_HEADLESS=1 python3 -m pip wheel --no-deps --no-binary lxml,xmlsec \
|
||||
-r /tmp/cvat/requirements/${CVAT_CONFIGURATION}.txt \
|
||||
-w /tmp/wheelhouse
|
||||
|
||||
FROM golang:1.26.4 AS build-smokescreen
|
||||
|
||||
RUN git clone --filter=blob:none --no-checkout https://github.com/stripe/smokescreen.git
|
||||
RUN cd smokescreen && git checkout eb1ac09 && go build -o /tmp/smokescreen
|
||||
|
||||
FROM ${BASE_IMAGE}
|
||||
|
||||
ARG http_proxy
|
||||
ARG https_proxy
|
||||
ARG no_proxy
|
||||
ARG socks_proxy
|
||||
ARG TZ="Etc/UTC"
|
||||
|
||||
ENV TERM=xterm \
|
||||
http_proxy=${http_proxy} \
|
||||
https_proxy=${https_proxy} \
|
||||
no_proxy=${no_proxy} \
|
||||
socks_proxy=${socks_proxy} \
|
||||
LANG='C.UTF-8' \
|
||||
LC_ALL='C.UTF-8' \
|
||||
TZ=${TZ}
|
||||
|
||||
ARG USER="django"
|
||||
ARG CVAT_CONFIGURATION="production"
|
||||
ENV DJANGO_SETTINGS_MODULE="cvat.settings.${CVAT_CONFIGURATION}"
|
||||
|
||||
# Install necessary apt packages
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
|
||||
adduser \
|
||||
bzip2 \
|
||||
ca-certificates \
|
||||
curl \
|
||||
git \
|
||||
libgl1 \
|
||||
libgomp1 \
|
||||
libldap2 \
|
||||
libmp3lame0 \
|
||||
libpython3.12t64 \
|
||||
libsasl2-2 \
|
||||
libxml2 \
|
||||
libxmlsec1 \
|
||||
libxmlsec1-openssl \
|
||||
nginx \
|
||||
p7zip-full \
|
||||
poppler-utils \
|
||||
python3 \
|
||||
python3-venv \
|
||||
supervisor \
|
||||
tzdata \
|
||||
unrar \
|
||||
wait-for-it \
|
||||
&& ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \
|
||||
dpkg-reconfigure -f noninteractive tzdata && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install smokescreen
|
||||
COPY --from=build-smokescreen /tmp/smokescreen /usr/local/bin/smokescreen
|
||||
|
||||
# Add a non-root user
|
||||
ENV USER=${USER}
|
||||
ENV HOME /home/${USER}
|
||||
RUN deluser --remove-home ubuntu && \
|
||||
adduser --uid=1000 --shell /bin/bash --disabled-password --gecos "" ${USER}
|
||||
|
||||
ARG CLAM_AV="no"
|
||||
RUN if [ "$CLAM_AV" = "yes" ]; then \
|
||||
apt-get update && \
|
||||
apt-get --no-install-recommends install -yq \
|
||||
clamav \
|
||||
libclamunrar && \
|
||||
sed -i 's/ReceiveTimeout 30/ReceiveTimeout 300/g' /etc/clamav/freshclam.conf && \
|
||||
freshclam && \
|
||||
chown -R ${USER}:${USER} /var/lib/clamav && \
|
||||
rm -rf /var/lib/apt/lists/*; \
|
||||
fi
|
||||
|
||||
# Install wheels from the build image
|
||||
RUN python3 -m venv /opt/venv
|
||||
ENV PATH="/opt/venv/bin:${PATH}"
|
||||
ARG PIP_DISABLE_PIP_VERSION_CHECK=1
|
||||
|
||||
RUN --mount=type=bind,from=build-image,source=/tmp/wheelhouse,target=/mnt/wheelhouse \
|
||||
--mount=type=bind,from=build-image-av,source=/tmp/wheelhouse,target=/mnt/wheelhouse-av \
|
||||
python -m pip install --no-index /mnt/wheelhouse/*.whl /mnt/wheelhouse-av/*.whl
|
||||
|
||||
ENV NUMPROCS=1
|
||||
COPY --from=build-image-av /opt/ffmpeg/lib /usr/lib
|
||||
|
||||
# These variables are required for supervisord substitutions in files
|
||||
# This library allows remote python debugging with VS Code
|
||||
ARG CVAT_DEBUG_ENABLED
|
||||
RUN if [ "${CVAT_DEBUG_ENABLED}" = 'yes' ]; then \
|
||||
python3 -m pip install --no-cache-dir debugpy; \
|
||||
fi
|
||||
|
||||
# Removing pip due to security reasons. See: https://scout.docker.com/vulnerabilities/id/CVE-2018-20225
|
||||
# The vulnerability is dubious and we don't use pip at runtime, but some vulnerability scanners mark it as a high vulnerability,
|
||||
# and it was decided to remove pip from the final image
|
||||
RUN python -m pip uninstall -y pip
|
||||
|
||||
# Install and initialize CVAT, copy all necessary files
|
||||
COPY cvat/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY --chown=${USER} supervisord/ ${HOME}/supervisord
|
||||
COPY --chown=${USER} backend_entrypoint.d/ ${HOME}/backend_entrypoint.d
|
||||
COPY --chown=${USER} manage.py rqscheduler.py backend_entrypoint.sh wait_for_deps.sh ${HOME}/
|
||||
COPY --chown=${USER} utils/ ${HOME}/utils
|
||||
COPY --chown=${USER} cvat/ ${HOME}/cvat
|
||||
COPY --chown=${USER} components/analytics/clickhouse/init.py ${HOME}/components/analytics/clickhouse/init.py
|
||||
|
||||
ARG COVERAGE_PROCESS_START
|
||||
RUN if [ "${COVERAGE_PROCESS_START}" ]; then \
|
||||
echo "import coverage; coverage.process_startup()" > /opt/venv/lib/python3.12/site-packages/coverage_subprocess.pth; \
|
||||
fi
|
||||
|
||||
# RUN all commands below as 'django' user.
|
||||
# Use numeric UID/GID so that the image is compatible with the Kubernetes runAsNonRoot setting.
|
||||
USER 1000:1000
|
||||
WORKDIR ${HOME}
|
||||
|
||||
RUN mkdir -p data share keys logs /tmp/supervisord /tmp/cvat static
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["./backend_entrypoint.sh"]
|
||||
@@ -0,0 +1,21 @@
|
||||
FROM cvat/server:local
|
||||
|
||||
ENV DJANGO_SETTINGS_MODULE=cvat.settings.testing
|
||||
USER root
|
||||
|
||||
RUN apt-get update && \
|
||||
DEBIAN_FRONTEND=noninteractive apt-get --no-install-recommends install -yq \
|
||||
build-essential \
|
||||
python3-dev \
|
||||
&& \
|
||||
rm -rf /var/lib/apt/lists/*;
|
||||
|
||||
COPY cvat/requirements/ /tmp/cvat/requirements/
|
||||
COPY utils/dataset_manifest/requirements.txt /tmp/utils/dataset_manifest/requirements.txt
|
||||
|
||||
RUN python3 -m ensurepip
|
||||
RUN DATUMARO_HEADLESS=1 python3 -m pip install --no-cache-dir -r /tmp/cvat/requirements/testing.txt
|
||||
|
||||
COPY .coveragerc .
|
||||
|
||||
ENTRYPOINT []
|
||||
@@ -0,0 +1,40 @@
|
||||
FROM node:lts-slim AS cvat-ui
|
||||
|
||||
ENV TERM=xterm \
|
||||
LANG='C.UTF-8' \
|
||||
LC_ALL='C.UTF-8'
|
||||
|
||||
# Install dependencies
|
||||
RUN npm install -g corepack
|
||||
COPY .yarnrc.yml /tmp/
|
||||
COPY package.json /tmp/
|
||||
COPY yarn.lock /tmp/
|
||||
COPY cvat-core/package.json /tmp/cvat-core/
|
||||
COPY cvat-canvas/package.json /tmp/cvat-canvas/
|
||||
COPY cvat-canvas3d/package.json /tmp/cvat-canvas3d/
|
||||
COPY cvat-ui/package.json /tmp/cvat-ui/
|
||||
COPY cvat-data/package.json /tmp/cvat-data/
|
||||
|
||||
# Install common dependencies
|
||||
WORKDIR /tmp/
|
||||
RUN DISABLE_HUSKY=1 yarn install --immutable
|
||||
|
||||
# Build source code
|
||||
COPY cvat-data/ /tmp/cvat-data/
|
||||
COPY cvat-core/ /tmp/cvat-core/
|
||||
COPY cvat-canvas3d/ /tmp/cvat-canvas3d/
|
||||
COPY cvat-canvas/ /tmp/cvat-canvas/
|
||||
COPY cvat-ui/ /tmp/cvat-ui/
|
||||
|
||||
ARG CLIENT_PLUGINS
|
||||
ARG SOURCE_MAPS_ENABLED=false
|
||||
|
||||
RUN CLIENT_PLUGINS="${CLIENT_PLUGINS}" \
|
||||
SOURCE_MAPS_ENABLED="${SOURCE_MAPS_ENABLED}" yarn run build:cvat-ui
|
||||
|
||||
FROM nginxinc/nginx-unprivileged:1.31.2-alpine3.23-slim
|
||||
|
||||
# Replace default.conf configuration to remove unnecessary rules
|
||||
COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf
|
||||
COPY cvat-ui/robots.txt /usr/share/nginx/html/
|
||||
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/
|
||||
@@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (C) 2018-2022 Intel Corporation
|
||||
Copyright (C) 2022-2025 CVAT.ai Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,265 @@
|
||||
[](https://app.cvat.ai)
|
||||
# CVAT: Computer Vision Annotation Tool
|
||||
|
||||
[![Release][release-img]][release-url]
|
||||
[![GitHub stars][stars-img]][stars-url]
|
||||
[![License][license-img]][license-url]
|
||||
[![CI][ci-img]][ci-url]
|
||||
[![server pulls][docker-server-pulls-img]][docker-server-image-url]
|
||||
[![ui pulls][docker-ui-pulls-img]][docker-ui-image-url]
|
||||
[![CVAT Online][online-img]][online-url]
|
||||
[![CVAT Enterprise][enterprise-img]][enterprise-url]
|
||||
[![Status][status-img]][status-url]
|
||||
[![Discord][discord-img]][discord-url]
|
||||
[![Docs][docs-img]][docs-url]
|
||||
|
||||
[Website](https://www.cvat.ai/) ·
|
||||
[Docs](https://docs.cvat.ai/docs/) ·
|
||||
[Changelog](https://www.cvat.ai/resources/changelog) ·
|
||||
[Tutorials](https://www.cvat.ai/resources/videos) ·
|
||||
[Academy](https://www.cvat.ai/resources/academy) ·
|
||||
[Blog](https://www.cvat.ai/resources/blog)
|
||||
|
||||
## What is CVAT Community?
|
||||
|
||||
**CVAT Community** is the free, self-hosted open-source edition of [CVAT](https://www.cvat.ai/) — one of
|
||||
the most widely used data annotation platforms for building high-quality visual datasets for
|
||||
computer vision and visual AI.
|
||||
Since 2018, CVAT has become one of the best-known data annotation tools in computer vision, with a
|
||||
large open-source community, millions of Docker pulls, and broad adoption across research and
|
||||
production AI teams.
|
||||
|
||||
CVAT Community supports image, video, and 3D annotation, dataset management, team collaboration, cloud storage
|
||||
integration, developer-friendly SDKs and APIs, and gives your team full control over your data
|
||||
and annotation infrastructure.
|
||||
The platform serves as the foundation of
|
||||
[CVAT Online](https://www.cvat.ai/pricing/cvat-online) and
|
||||
[CVAT Enterprise](https://www.cvat.ai/enterprise), and is actively maintained by the CVAT engineering team.
|
||||
|
||||
Why teams choose CVAT Community:
|
||||
|
||||
- **Own your data:** Run entirely within your own infrastructure. No data leaves your environment.
|
||||
- **AI-powered annotation:** Connect your own ML models for detection, segmentation, and tracking to speed up labeling.
|
||||
- **Team collaboration:** Multi-user and multi-organization support with roles, task assignments,
|
||||
and review workflows.
|
||||
- **MIT-licensed core:** Use, modify, and distribute CVAT Community under the permissive MIT License. Some serverless
|
||||
assets and dependencies may have separate licenses.
|
||||
- **Production-grade:** The foundation of all CVAT commercial products — battle-tested at scale.
|
||||
- **True open-source:** Transparent development, active community, on GitHub since 2018.
|
||||
|
||||
This repository contains the source code and deployment assets for CVAT Community.
|
||||
|
||||
For a fully managed setup, annotation services, or enterprise features, see
|
||||
[CVAT Online](https://www.cvat.ai/pricing/cvat-online),
|
||||
[CVAT Enterprise](https://www.cvat.ai/enterprise) and
|
||||
[CVAT Labeling Services](https://www.cvat.ai/annotation-services).
|
||||
|
||||
## Getting Started
|
||||
|
||||
> 💡 Want to explore CVAT before deploying anything?
|
||||
> **[Try CVAT Online (Free plan)](https://app.cvat.ai)** directly in your browser.
|
||||
> Feature availability and usage limits vary by plan; see
|
||||
> [CVAT Online pricing](https://www.cvat.ai/pricing/cvat-online) for details.
|
||||
|
||||
### Installation
|
||||
|
||||
**Prerequisites:**
|
||||
|
||||
- [Docker Engine](https://docs.docker.com/engine/install/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
- [Git](https://git-scm.com/)
|
||||
|
||||
> 💡 CVAT is primarily tested with Chromium-based browsers (Google Chrome, Microsoft Edge).
|
||||
> Firefox may work with some caveats; Safari/WebKit is not supported.
|
||||
|
||||
**1. Start the default stack**
|
||||
|
||||
Clone the repository and launch the services.
|
||||
|
||||
```bash
|
||||
git clone https://github.com/cvat-ai/cvat
|
||||
cd cvat
|
||||
|
||||
# Optional: set your IP or domain
|
||||
# export CVAT_HOST=your-ip-or-domain
|
||||
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
**2. Create an admin account**
|
||||
|
||||
```bash
|
||||
docker exec -it cvat_server bash -ic 'python3 ~/manage.py createsuperuser'
|
||||
```
|
||||
|
||||
See the [Installation Guide](https://docs.cvat.ai/docs/administration/community/basics/installation/) for full
|
||||
instructions and OS-specific setup.
|
||||
|
||||
**3. Sign in and start labeling**
|
||||
|
||||
- Open [http://localhost:8080](http://localhost:8080) (or your `CVAT_HOST`) in your browser.
|
||||
- Log in with your superuser account.
|
||||
- Create a project or task, upload your data (images, videos, or point clouds), and define labels to start annotating.
|
||||
|
||||
Learn more about annotation tools and workflows in the [CVAT Documentation](https://docs.cvat.ai/docs/) or
|
||||
take our free course – [CVAT Academy](https://www.cvat.ai/resources/academy).
|
||||
|
||||
_For alternative deployments (AWS, Kubernetes, external PostgreSQL, backups, upgrades), see the [Deployment Guides](https://docs.cvat.ai/docs/administration/community/advanced/)._
|
||||
|
||||
## Key Capabilities
|
||||
|
||||
- **[Manual & Auto-labeling](https://docs.cvat.ai/docs/annotation/manual-annotation/):** Annotate images, videos, and
|
||||
3D point clouds with bounding boxes, polygons, masks, keypoints, cuboids, tags, and more. Speed up labeling
|
||||
by connecting your own models for automatic annotation.
|
||||
- **[Task Management](https://docs.cvat.ai/docs/workspace/):** Organize datasets into projects, split them into tasks
|
||||
and jobs, assign work to annotators, and track progress in real time.
|
||||
- **[Collaboration](https://docs.cvat.ai/docs/account_management/user-roles/):** Create organizations, invite teammates,
|
||||
assign roles, and collaborate on annotations with comments and issues.
|
||||
- **[Quality Control](https://docs.cvat.ai/docs/qa-analytics/manual-qa/):** Review annotations, flag issues, compare
|
||||
results across annotators with consensus, and run Ground Truth and Honeypot checks through the server API.
|
||||
- **[Analytics](https://docs.cvat.ai/docs/administration/community/advanced/analytics/):** Monitor user activity,
|
||||
working time by job, events, and server logs with Grafana dashboards.
|
||||
- **[Data Ops & Integrations](https://docs.cvat.ai/docs/dataset_management/export-datasets/):** Export/import in 20+
|
||||
formats (COCO, YOLO, Pascal VOC, KITTI, etc.), connect to cloud storage (S3, Azure, Google Cloud), and automate
|
||||
via REST API and Python SDK.
|
||||
|
||||
Advanced capabilities such as advanced project analytics, quality control UI, built-in auto-labeling with SAM 2
|
||||
and SAM 3, AI agents, SSO, and more are available in [CVAT Online](https://www.cvat.ai/pricing/cvat-online)
|
||||
paid plans (Solo, Team) and [CVAT Enterprise](https://www.cvat.ai/enterprise).
|
||||
|
||||
## Developer Tools
|
||||
|
||||
CVAT is designed for automation. Beyond the Web UI, you can integrate it into your pipelines using:
|
||||
|
||||
- [Python SDK](https://docs.cvat.ai/docs/api_sdk/sdk/): install with `pip install cvat-sdk` and automate task creation,
|
||||
uploads, and exports from Python.
|
||||
- [Command line tool](https://docs.cvat.ai/docs/api_sdk/cli/): install with `pip install cvat-cli`
|
||||
and script common CVAT workflows from the terminal.
|
||||
- [REST API](https://docs.cvat.ai/docs/api_sdk/api/): full programmatic control over CVAT.
|
||||
|
||||
## Data and Formats
|
||||
|
||||
CVAT Community supports image, video, and 3D (point cloud) annotation workflows. You can move data in and out using 20+
|
||||
industry-standard formats: CVAT (XML), COCO (JSON), YOLO (TXT), Ultralytics YOLO (TXT/YAML), Pascal VOC (XML),
|
||||
KITTI (TXT), MOT (TXT), and more.
|
||||
|
||||
[Full list of supported formats.](https://docs.cvat.ai/docs/dataset_management/formats/)
|
||||
|
||||
## ML and AI Models
|
||||
|
||||
CVAT Community supports automatic annotation via pre-built serverless models powered by Nuclio,
|
||||
covering detection, segmentation, pose estimation, and tracking:
|
||||
|
||||
| Model | Framework | Type |
|
||||
| --- | --- | --- |
|
||||
| [Segment Anything (SAM)](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/facebookresearch/sam/nuclio) | PyTorch | Interactor |
|
||||
| [Inside-Outside Guidance (IOG)](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/shiyinzhang/iog/nuclio) | PyTorch | Interactor |
|
||||
| [RetinaNet R101](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/facebookresearch/detectron2/retinanet_r101/nuclio) | PyTorch | Detector |
|
||||
| [HRNet32 Whole Body Pose](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/mmpose/hrnet32/nuclio) | PyTorch | Pose Estimation |
|
||||
| [TransT](https://github.com/cvat-ai/cvat/tree/develop/serverless/pytorch/dschoerk/transt/nuclio) | PyTorch | Tracker |
|
||||
| [YOLO v7](https://github.com/cvat-ai/cvat/tree/develop/serverless/onnx/WongKinYiu/yolov7/nuclio) | ONNX | Detector |
|
||||
| [Mask RCNN Inception ResNet v2](https://github.com/cvat-ai/cvat/tree/develop/serverless/openvino/omz/public/mask_rcnn_inception_resnet_v2_atrous_coco/nuclio) | OpenVINO | Detector |
|
||||
| [Face Detection 0205](https://github.com/cvat-ai/cvat/tree/develop/serverless/openvino/omz/intel/face-detection-0205/nuclio) | OpenVINO | Detector |
|
||||
| [Faster RCNN Inception v2](https://github.com/cvat-ai/cvat/tree/develop/serverless/tensorflow/faster_rcnn_inception_v2_coco/nuclio) | TensorFlow | Detector |
|
||||
|
||||
To enable automatic annotation, add the serverless component to your deployment:
|
||||
|
||||
```bash
|
||||
docker compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d
|
||||
```
|
||||
|
||||
This starts the serverless infrastructure. To make models available in CVAT, install `nuctl` and deploy
|
||||
the functions you need, for example SAM or YOLO, as described in the [Automatic Annotation Guide](https://docs.cvat.ai/docs/annotation/auto-annotation/automatic-annotation/).
|
||||
|
||||
## Which CVAT edition should I choose?
|
||||
|
||||
- **CVAT Online**: the fastest way to try CVAT and start labeling without deployment. Use it to evaluate CVAT in
|
||||
the browser, explore managed features, and move to cost-efficient paid plans when you need more capacity or team
|
||||
workflows.
|
||||
- **CVAT Community**: the MIT-licensed self-hosted edition for teams that want to run CVAT themselves, customize the
|
||||
stack, and control their infrastructure.
|
||||
- **CVAT Enterprise**: for organizations that need CVAT in their own cloud or internal environment, enterprise support,
|
||||
security controls such as SSO, paid platform features, and SLAs.
|
||||
- **Labeling Services**: for teams that want to outsource annotation work to CVAT.ai’s experienced labeling team instead
|
||||
of building an internal labeling operation. Customers get trial access to CVAT Online during the project.
|
||||
|
||||
For detailed plan limits and feature availability, see [CVAT Online pricing](https://www.cvat.ai/pricing/cvat-online),
|
||||
[CVAT Enterprise](https://www.cvat.ai/enterprise), and [Labeling Services](https://www.cvat.ai/annotation-services).
|
||||
|
||||
## Support
|
||||
|
||||
- **Usage questions:** ask the community on [Discord](https://discord.com/invite/fNR3eXfk6C) or
|
||||
Stack Overflow with the `cvat` tag.
|
||||
- **Bugs and feature requests:** use [GitHub Issues](https://github.com/cvat-ai/cvat/issues).
|
||||
- **FAQ:** [Installation, upgrades, troubleshooting](https://docs.cvat.ai/docs/faq/).
|
||||
|
||||
For dedicated support, SLAs, or advanced deployments, consider [CVAT Enterprise](https://www.cvat.ai/enterprise).
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome all contributions: bug reports, documentation fixes, integrations, and code.
|
||||
|
||||
- If you'd like to contribute to CVAT, please refer to our
|
||||
[contribution documentation](https://docs.cvat.ai/docs/contributing/).
|
||||
- For bug reports or feature requests, please use the [GitHub Issues](https://github.com/cvat-ai/cvat/issues) tracker.
|
||||
|
||||
## Security
|
||||
|
||||
- Please review our [Security Policy](https://github.com/cvat-ai/cvat/security/policy) before reporting vulnerabilities.
|
||||
- For sensitive issues, contact: [secure@cvat.ai](mailto:secure@cvat.ai).
|
||||
|
||||
## License
|
||||
|
||||
CVAT Community is released under the MIT License.
|
||||
|
||||
- Code in `/serverless` is also MIT-licensed, but may use third-party assets under separate licenses (including
|
||||
non-commercial). Review those licenses before use.
|
||||
- This software uses FFmpeg libraries under LGPL/GPL. See the Dockerfile and
|
||||
[FFmpeg legal info](https://www.ffmpeg.org/legal.html) for details.
|
||||
|
||||
## Additional Resources
|
||||
|
||||
For the latest product releases, feature walkthroughs, and all things CVAT see:
|
||||
|
||||
<table cellspacing="10" border="0"><tr>
|
||||
<td><a href="https://www.cvat.ai/resources/blog"><img src="site/content/en/images/badge-blog.png" alt="CVAT Blog" height="120"/></a></td>
|
||||
<td><a href="https://www.cvat.ai/resources/academy"><img src="site/content/en/images/badge-academy.png" alt="CVAT Academy" height="120"/></a></td>
|
||||
<td><a href="https://www.cvat.ai/resources/case-studies"><img src="site/content/en/images/badge-case-studies.png" alt="Case Studies" height="120"/></a></td>
|
||||
<td><a href="https://www.youtube.com/@cvat-ai"><img src="site/content/en/images/badge-youtube.png" alt="YouTube" height="120"/></a></td>
|
||||
<td><a href="https://www.linkedin.com/company/cvat-ai"><img src="site/content/en/images/badge-linkedin.png" alt="LinkedIn" height="120"/></a></td>
|
||||
</tr></table>
|
||||
|
||||
<!-- Badges -->
|
||||
|
||||
[ci-img]: https://github.com/cvat-ai/cvat/actions/workflows/main.yml/badge.svg?branch=develop
|
||||
[ci-url]: https://github.com/cvat-ai/cvat/actions
|
||||
|
||||
[docs-img]: https://img.shields.io/badge/docs-docs.cvat.ai-blue?style=flat-square
|
||||
[docs-url]: https://docs.cvat.ai
|
||||
|
||||
[online-img]: https://img.shields.io/badge/CVAT%20Online-app.cvat.ai-success?style=flat-square
|
||||
[online-url]: https://app.cvat.ai
|
||||
|
||||
[release-img]: https://img.shields.io/github/v/release/cvat-ai/cvat?style=flat-square
|
||||
[release-url]: https://github.com/cvat-ai/cvat/releases
|
||||
|
||||
[license-img]: https://img.shields.io/github/license/cvat-ai/cvat?style=flat-square
|
||||
[license-url]: https://github.com/cvat-ai/cvat/blob/develop/LICENSE
|
||||
|
||||
[stars-img]: https://img.shields.io/github/stars/cvat-ai/cvat?style=flat-square
|
||||
[stars-url]: https://github.com/cvat-ai/cvat/stargazers
|
||||
|
||||
[status-img]: https://uptime.betterstack.com/status-badges/v2/monitor/1yl3h.svg
|
||||
[status-url]: https://status.cvat.ai
|
||||
|
||||
[enterprise-img]: https://img.shields.io/badge/CVAT%20Enterprise-cvat.ai-orange?style=flat-square
|
||||
[enterprise-url]: https://www.cvat.ai/enterprise
|
||||
|
||||
[docker-server-pulls-img]: https://img.shields.io/docker/pulls/cvat/server.svg?style=flat-square&label=server%20pulls
|
||||
[docker-server-image-url]: https://hub.docker.com/r/cvat/server
|
||||
|
||||
[docker-ui-pulls-img]: https://img.shields.io/docker/pulls/cvat/ui.svg?style=flat-square&label=UI%20pulls
|
||||
[docker-ui-image-url]: https://hub.docker.com/r/cvat/ui
|
||||
|
||||
[discord-img]: https://img.shields.io/discord/1000789942802337834?label=discord
|
||||
[discord-url]: https://discord.gg/fNR3eXfk6C
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`cvat-ai/cvat`
|
||||
- 原始仓库:https://github.com/cvat-ai/cvat
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
At the moment only the latest release is supported. When you report a security issue,
|
||||
be sure it can be reproduced in the supported version.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you have information about a security issue or vulnerability in the product, please
|
||||
send an e-mail to [secure@cvat.ai](mailto:secure+github@cvat.ai).
|
||||
|
||||
Please provide as much information as possible, including:
|
||||
|
||||
- The products and versions affected
|
||||
- Detailed description of the vulnerability
|
||||
- Information on known exploits
|
||||
- A member of the CVAT.ai Product Security Team will review your e-mail and contact you to
|
||||
collaborate on resolving the issue.
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
files.extend-exclude = [
|
||||
"ai-models/agents_deployment/transformers/supported_models",
|
||||
"cvat-ui/src/assets/opencv_4.8.0.js",
|
||||
"cvat-data/src/ts/3rdparty/*.js",
|
||||
"site/themes/docsy/",
|
||||
"*.log",
|
||||
"*.min.js",
|
||||
"*.patch",
|
||||
]
|
||||
default.extend-ignore-re = [
|
||||
# test passwords
|
||||
"\"md5.*?\"",
|
||||
|
||||
# Line ignore with trailing "# spellchecker:disable-line"
|
||||
# or trailing "// spellchecker:disable-line"
|
||||
"(?Rm)^.*(#|//)\\s*spellchecker:disable-line$",
|
||||
|
||||
# Line block with "# spellchecker:<on|off>"
|
||||
# or "// spellchecker:<on|off>"
|
||||
"(?s)(#|//)\\s*spellchecker:off.*?\\n\\s*(#|//)\\s*spellchecker:on"
|
||||
# Taken from https://github.com/crate-ci/typos/blob/master/docs/reference.md#example-configurations
|
||||
|
||||
]
|
||||
|
||||
[default.extend-words]
|
||||
AVOD = "AVOD"
|
||||
NAM = "NAM"
|
||||
blok = "block"
|
||||
cheeck = "check"
|
||||
configurate = "configure"
|
||||
convertation = "conversion"
|
||||
convertor = "converter"
|
||||
cubiod = "cuboid"
|
||||
cuboiud = "cuboid"
|
||||
cuccessfully = "successfully"
|
||||
firts = "first"
|
||||
leafs = "leafs"
|
||||
nd = "nd"
|
||||
onject = ""
|
||||
perameters = "parameters"
|
||||
segway = "segway"
|
||||
splitted = "splitted"
|
||||
succefull = "successful"
|
||||
tesk = "task"
|
||||
tpic = "tpic"
|
||||
trak = "trak"
|
||||
typ = "type"
|
||||
@@ -0,0 +1,3 @@
|
||||
### This folder was moved to a separate repository:
|
||||
|
||||
#### <https://github.com/cvat-ai/cvat-models.git>
|
||||
@@ -0,0 +1,12 @@
|
||||
(
|
||||
["annotation"]="smokescreen"
|
||||
["chunks"]="smokescreen"
|
||||
["consensus"]=""
|
||||
["export"]="smokescreen"
|
||||
["import"]="smokescreen clamav"
|
||||
["quality_reports"]=""
|
||||
["cleaning"]="rqscheduler"
|
||||
["webhooks"]="smokescreen"
|
||||
["notifications"]=""
|
||||
["server"]="smokescreen clamav"
|
||||
)
|
||||
Executable
+209
@@ -0,0 +1,209 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
|
||||
fail() {
|
||||
printf >&2 "%s: %s\n" "$0" "$1"
|
||||
exit 1
|
||||
}
|
||||
|
||||
wait_for_db() {
|
||||
wait-for-it "${CVAT_POSTGRES_HOST}:${CVAT_POSTGRES_PORT:-5432}" -t 0
|
||||
}
|
||||
|
||||
wait_for_redis_inmem() {
|
||||
wait-for-it "${CVAT_REDIS_INMEM_HOST}:${CVAT_REDIS_INMEM_PORT:-6379}" -t 0
|
||||
}
|
||||
|
||||
wait_for_clickhouse() {
|
||||
wait-for-it "${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT:-8123}" -t 0
|
||||
}
|
||||
|
||||
account_for_internal_proxy() {
|
||||
CVAT_NUM_PROXIES="${CVAT_NUM_PROXIES:-0}"
|
||||
|
||||
if ! [[ "$CVAT_NUM_PROXIES" =~ ^[0-9]+$ ]]; then
|
||||
fail "CVAT_NUM_PROXIES must be a non-negative integer"
|
||||
fi
|
||||
|
||||
# The user-facing setting counts proxies before the backend container.
|
||||
# Add the internal nginx hop that proxies requests to uvicorn.
|
||||
export CVAT_NUM_PROXIES=$((CVAT_NUM_PROXIES + 1))
|
||||
}
|
||||
|
||||
cmd_bash() {
|
||||
exec bash "$@"
|
||||
}
|
||||
|
||||
cmd_init() {
|
||||
wait_for_db
|
||||
~/manage.py migrate
|
||||
|
||||
wait_for_redis_inmem
|
||||
~/manage.py migrateredis
|
||||
~/manage.py syncperiodicjobs
|
||||
|
||||
if [[ "${CVAT_ANALYTICS:-0}" == "1" ]]; then
|
||||
wait_for_clickhouse
|
||||
python components/analytics/clickhouse/init.py
|
||||
fi
|
||||
}
|
||||
|
||||
_load_component_config() {
|
||||
declare -gA merged_config=()
|
||||
for config_file in ~/backend_entrypoint.d/*.conf; do
|
||||
declare -A config=$(cat $config_file)
|
||||
for key in "${!config[@]}"; do
|
||||
if [[ -n ${merged_config[$key]+_} ]]; then
|
||||
fail "Duplicated component definition: $key"
|
||||
fi
|
||||
merged_config[$key]=${config[$key]}
|
||||
done
|
||||
unset config
|
||||
done
|
||||
}
|
||||
|
||||
_get_includes() {
|
||||
extra_configs=()
|
||||
for key in "$@"; do
|
||||
if [[ -z ${merged_config[$key]+_} ]]; then
|
||||
fail "Unexpected component: $key"
|
||||
fi
|
||||
|
||||
for include in ${merged_config["$key"]}; do
|
||||
if ! [[ ${extra_configs[@]} =~ $include ]] && \
|
||||
( ! [[ "$include" == "clamav" ]] || [[ "${CLAM_AV:-}" == "yes" ]] ); then
|
||||
extra_configs+=("$include")
|
||||
fi
|
||||
done
|
||||
done
|
||||
|
||||
if [ ${#extra_configs[@]} -gt 0 ]; then
|
||||
printf 'reusable/%s.conf ' "${extra_configs[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
_get_reusable_includes() {
|
||||
extra_configs=()
|
||||
for include in "$@"; do
|
||||
if ! [ -r "$HOME/supervisord/reusable/$include.conf" ]; then
|
||||
fail "Unexpected supervisor include: $include"
|
||||
fi
|
||||
|
||||
if ! [[ ${extra_configs[@]} =~ $include ]]; then
|
||||
extra_configs+=("$include")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#extra_configs[@]} -gt 0 ]; then
|
||||
printf 'reusable/%s.conf ' "${extra_configs[@]}"
|
||||
fi
|
||||
}
|
||||
|
||||
cmd_run() {
|
||||
if [ "$#" -eq 0 ]; then
|
||||
fail "run: at least 1 argument is expected"
|
||||
fi
|
||||
|
||||
component="$1"
|
||||
_load_component_config
|
||||
|
||||
case "$component" in
|
||||
server|worker|worker-pool|nginx) ;;
|
||||
*) fail "Unexpected run component: $component" ;;
|
||||
esac
|
||||
|
||||
if [ "$component" = "nginx" ]; then
|
||||
exec supervisord -c "supervisord/nginx.conf"
|
||||
fi
|
||||
|
||||
if [ "$component" = "server" ]; then
|
||||
account_for_internal_proxy
|
||||
~/manage.py collectstatic --no-input
|
||||
fi
|
||||
|
||||
wait_for_db
|
||||
|
||||
echo "waiting for migrations to complete..."
|
||||
while ! ~/manage.py migrate --check; do
|
||||
sleep 10
|
||||
done
|
||||
|
||||
wait_for_redis_inmem
|
||||
echo "waiting for Redis migrations to complete..."
|
||||
while ! ~/manage.py migrateredis --check; do
|
||||
sleep 10
|
||||
done
|
||||
|
||||
supervisord_includes=""
|
||||
postgres_app_name="cvat:$component"
|
||||
if [ "$component" = "server" ]; then
|
||||
supervisord_includes="$(_get_includes "server")$(_get_reusable_includes "${@:2}")"
|
||||
elif [ "$component" = "worker" ] || [ "$component" = "worker-pool" ]; then
|
||||
if [ "$#" -eq 1 ]; then
|
||||
fail "run worker: expected at least 1 queue name"
|
||||
fi
|
||||
|
||||
queues=()
|
||||
extra_flags=()
|
||||
for arg in "${@:2}"; do
|
||||
if [[ "$arg" == --* ]]; then
|
||||
extra_flags+=("$arg")
|
||||
else
|
||||
queues+=("$arg")
|
||||
fi
|
||||
done
|
||||
|
||||
if [ ${#queues[@]} -eq 0 ]; then
|
||||
fail "run worker: expected at least 1 queue name"
|
||||
fi
|
||||
|
||||
queue_list="${queues[*]}"
|
||||
echo "Workers to run: $queue_list"
|
||||
if [ ${#extra_flags[@]} -gt 0 ]; then
|
||||
echo "Extra rqworker flags: ${extra_flags[*]}"
|
||||
fi
|
||||
export CVAT_QUEUES=$queue_list
|
||||
export CVAT_RQWORKER_EXTRA_FLAGS="${extra_flags[*]:-}"
|
||||
|
||||
postgres_app_name+=":${queue_list// /+}"
|
||||
|
||||
supervisord_includes=$(_get_includes "${queues[@]}")
|
||||
fi
|
||||
echo "Additional supervisor configs that will be included: $supervisord_includes"
|
||||
|
||||
export CVAT_POSTGRES_APPLICATION_NAME=$postgres_app_name
|
||||
export CVAT_SUPERVISORD_INCLUDES=$supervisord_includes
|
||||
|
||||
exec supervisord -c "supervisord/$component.conf"
|
||||
}
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo >&2 "$0: at least one subcommand required"
|
||||
echo >&2 ""
|
||||
echo >&2 "available subcommands:"
|
||||
echo >&2 " bash <bash args...>"
|
||||
echo >&2 " init"
|
||||
echo >&2 " run server [additional components]"
|
||||
echo >&2 " run nginx"
|
||||
echo >&2 " run worker <list of queues>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for init_script in /etc/cvat/init.d/*; do
|
||||
if [ -r "$init_script" ]; then
|
||||
. "$init_script"
|
||||
fi
|
||||
done
|
||||
|
||||
while [ $# -ne 0 ]; do
|
||||
if [ "$(type -t "cmd_$1")" != "function" ]; then
|
||||
fail "unknown subcommand: $1"
|
||||
fi
|
||||
|
||||
cmd_name="$1"
|
||||
|
||||
shift
|
||||
|
||||
"cmd_$cmd_name" "$@"
|
||||
done
|
||||
@@ -0,0 +1,5 @@
|
||||
### Removed
|
||||
|
||||
- \[SDK, CLI\] Removed server schema autodetection, previously deprecated
|
||||
in v2.36.0
|
||||
(<https://github.com/cvat-ai/cvat/pull/10673>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Added
|
||||
|
||||
- Added region of interest support for automatic annotation detector and interactor functions
|
||||
(<https://github.com/cvat-ai/cvat/pull/10794>)
|
||||
@@ -0,0 +1,6 @@
|
||||
### Added
|
||||
|
||||
- \[SDK\] `list()` methods on high-level proxy classes now accept filter parameters
|
||||
via a composable `F` field DSL and keyword lookups,
|
||||
which are compiled into the server-side `filter` query parameter automatically
|
||||
(<https://github.com/cvat-ai/cvat/pull/10778>)
|
||||
@@ -0,0 +1,8 @@
|
||||
### Added
|
||||
|
||||
- \[SDK\] Persistent authentication support with saved profiles:
|
||||
`make_client_from_profile`, `make_client_from_cli`, and
|
||||
`resolve_server_host` helpers let SDK users save server + credential profiles
|
||||
to disk and instantiate a `Client` from a profile name without re-entering
|
||||
credentials
|
||||
(<https://github.com/cvat-ai/cvat/pull/10824>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
|
||||
- Requests API no longer returns a `null` status when a job's status cannot be
|
||||
read from Redis (e.g. the job expired) (<https://github.com/cvat-ai/cvat/pull/10849>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
|
||||
- Fix snap to contour crashes when drawing stops (drawCircles of undefined / array of null)
|
||||
(<https://github.com/cvat-ai/cvat/pull/10848>)
|
||||
@@ -0,0 +1,5 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed task creation from images hosted on Azure Blob Storage when
|
||||
at least one image's header doesn't fit into 2047 bytes
|
||||
(<https://github.com/cvat-ai/cvat/pull/10851>)
|
||||
@@ -0,0 +1,5 @@
|
||||
### Fixed
|
||||
|
||||
- Requesting task metadata (`GET /api/tasks/<id>/data/meta`) before any
|
||||
data was uploaded to the task now returns 400 with a clear message.
|
||||
(<https://github.com/cvat-ai/cvat/pull/10855>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
|
||||
- Client error `Cannot read properties of undefined (reading 'occluded')` while copying masks
|
||||
(<https://github.com/cvat-ai/cvat/pull/10853>)
|
||||
@@ -0,0 +1,6 @@
|
||||
### Fixed
|
||||
|
||||
- \[Server API\] Added missing input validation for the `source` field of annotations
|
||||
in the `/api/tasks/<id>/annotations` and `/api/jobs/<id>/annotations` endpoints.
|
||||
The existing invalid values are returned unmodified in the server responses.
|
||||
(<https://github.com/cvat-ai/cvat/pull/10521>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Removed
|
||||
|
||||
- Removed the unused `PATCH /api/invitations/{key}` Server API operation.
|
||||
(<https://github.com/cvat-ai/cvat/pull/10850>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
|
||||
- Fixed AI tools tracking when starting tracking from multiple drawn rectangles
|
||||
(<https://github.com/cvat-ai/cvat/pull/10860>)
|
||||
@@ -0,0 +1,5 @@
|
||||
### Fixed
|
||||
|
||||
- Client error thrown
|
||||
when listing lambda functions failed (e.g. network
|
||||
error or timeout) (<https://github.com/cvat-ai/cvat/pull/10857>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
|
||||
- Fix pagination being reset when refreshing or going "back" to a resources page
|
||||
(<https://github.com/cvat-ai/cvat/pull/10858>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
|
||||
- Make loading previews on project/job cards clickable to allow opening the resource regardless of the preview state
|
||||
(<https://github.com/cvat-ai/cvat/pull/10859>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
|
||||
- Prevent requests cards content overflow
|
||||
(<https://github.com/cvat-ai/cvat/pull/10862>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### Fixed
|
||||
|
||||
- Improved performance of annotation responses.
|
||||
(<https://github.com/cvat-ai/cvat/pull/10867>)
|
||||
@@ -0,0 +1,4 @@
|
||||
### {{ config.categories | join('|') }} <!-- pick one -->
|
||||
|
||||
- Describe your change here...
|
||||
(<https://github.com/cvat-ai/cvat/pull/XXXX>)
|
||||
@@ -0,0 +1,6 @@
|
||||
[scriv]
|
||||
categories = Added, Changed, Deprecated, Removed, Fixed, Security
|
||||
entry_title_template = \[{{ version }}\] - {{ date.strftime('%%Y-%%m-%%d') }}
|
||||
format = md
|
||||
md_header_level = 2
|
||||
new_fragment_template = file: fragment.j2
|
||||
@@ -0,0 +1,37 @@
|
||||
<!--
|
||||
Copyright (C) CVAT.ai Corporation
|
||||
|
||||
SPDX-License-Identifier: MIT
|
||||
-->
|
||||
|
||||
# Clickhouse DB migrations
|
||||
|
||||
This directory contains migrations for the Clickhouse DB used by CVAT.
|
||||
|
||||
## Implementation details
|
||||
|
||||
The Clickhouse documentation explains options to customize DB loading here:
|
||||
<https://clickhouse.com/docs/install/docker#how-to-extend-image>
|
||||
In the default Docker image, Clickhouse only runs these files if the DB is not initialized.
|
||||
This is not the desired behavior for us, because it doesn't allow us to add new migrations
|
||||
added after the DB is initialized. Instead, we run the migrations on each start using a custom
|
||||
script. Therefore **make sure the migrations can be safely called multiple times**
|
||||
on an existing DB.
|
||||
|
||||
The directory contains scripts for the Clickhouse instance:
|
||||
- `init.py`: initializes the DB and applies required migrations
|
||||
|
||||
## Adding a migration
|
||||
|
||||
To add a migration, create a new `migration_NNN_<custom name>` function in the `init.py` script
|
||||
and add it to the list of migrations.
|
||||
|
||||
Recommendations on the migrations:
|
||||
- **Make sure the migrations can be safely called multiple times**
|
||||
and do not fail on repeated calls on the initialized DB.
|
||||
Typically, this can be achieved by using "IF NOT EXISTS" at some point.
|
||||
- Avoid heavy data migrations as much as possible. In most cases, changing only the table
|
||||
columns should be enough and you won't need a data migration.
|
||||
|
||||
As follows from the recommendations above, the current implementation is limited
|
||||
to simple migrations only.
|
||||
@@ -0,0 +1,117 @@
|
||||
# Copyright (C) CVAT.ai Corporation
|
||||
#
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import clickhouse_connect
|
||||
import clickhouse_connect.driver.client
|
||||
|
||||
CLICKHOUSE_HOST_ENV_VAR = "CLICKHOUSE_HOST"
|
||||
CLICKHOUSE_PORT_ENV_VAR = "CLICKHOUSE_PORT"
|
||||
CLICKHOUSE_DB_ENV_VAR = "CLICKHOUSE_DB"
|
||||
CLICKHOUSE_USER_ENV_VAR = "CLICKHOUSE_USER" # nosec
|
||||
CLICKHOUSE_PASSWORD_ENV_VAR = "CLICKHOUSE_PASSWORD" # nosec
|
||||
|
||||
|
||||
def clear_db(client: clickhouse_connect.driver.client.Client):
|
||||
client.query(f"TRUNCATE DATABASE IF EXISTS {client.database};")
|
||||
|
||||
|
||||
def create_db(client: clickhouse_connect.driver.client.Client, db_name: str):
|
||||
client.query(f"CREATE DATABASE IF NOT EXISTS {db_name};")
|
||||
|
||||
|
||||
def migration_000_initial(client: clickhouse_connect.driver.client.Client):
|
||||
client.query(textwrap.dedent("""\
|
||||
CREATE TABLE IF NOT EXISTS events
|
||||
(
|
||||
`scope` String NOT NULL,
|
||||
`obj_name` String NULL,
|
||||
`obj_id` UInt64 NULL,
|
||||
`obj_val` String NULL,
|
||||
`source` String NOT NULL,
|
||||
`timestamp` DateTime64(3, 'Etc/UTC') NOT NULL,
|
||||
`count` UInt16 NULL,
|
||||
`duration` UInt32 DEFAULT toUInt32(0),
|
||||
`project_id` UInt64 NULL,
|
||||
`task_id` UInt64 NULL,
|
||||
`job_id` UInt64 NULL,
|
||||
`user_id` UInt64 NULL,
|
||||
`user_name` String NULL,
|
||||
`user_email` String NULL,
|
||||
`org_id` UInt64 NULL,
|
||||
`org_slug` String NULL,
|
||||
`payload` String NULL
|
||||
)
|
||||
ENGINE = MergeTree
|
||||
PARTITION BY toYYYYMM(timestamp)
|
||||
ORDER BY (timestamp)
|
||||
SETTINGS index_granularity = 8192;
|
||||
"""))
|
||||
|
||||
|
||||
def migration_001_add_access_token(client: clickhouse_connect.driver.client.Client):
|
||||
client.query(textwrap.dedent("""\
|
||||
ALTER TABLE events
|
||||
ADD COLUMN IF NOT EXISTS
|
||||
`access_token_id` Nullable(UInt64) DEFAULT NULL
|
||||
"""))
|
||||
|
||||
|
||||
def migration_002_add_remote_addr(client: clickhouse_connect.driver.client.Client):
|
||||
client.query(textwrap.dedent("""\
|
||||
ALTER TABLE events
|
||||
ADD COLUMN IF NOT EXISTS
|
||||
`remote_addr` Nullable(String) DEFAULT NULL
|
||||
"""))
|
||||
|
||||
|
||||
migrations = [
|
||||
migration_000_initial,
|
||||
migration_001_add_access_token,
|
||||
migration_002_add_remote_addr,
|
||||
]
|
||||
|
||||
|
||||
def main(args: list[str] | None = None) -> int:
|
||||
parser = ArgumentParser(description="Initializes the DB and applies migrations")
|
||||
parser.add_argument("--host", help=f"Server host (env: {CLICKHOUSE_HOST_ENV_VAR})")
|
||||
parser.add_argument("--port", help=f"Server port (env: {CLICKHOUSE_PORT_ENV_VAR})")
|
||||
parser.add_argument("--db", help=f"Database name (env: {CLICKHOUSE_DB_ENV_VAR})")
|
||||
parser.add_argument("--user", help=f"Username (env: {CLICKHOUSE_USER_ENV_VAR})")
|
||||
parser.add_argument("--password", help=f"Password (env: {CLICKHOUSE_PASSWORD_ENV_VAR})")
|
||||
parser.add_argument(
|
||||
"--clear", action="store_true", help="Clear the existing DB and initialize a new one"
|
||||
)
|
||||
parsed_args = parser.parse_args(args)
|
||||
|
||||
db_name = parsed_args.db or os.getenv(CLICKHOUSE_DB_ENV_VAR)
|
||||
|
||||
with clickhouse_connect.get_client(
|
||||
host=parsed_args.host or os.getenv(CLICKHOUSE_HOST_ENV_VAR),
|
||||
port=parsed_args.port or os.getenv(CLICKHOUSE_PORT_ENV_VAR),
|
||||
username=parsed_args.user or os.getenv(CLICKHOUSE_USER_ENV_VAR),
|
||||
password=parsed_args.password or os.getenv(CLICKHOUSE_PASSWORD_ENV_VAR),
|
||||
) as client:
|
||||
create_db(client, db_name)
|
||||
client.database = db_name # client can't be created with non existing DB
|
||||
|
||||
if parsed_args.clear:
|
||||
print("Clearing the DB")
|
||||
clear_db(client)
|
||||
print("done")
|
||||
|
||||
for migration_func in migrations:
|
||||
print("Applying migration", migration_func.__name__)
|
||||
migration_func(client)
|
||||
print("done")
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv[1:]))
|
||||
@@ -0,0 +1,760 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-GrYlRd"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 90,
|
||||
"gradientMode": "scheme",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"metrics": [
|
||||
{
|
||||
"aggregation": "count",
|
||||
"field": "*"
|
||||
}
|
||||
],
|
||||
"mode": "trend",
|
||||
"orderBy": [],
|
||||
"table": "events",
|
||||
"timeField": "timestamp",
|
||||
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"format": 0,
|
||||
"meta": {
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"metrics": [
|
||||
{
|
||||
"aggregation": "count",
|
||||
"field": "*"
|
||||
}
|
||||
],
|
||||
"mode": "trend",
|
||||
"orderBy": [],
|
||||
"table": "events",
|
||||
"timeField": "timestamp",
|
||||
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
|
||||
}
|
||||
},
|
||||
"queryType": "sql",
|
||||
"rawSql": "SELECT $__timeInterval(timestamp) as time, count(*)\r\nFROM events\r\nWHERE $__timeFilter(timestamp)\r\nAND scope IN (${scopes})\r\nAND source IN (${sources})\r\nAND (-1 IN (${users}) OR user_id IN (${users}))\r\nAND (-1 IN (${organizations}) OR org_id IN (${organizations}))\r\nAND (-1 IN (${projects}) OR project_id IN (${projects}))\r\nAND (-1 IN (${tasks}) OR task_id IN (${tasks}))\r\nAND (-1 IN (${jobs}) OR job_id IN (${jobs}))\r\nGROUP BY time ORDER BY time ASC",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Overall Activity",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"inspect": true
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"matcher": {
|
||||
"id": "byName",
|
||||
"options": "timestamp"
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.width",
|
||||
"value": 158
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 23,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 7
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"footer": {
|
||||
"enablePagination": true,
|
||||
"fields": "",
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": false
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "9.3.6",
|
||||
"targets": [
|
||||
{
|
||||
"builderOptions": {
|
||||
"fields": [
|
||||
"*"
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"condition": "AND",
|
||||
"filterType": "custom",
|
||||
"key": "timestamp",
|
||||
"operator": "WITH IN DASHBOARD TIME RANGE",
|
||||
"type": "DateTime64(3, 'Etc/UTC')",
|
||||
"value": "TODAY"
|
||||
},
|
||||
{
|
||||
"condition": "AND",
|
||||
"filterType": "custom",
|
||||
"key": "scope",
|
||||
"operator": "IN",
|
||||
"type": "String",
|
||||
"value": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"mode": "list",
|
||||
"orderBy": [
|
||||
{
|
||||
"dir": "ASC",
|
||||
"name": "timestamp"
|
||||
}
|
||||
],
|
||||
"table": "events"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"format": 1,
|
||||
"meta": {
|
||||
"builderOptions": {
|
||||
"fields": [
|
||||
"*"
|
||||
],
|
||||
"filters": [
|
||||
{
|
||||
"condition": "AND",
|
||||
"filterType": "custom",
|
||||
"key": "timestamp",
|
||||
"operator": "WITH IN DASHBOARD TIME RANGE",
|
||||
"type": "DateTime64(3, 'Etc/UTC')",
|
||||
"value": "TODAY"
|
||||
},
|
||||
{
|
||||
"condition": "AND",
|
||||
"filterType": "custom",
|
||||
"key": "scope",
|
||||
"operator": "IN",
|
||||
"type": "String",
|
||||
"value": [
|
||||
""
|
||||
]
|
||||
}
|
||||
],
|
||||
"mode": "list",
|
||||
"orderBy": [
|
||||
{
|
||||
"dir": "ASC",
|
||||
"name": "timestamp"
|
||||
}
|
||||
],
|
||||
"table": "events"
|
||||
}
|
||||
},
|
||||
"queryType": "sql",
|
||||
"rawSql": "SELECT * \r\nFROM events \r\nWHERE $__timeFilter(timestamp)\r\n AND scope IN (${scopes})\r\n AND source IN (${sources})\r\n AND (-1 IN (${users}) OR user_id IN (${users}))\r\n AND (' ' IN (${usernames}) OR user_name IN (${usernames}))\r\n AND (-1 IN (${organizations}) OR org_id IN (${organizations}))\r\n AND (-1 IN (${projects}) OR project_id IN (${projects}))\r\n AND (-1 IN (${tasks}) OR task_id IN (${tasks}))\r\n AND (-1 IN (${jobs}) OR job_id IN (${jobs}))\r\nORDER BY timestamp DESC\r\nLIMIT 1000",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "All events",
|
||||
"type": "table"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisGridShow": false,
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "percentage",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 0,
|
||||
"y": 30
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 0.51,
|
||||
"groupWidth": 0.7,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": false
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"showValue": "always",
|
||||
"stacking": "none",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xField": "browser",
|
||||
"xTickLabelRotation": 0,
|
||||
"xTickLabelSpacing": 0
|
||||
},
|
||||
"pluginVersion": "9.3.6",
|
||||
"targets": [
|
||||
{
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"limit": 100,
|
||||
"mode": "list",
|
||||
"orderBy": [],
|
||||
"table": "events"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"format": 1,
|
||||
"meta": {
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"limit": 100,
|
||||
"mode": "list",
|
||||
"orderBy": [],
|
||||
"table": "events"
|
||||
}
|
||||
},
|
||||
"queryType": "sql",
|
||||
"rawSql": "SELECT\r\n browser,\r\n count()\r\nFROM\r\n(\r\n SELECT\r\n concat(JSON_VALUE(payload, '$.platform.name'), ' ', JSON_VALUE(payload, '$.platform.version')) AS browser,\r\n user_id\r\n FROM cvat.events\r\n WHERE $__timeFilter(timestamp) AND (scope = 'load:cvat') AND (browser != ' ')\r\n GROUP BY\r\n user_id,\r\n browser\r\n)\r\nGROUP BY browser\r\nORDER BY count() DESC",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Browser",
|
||||
"type": "barchart"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisGridShow": false,
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"fillOpacity": 80,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineWidth": 0,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "percentage",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 4,
|
||||
"w": 12,
|
||||
"x": 12,
|
||||
"y": 30
|
||||
},
|
||||
"id": 8,
|
||||
"options": {
|
||||
"barRadius": 0,
|
||||
"barWidth": 0.51,
|
||||
"groupWidth": 0.7,
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": false
|
||||
},
|
||||
"orientation": "horizontal",
|
||||
"showValue": "always",
|
||||
"stacking": "none",
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
},
|
||||
"xField": "os",
|
||||
"xTickLabelRotation": 0,
|
||||
"xTickLabelSpacing": 0
|
||||
},
|
||||
"pluginVersion": "9.3.6",
|
||||
"targets": [
|
||||
{
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"limit": 100,
|
||||
"mode": "list",
|
||||
"orderBy": [],
|
||||
"table": "events"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"format": 1,
|
||||
"meta": {
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"limit": 100,
|
||||
"mode": "list",
|
||||
"orderBy": [],
|
||||
"table": "events"
|
||||
}
|
||||
},
|
||||
"queryType": "sql",
|
||||
"rawSql": "SELECT\r\n os,\r\n count()\r\nFROM\r\n(\r\n SELECT\r\n JSON_VALUE(payload, '$.platform.os') AS os,\r\n user_id\r\n FROM cvat.events\r\n WHERE $__timeFilter(timestamp) AND (scope = 'load:cvat') AND (os != '')\r\n GROUP BY\r\n user_id,\r\n os\r\n)\r\nGROUP BY os\r\nORDER BY count() DESC",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "OS",
|
||||
"type": "barchart"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"allValue": "",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT scope\nFROM events\nWHERE $__timeFilter(timestamp)",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Scope",
|
||||
"multi": true,
|
||||
"name": "scopes",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT scope\nFROM events\nWHERE $__timeFilter(timestamp)",
|
||||
"refresh": 2,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT source\nFROM events\nWHERE $__timeFilter(timestamp)",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Source",
|
||||
"multi": true,
|
||||
"name": "sources",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT source\nFROM events\nWHERE $__timeFilter(timestamp)",
|
||||
"refresh": 2,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "-1",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT user_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND user_id IS NOT NULL",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "User",
|
||||
"multi": true,
|
||||
"name": "users",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT user_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND user_id IS NOT NULL",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "' '",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"definition": "SELECT DISTINCT user_name\nFROM events\nWHERE $__timeFilter(timestamp)",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Username",
|
||||
"multi": true,
|
||||
"name": "usernames",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT user_name\nFROM events\nWHERE $__timeFilter(timestamp)",
|
||||
"refresh": 2,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "-1",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT project_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND project_id IS NOT NULL",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Project",
|
||||
"multi": true,
|
||||
"name": "projects",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT project_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND project_id IS NOT NULL",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "-1",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT task_id\nFROM events\nWHERE $__timeFilter(timestamp) \n AND task_id IS NOT NULL",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Task",
|
||||
"multi": true,
|
||||
"name": "tasks",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT task_id\nFROM events\nWHERE $__timeFilter(timestamp) \n AND task_id IS NOT NULL",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "-1",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT job_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND job_id IS NOT NULL",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Job",
|
||||
"multi": true,
|
||||
"name": "jobs",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT job_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND job_id IS NOT NULL",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "-1",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT org_id\nFROM events\nWHERE $__timeFilter(timestamp)\nAND org_id IS NOT NULL",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Organization",
|
||||
"multi": true,
|
||||
"name": "organizations",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT org_id\nFROM events\nWHERE $__timeFilter(timestamp)\nAND org_id IS NOT NULL",
|
||||
"refresh": 1,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-7d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "All events",
|
||||
"uid": "EIGSTDAVz",
|
||||
"version": 1,
|
||||
"weekStart": ""
|
||||
}
|
||||
@@ -0,0 +1,532 @@
|
||||
{
|
||||
"annotations": {
|
||||
"list": [
|
||||
{
|
||||
"builtIn": 1,
|
||||
"datasource": {
|
||||
"type": "grafana",
|
||||
"uid": "-- Grafana --"
|
||||
},
|
||||
"enable": true,
|
||||
"hide": true,
|
||||
"iconColor": "rgba(0, 211, 255, 1)",
|
||||
"name": "Annotations & Alerts",
|
||||
"target": {
|
||||
"limit": 100,
|
||||
"matchAny": false,
|
||||
"tags": [],
|
||||
"type": "dashboard"
|
||||
},
|
||||
"type": "dashboard"
|
||||
}
|
||||
]
|
||||
},
|
||||
"editable": true,
|
||||
"fiscalYearStartMonth": 0,
|
||||
"graphTooltip": 0,
|
||||
"links": [],
|
||||
"liveNow": false,
|
||||
"panels": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "palette-classic"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "line",
|
||||
"fillOpacity": 25,
|
||||
"gradientMode": "none",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "smooth",
|
||||
"lineStyle": {
|
||||
"fill": "solid"
|
||||
},
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"__systemRef": "hideSeriesFrom",
|
||||
"matcher": {
|
||||
"id": "byNames",
|
||||
"options": {
|
||||
"mode": "exclude",
|
||||
"names": [
|
||||
"User 1"
|
||||
],
|
||||
"prefix": "All except:",
|
||||
"readOnly": true
|
||||
}
|
||||
},
|
||||
"properties": [
|
||||
{
|
||||
"id": "custom.hideFrom",
|
||||
"value": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 9,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 0
|
||||
},
|
||||
"id": 6,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"pluginVersion": "9.3.6",
|
||||
"targets": [
|
||||
{
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"groupBy": [
|
||||
"user_id"
|
||||
],
|
||||
"metrics": [
|
||||
{
|
||||
"aggregation": "count",
|
||||
"alias": "value",
|
||||
"field": "*"
|
||||
}
|
||||
],
|
||||
"mode": "trend",
|
||||
"orderBy": [],
|
||||
"table": "events",
|
||||
"timeField": "timestamp",
|
||||
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"format": 0,
|
||||
"hide": false,
|
||||
"meta": {
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"groupBy": [
|
||||
"user_id"
|
||||
],
|
||||
"metrics": [
|
||||
{
|
||||
"aggregation": "count",
|
||||
"alias": "value",
|
||||
"field": "*"
|
||||
}
|
||||
],
|
||||
"mode": "trend",
|
||||
"orderBy": [],
|
||||
"table": "events",
|
||||
"timeField": "timestamp",
|
||||
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
|
||||
}
|
||||
},
|
||||
"queryType": "sql",
|
||||
"rawSql": "SELECT $__timeInterval(timestamp) as time, toString(user_id), count() as User\r\nFROM events\r\nWHERE $__timeFilter(timestamp)\r\nGROUP BY time, user_id\r\nORDER BY time ASC, user_id ASC",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "User Activity",
|
||||
"transformations": [],
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "continuous-GrYlRd"
|
||||
},
|
||||
"custom": {
|
||||
"axisCenteredZero": false,
|
||||
"axisColorMode": "text",
|
||||
"axisLabel": "",
|
||||
"axisPlacement": "auto",
|
||||
"barAlignment": 0,
|
||||
"drawStyle": "bars",
|
||||
"fillOpacity": 90,
|
||||
"gradientMode": "scheme",
|
||||
"hideFrom": {
|
||||
"legend": false,
|
||||
"tooltip": false,
|
||||
"viz": false
|
||||
},
|
||||
"lineInterpolation": "linear",
|
||||
"lineWidth": 1,
|
||||
"pointSize": 5,
|
||||
"scaleDistribution": {
|
||||
"type": "linear"
|
||||
},
|
||||
"showPoints": "auto",
|
||||
"spanNulls": false,
|
||||
"stacking": {
|
||||
"group": "A",
|
||||
"mode": "none"
|
||||
},
|
||||
"thresholdsStyle": {
|
||||
"mode": "off"
|
||||
}
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 7,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 9
|
||||
},
|
||||
"id": 4,
|
||||
"options": {
|
||||
"legend": {
|
||||
"calcs": [],
|
||||
"displayMode": "list",
|
||||
"placement": "bottom",
|
||||
"showLegend": true
|
||||
},
|
||||
"tooltip": {
|
||||
"mode": "single",
|
||||
"sort": "none"
|
||||
}
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"filters": [],
|
||||
"metrics": [
|
||||
{
|
||||
"aggregation": "count",
|
||||
"alias": "Count",
|
||||
"field": "*"
|
||||
}
|
||||
],
|
||||
"mode": "trend",
|
||||
"orderBy": [],
|
||||
"table": "events",
|
||||
"timeField": "timestamp",
|
||||
"timeFieldType": "DateTime64(3, 'Etc/UTC')"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"format": 0,
|
||||
"queryType": "builder",
|
||||
"rawSql": "SELECT $__timeInterval(timestamp) as time, count(*) Count FROM events WHERE $__timeFilter(timestamp) GROUP BY time ORDER BY time ASC",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Overall Activity",
|
||||
"type": "timeseries"
|
||||
},
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"fieldConfig": {
|
||||
"defaults": {
|
||||
"color": {
|
||||
"mode": "thresholds"
|
||||
},
|
||||
"custom": {
|
||||
"align": "auto",
|
||||
"displayMode": "auto",
|
||||
"filterable": false,
|
||||
"inspect": true
|
||||
},
|
||||
"mappings": [],
|
||||
"thresholds": {
|
||||
"mode": "absolute",
|
||||
"steps": [
|
||||
{
|
||||
"color": "green",
|
||||
"value": null
|
||||
},
|
||||
{
|
||||
"color": "red",
|
||||
"value": 80
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"overrides": []
|
||||
},
|
||||
"gridPos": {
|
||||
"h": 13,
|
||||
"w": 24,
|
||||
"x": 0,
|
||||
"y": 16
|
||||
},
|
||||
"id": 2,
|
||||
"options": {
|
||||
"footer": {
|
||||
"enablePagination": true,
|
||||
"fields": [
|
||||
"Working time(h)",
|
||||
"Activity"
|
||||
],
|
||||
"reducer": [
|
||||
"sum"
|
||||
],
|
||||
"show": true
|
||||
},
|
||||
"showHeader": true,
|
||||
"sortBy": []
|
||||
},
|
||||
"pluginVersion": "9.3.6",
|
||||
"targets": [
|
||||
{
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"format": 1,
|
||||
"meta": {
|
||||
"builderOptions": {
|
||||
"fields": [],
|
||||
"limit": 100,
|
||||
"mode": "list"
|
||||
}
|
||||
},
|
||||
"queryType": "sql",
|
||||
"rawSql": "SELECT\r\n user_id as User,\r\n user_name as Username,\r\n project_id as Project,\r\n task_id as Task,\r\n job_id as Job, sum(JSONExtractUInt(payload, 'working_time')) / 1000 / 3600 as \"Working time(h)\",\r\n count() as Activity\r\nFROM events\r\nWHERE JSONHas(payload, 'working_time')\r\n AND $__timeFilter(timestamp)\r\n AND(-1 IN (${users}) OR user_id IN (${users}))\r\n AND (' ' IN (${usernames}) OR user_name IN (${usernames}))\r\n AND (-1 IN (${projects}) OR project_id IN (${projects}))\r\n AND task_id IN (${tasks})\r\n AND job_id IN (${jobs})\r\nGROUP BY user_id, user_name, project_id, task_id, job_id",
|
||||
"refId": "A"
|
||||
}
|
||||
],
|
||||
"title": "Working time",
|
||||
"type": "table"
|
||||
}
|
||||
],
|
||||
"refresh": false,
|
||||
"schemaVersion": 38,
|
||||
"style": "dark",
|
||||
"tags": [],
|
||||
"templating": {
|
||||
"list": [
|
||||
{
|
||||
"allValue": "-1",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT user_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND source = 'client'",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "User",
|
||||
"multi": true,
|
||||
"name": "users",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT user_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND source = 'client'",
|
||||
"refresh": 2,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "' '",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"definition": "SELECT DISTINCT user_name\nFROM events\nWHERE $__timeFilter(timestamp)",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Username",
|
||||
"multi": true,
|
||||
"name": "usernames",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT user_name\nFROM events\nWHERE $__timeFilter(timestamp)",
|
||||
"refresh": 2,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"allValue": "-1",
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT project_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND project_id IS NOT NULL\n AND source = 'client'",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Project",
|
||||
"multi": true,
|
||||
"name": "projects",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT project_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND project_id IS NOT NULL\n AND source = 'client'",
|
||||
"refresh": 2,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": false,
|
||||
"text": "All",
|
||||
"value": "$__all"
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT task_id\nFROM events\nWHERE $__timeFilter(timestamp) \n AND task_id IS NOT NULL\n AND source = 'client'\n AND (-1 IN (${projects}) OR project_id IN (${projects}))",
|
||||
"description": "",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Task",
|
||||
"multi": true,
|
||||
"name": "tasks",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT task_id\nFROM events\nWHERE $__timeFilter(timestamp) \n AND task_id IS NOT NULL\n AND source = 'client'\n AND (-1 IN (${projects}) OR project_id IN (${projects}))",
|
||||
"refresh": 2,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 1,
|
||||
"type": "query"
|
||||
},
|
||||
{
|
||||
"current": {
|
||||
"selected": true,
|
||||
"text": [
|
||||
"All"
|
||||
],
|
||||
"value": [
|
||||
"$__all"
|
||||
]
|
||||
},
|
||||
"datasource": {
|
||||
"type": "grafana-clickhouse-datasource",
|
||||
"uid": "PDEE91DDB90597936"
|
||||
},
|
||||
"definition": "SELECT DISTINCT job_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND job_id IS NOT NULL\n AND source = 'client'\n AND task_id in (${tasks})",
|
||||
"hide": 0,
|
||||
"includeAll": true,
|
||||
"label": "Job",
|
||||
"multi": true,
|
||||
"name": "jobs",
|
||||
"options": [],
|
||||
"query": "SELECT DISTINCT job_id\nFROM events\nWHERE $__timeFilter(timestamp)\n AND job_id IS NOT NULL\n AND source = 'client'\n AND task_id in (${tasks})",
|
||||
"refresh": 2,
|
||||
"regex": "",
|
||||
"skipUrlSync": false,
|
||||
"sort": 0,
|
||||
"type": "query"
|
||||
}
|
||||
]
|
||||
},
|
||||
"time": {
|
||||
"from": "now-7d",
|
||||
"to": "now"
|
||||
},
|
||||
"timepicker": {},
|
||||
"timezone": "",
|
||||
"title": "Management",
|
||||
"uid": "w0if6WAVz",
|
||||
"version": 2,
|
||||
"weekStart": ""
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,39 @@
|
||||
http:
|
||||
routers:
|
||||
grafana:
|
||||
entryPoints:
|
||||
- web
|
||||
middlewares:
|
||||
- analytics-auth
|
||||
- strip-prefix
|
||||
service: grafana
|
||||
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)
|
||||
grafana_https:
|
||||
entryPoints:
|
||||
- websecure
|
||||
middlewares:
|
||||
- analytics-auth
|
||||
- strip-prefix
|
||||
service: grafana
|
||||
tls: {}
|
||||
rule: Host(`{{ env "CVAT_HOST" }}`) && PathPrefix(`/analytics`)
|
||||
|
||||
middlewares:
|
||||
analytics-auth:
|
||||
forwardauth:
|
||||
address: http://cvat_server:8080/analytics
|
||||
authRequestHeaders:
|
||||
- "Cookie"
|
||||
- "Authorization"
|
||||
|
||||
strip-prefix:
|
||||
stripprefix:
|
||||
prefixes:
|
||||
- /analytics
|
||||
|
||||
services:
|
||||
grafana:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: http://{{ env "DJANGO_LOG_VIEWER_HOST" }}:{{ env "DJANGO_LOG_VIEWER_PORT" }}
|
||||
passHostHeader: false
|
||||
@@ -0,0 +1,47 @@
|
||||
data_dir = "/vector-data-dir"
|
||||
|
||||
[sources.http-events]
|
||||
type = "http_server"
|
||||
address = "0.0.0.0:8282"
|
||||
encoding = "json"
|
||||
|
||||
# Uncomment for debug
|
||||
# [sinks.console]
|
||||
# type = "console"
|
||||
# inputs = [ "http-events" ]
|
||||
# target = "stdout"
|
||||
|
||||
# [sinks.console.encoding]
|
||||
# codec = "json"
|
||||
|
||||
[sinks.clickhouse]
|
||||
inputs = [ "http-events" ]
|
||||
type = "clickhouse"
|
||||
database = "${CLICKHOUSE_DB}"
|
||||
table = "events"
|
||||
auth.strategy = "basic"
|
||||
auth.user = "${CLICKHOUSE_USER}"
|
||||
auth.password = "${CLICKHOUSE_PASSWORD}"
|
||||
endpoint = "http://${CLICKHOUSE_HOST}:${CLICKHOUSE_PORT}"
|
||||
request.concurrency = "adaptive"
|
||||
encoding.only_fields = [
|
||||
"scope",
|
||||
"obj_name",
|
||||
"obj_id",
|
||||
"obj_val",
|
||||
"source",
|
||||
"timestamp",
|
||||
"count",
|
||||
"duration",
|
||||
"project_id",
|
||||
"task_id",
|
||||
"job_id",
|
||||
"user_id",
|
||||
"user_name",
|
||||
"user_email",
|
||||
"org_id",
|
||||
"org_slug",
|
||||
"payload",
|
||||
"access_token_id",
|
||||
"remote_addr",
|
||||
]
|
||||
@@ -0,0 +1,8 @@
|
||||
## Serverless for Computer Vision Annotation Tool (CVAT)
|
||||
|
||||
### Run docker container
|
||||
|
||||
```bash
|
||||
# From project root directory
|
||||
docker compose -f docker-compose.yml -f components/serverless/docker-compose.serverless.yml up -d
|
||||
```
|
||||
@@ -0,0 +1,30 @@
|
||||
services:
|
||||
nuclio:
|
||||
container_name: nuclio
|
||||
image: quay.io/nuclio/dashboard:1.16.3-amd64
|
||||
restart: always
|
||||
networks:
|
||||
- cvat
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
http_proxy:
|
||||
https_proxy:
|
||||
no_proxy: ${no_proxy:-}
|
||||
NUCLIO_CHECK_FUNCTION_CONTAINERS_HEALTHINESS: 'true'
|
||||
NUCLIO_DASHBOARD_DEFAULT_FUNCTION_MOUNT_MODE: 'volume'
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: 100m
|
||||
max-file: "3"
|
||||
|
||||
cvat_server:
|
||||
environment:
|
||||
CVAT_SERVERLESS: 1
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
|
||||
cvat_worker_annotation:
|
||||
extra_hosts:
|
||||
- "host.docker.internal:host-gateway"
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2019-2022 Intel Corporation
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const { join } = require('path');
|
||||
|
||||
module.exports = {
|
||||
ignorePatterns: [
|
||||
'.eslintrc.cjs',
|
||||
'webpack.config.js',
|
||||
'node_modules/**',
|
||||
'dist/**',
|
||||
],
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
{
|
||||
packageDir: [__dirname, join(__dirname, '../')]
|
||||
},
|
||||
],
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
# Module CVAT-CANVAS
|
||||
|
||||
## Description
|
||||
|
||||
The CVAT module written in TypeScript language.
|
||||
It presents a canvas to viewing, drawing and editing of annotations.
|
||||
|
||||
## Commands
|
||||
|
||||
- Building of the module from sources in the `dist` directory:
|
||||
|
||||
```bash
|
||||
yarn run build
|
||||
yarn run build --mode=development # without a minification
|
||||
```
|
||||
|
||||
### API Methods
|
||||
|
||||
For API methods, their arguments and return types, please look at ``canvas.ts``.
|
||||
|
||||
### API CSS
|
||||
|
||||
- All drawn objects (shapes, tracks) have an id `cvat_canvas_shape_{objectState.clientID}`
|
||||
- Drawn shapes and tracks have classes `cvat_canvas_shape`,
|
||||
`cvat_canvas_shape_activated`,
|
||||
`cvat_canvas_shape_selection`,
|
||||
`cvat_canvas_shape_merging`,
|
||||
`cvat_canvas_shape_drawing`,
|
||||
`cvat_canvas_shape_occluded`
|
||||
- Drawn review ROIs have an id `cvat_canvas_issue_region_{issue.id}`
|
||||
- Drawn review roi has the class `cvat_canvas_issue_region`
|
||||
- Drawn texts have the class `cvat_canvas_text`
|
||||
- Tags have the class `cvat_canvas_tag`
|
||||
- Canvas image has ID `cvat_canvas_image`
|
||||
- Grid on the canvas has ID `cvat_canvas_grid` and `cvat_canvas_grid_pattern`
|
||||
- Crosshair during a draw has class `cvat_canvas_crosshair`
|
||||
- To stick something to a specific position you can use an element with id `cvat_canvas_attachment_board`
|
||||
|
||||
### Events
|
||||
|
||||
Standard JS events are used.
|
||||
|
||||
```js
|
||||
- canvas.setup
|
||||
- canvas.activated => {state: ObjectState}
|
||||
- canvas.clicked => {state: ObjectState}
|
||||
- canvas.moved => {states: ObjectState[], x: number, y: number}
|
||||
- canvas.find => {states: ObjectState[], x: number, y: number}
|
||||
- canvas.drawn => {state: DrawnData}
|
||||
- canvas.interacted => {shapes: InteractionResult[]}
|
||||
- canvas.editstart
|
||||
- canvas.edited => {state: ObjectState, points: number[], rotation?: number}
|
||||
- canvas.splitted => {state: ObjectState, frame: number, duration: number}
|
||||
- canvas.grouped => {states: ObjectState[], duration: number}
|
||||
- canvas.joined => {states: ObjectState[], points: number[], duration: number}
|
||||
- canvas.sliced => {state: ObjectState, results: number[][], duration: number}
|
||||
- canvas.merged => {states: ObjectState[], duration: number}
|
||||
- canvas.canceled
|
||||
- canvas.dragstart
|
||||
- canvas.dragstop
|
||||
- canvas.zoomstart
|
||||
- canvas.zoomstop
|
||||
- canvas.zoom
|
||||
- canvas.reshape
|
||||
- canvas.fit
|
||||
- canvas.regionselected => {points: number[]}
|
||||
- canvas.dragshape => {duration: number, state: ObjectState}
|
||||
- canvas.roiselected => {points: number[]}
|
||||
- canvas.resizeshape => {duration: number, state: ObjectState}
|
||||
- canvas.contextmenu => { mouseEvent: MouseEvent, objectState: ObjectState, pointID: number }
|
||||
- canvas.message => { messages: { type: 'text' | 'list'; content: string | string[]; className?: string; icon?: 'info' | 'loading' }[] | null, topic: string }
|
||||
- canvas.error => { exception: Error, domain?: string }
|
||||
- canvas.destroy
|
||||
```
|
||||
|
||||
### WEB
|
||||
|
||||
```js
|
||||
// Create an instance of a canvas
|
||||
const canvas = new window.canvas.Canvas();
|
||||
|
||||
console.log('Version ', window.canvas.CanvasVersion);
|
||||
console.log('Current mode is ', window.canvas.mode());
|
||||
|
||||
// Put canvas to a html container
|
||||
htmlContainer.appendChild(canvas.html());
|
||||
canvas.fitCanvas();
|
||||
|
||||
// Next you can use its API methods. For example:
|
||||
canvas.rotate(270);
|
||||
canvas.draw({
|
||||
enabled: true,
|
||||
shapeType: 'rectangle',
|
||||
crosshair: true,
|
||||
rectDrawingMethod: window.Canvas.RectDrawingMethod.CLASSIC,
|
||||
});
|
||||
```
|
||||
|
||||
<!--lint disable maximum-line-length-->
|
||||
|
||||
## API Reaction
|
||||
|
||||
| | IDLE | GROUP | SPLIT | DRAW | MERGE | EDIT | DRAG | RESIZE | ZOOM_CANVAS | DRAG_CANVAS | INTERACT | JOIN | SLICE | SELECT_REGION |
|
||||
| -------------- | ---- | ----- | ----- | ---- | ----- | ---- | ---- | ------ | ----------- | ----------- | -------- | ---- | ----- | ------------- |
|
||||
| setup() | + | + | + | +/- | + | +/- | +/- | +/- | + | + | + | + | + | + |
|
||||
| activate() | + | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| rotate() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| focus() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| fit() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| grid() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| draw() | + | - | - | + | - | - | - | - | - | - | - | - | - | - |
|
||||
| interact() | + | - | - | - | - | - | - | - | - | - | + | - | - | - |
|
||||
| split() | + | - | + | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| group() | + | + | - | - | - | - | - | - | - | - | - | - | - | - |
|
||||
| merge() | + | - | - | - | + | - | - | - | - | - | - | - | - | - |
|
||||
| edit() | + | - | - | - | - | + | - | - | - | - | - | - | - | - |
|
||||
| join() | + | - | - | - | - | - | - | - | - | - | - | + | - | - |
|
||||
| slice() | + | - | - | - | - | - | - | - | - | - | - | - | + | - |
|
||||
| selectRegion() | + | - | - | - | - | - | - | - | - | - | - | - | - | + |
|
||||
| fitCanvas() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| dragCanvas() | + | - | - | - | - | - | + | - | - | + | - | - | - | - |
|
||||
| zoomCanvas() | + | - | - | - | - | - | - | + | + | - | - | - | - | - |
|
||||
| cancel() | - | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| configure() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| bitmap() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| setZLayer() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
| destroy() | + | + | + | + | + | + | + | + | + | + | + | + | + | + |
|
||||
|
||||
<!--lint enable maximum-line-length-->
|
||||
|
||||
You can call setup() during editing, dragging, and resizing only to update objects, not to change a frame.
|
||||
You can change frame during draw only when you do not redraw an existing object
|
||||
|
||||
Other methods do not change state and can be used at any time.
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "cvat-canvas",
|
||||
"version": "2.20.10",
|
||||
"type": "module",
|
||||
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
|
||||
"main": "src/canvas.ts",
|
||||
"scripts": {
|
||||
"build": "tsc && webpack --config ./webpack.config.cjs"
|
||||
},
|
||||
"author": "CVAT.ai",
|
||||
"license": "MIT",
|
||||
"browserslist": [
|
||||
"Chrome >= 99",
|
||||
"Firefox >= 110",
|
||||
"not IE 11",
|
||||
"> 2%"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/polylabel": "^1.0.5",
|
||||
"martinez-polygon-clipping": "^0.8.1",
|
||||
"polylabel": "^1.1.0",
|
||||
"svg.draggable.js": "2.2.2",
|
||||
"svg.draw.js": "^2.0.4",
|
||||
"svg.js": "2.7.1",
|
||||
"svg.resize.js": "1.4.3",
|
||||
"svg.select.js": "3.0.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
// Copyright (C) 2020-2022 Intel Corporation
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/* stylelint-disable selector-class-pattern, selector-id-pattern */
|
||||
|
||||
.cvat_canvas_hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape {
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
g.cvat_canvas_shape {
|
||||
> circle {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
polyline.cvat_canvas_shape {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.cvat_shape_action_opacity {
|
||||
fill-opacity: 0.5;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
polyline.cvat_shape_action_opacity {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.cvat_shape_drawing_opacity {
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
|
||||
polyline.cvat_shape_drawing_opacity {
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.cvat_shape_action_dasharray {
|
||||
stroke-dasharray: 4 1 2 3;
|
||||
}
|
||||
|
||||
.cvat_canvas_text {
|
||||
font-weight: bold;
|
||||
fill: white;
|
||||
cursor: default;
|
||||
filter: drop-shadow(1px 1px 1px black) drop-shadow(-1px -1px 1px black);
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.cvat_canvas_text_dimensions {
|
||||
fill: lightskyblue;
|
||||
}
|
||||
|
||||
.cvat_canvas_text_layer {
|
||||
fill: #73d13d;
|
||||
}
|
||||
|
||||
.cvat_canvas_text_description {
|
||||
fill: yellow;
|
||||
font-style: oblique 40deg;
|
||||
}
|
||||
|
||||
.cvat_canvas_text_score {
|
||||
fill: #FFB347;
|
||||
font-style: oblique 10deg;
|
||||
}
|
||||
|
||||
.cvat_canvas_crosshair {
|
||||
stroke: red;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_selection {
|
||||
@extend .cvat_shape_action_dasharray;
|
||||
@extend .cvat_shape_action_opacity;
|
||||
|
||||
fill: #fcfbfc;
|
||||
}
|
||||
|
||||
image.cvat_canvas_shape_selection {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.cvat_canvas_selection_box {
|
||||
fill: white;
|
||||
fill-opacity: 0.1;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_region_selection {
|
||||
@extend .cvat_shape_action_dasharray;
|
||||
@extend .cvat_shape_action_opacity;
|
||||
|
||||
fill: white;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
.cvat_canvas_issue_region {
|
||||
pointer-events: none;
|
||||
stroke-width: 0;
|
||||
}
|
||||
|
||||
circle.cvat_canvas_issue_region {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
polyline.cvat_canvas_shape_selection {
|
||||
@extend .cvat_shape_action_dasharray;
|
||||
@extend .cvat_shape_action_opacity;
|
||||
|
||||
stroke: darkmagenta;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_merging {
|
||||
@extend .cvat_shape_action_dasharray;
|
||||
@extend .cvat_shape_action_opacity;
|
||||
|
||||
fill: blue;
|
||||
|
||||
> circle[data-node-id] {
|
||||
fill: blue;
|
||||
}
|
||||
}
|
||||
|
||||
polyline.cvat_canvas_shape_merging {
|
||||
@extend .cvat_shape_action_dasharray;
|
||||
@extend .cvat_shape_action_opacity;
|
||||
|
||||
stroke: blue;
|
||||
}
|
||||
|
||||
polyline.cvat_canvas_shape_splitting {
|
||||
@extend .cvat_shape_action_dasharray;
|
||||
@extend .cvat_shape_action_opacity;
|
||||
|
||||
stroke: dodgerblue;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_splitting {
|
||||
@extend .cvat_shape_action_dasharray;
|
||||
@extend .cvat_shape_action_opacity;
|
||||
|
||||
fill: dodgerblue;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_drawing {
|
||||
@extend .cvat_shape_drawing_opacity;
|
||||
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.cvat_canvas_zoom_selection {
|
||||
@extend .cvat_shape_action_dasharray;
|
||||
|
||||
stroke: #096dd9;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_occluded {
|
||||
stroke-dasharray: 5;
|
||||
}
|
||||
|
||||
.cvat_canvas_ground_truth {
|
||||
stroke-dasharray: 1;
|
||||
}
|
||||
|
||||
.cvat_canvas_conflicted {
|
||||
stroke: #ff4800;
|
||||
fill: #ff4800;
|
||||
|
||||
rect,
|
||||
ellipse,
|
||||
polygon,
|
||||
polyline,
|
||||
line {
|
||||
fill: #ff4800;
|
||||
stroke: #ff4800;
|
||||
}
|
||||
|
||||
circle {
|
||||
fill: #ff4800;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat_canvas_warned {
|
||||
stroke: #ff7301;
|
||||
fill: #ff7301;
|
||||
|
||||
rect,
|
||||
ellipse,
|
||||
polygon,
|
||||
polyline,
|
||||
line {
|
||||
fill: #ff7301;
|
||||
stroke: #ff7301;
|
||||
}
|
||||
|
||||
circle {
|
||||
fill: #ff7301;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_occluded_point {
|
||||
stroke-dasharray: 1 !important;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
circle.cvat_canvas_shape_occluded {
|
||||
@extend .cvat_canvas_shape_occluded_point;
|
||||
}
|
||||
|
||||
g.cvat_canvas_shape_occluded {
|
||||
> rect {
|
||||
stroke-dasharray: 5;
|
||||
}
|
||||
|
||||
> circle {
|
||||
@extend .cvat_canvas_shape_occluded_point;
|
||||
}
|
||||
}
|
||||
|
||||
.svg_select_points_rot {
|
||||
fill: white;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape .svg_select_points,
|
||||
.cvat_canvas_shape .cvat_canvas_cuboid_projections {
|
||||
stroke-dasharray: none;
|
||||
}
|
||||
|
||||
.cvat_canvas_autoborder_point {
|
||||
transition: opacity 0.05s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cvat_canvas_autoborder_point:hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.cvat_canvas_autoborder_point:active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.cvat_canvas_interact_intermediate_shape {
|
||||
@extend .cvat_canvas_shape;
|
||||
}
|
||||
|
||||
.cvat_interaction_delete_button {
|
||||
opacity: 0.25;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat_interaction_rectangle {
|
||||
@extend .cvat_canvas_shape_drawing;
|
||||
}
|
||||
|
||||
.cvat_interaction_delete_button, .cvat_interaction_point {
|
||||
transition: stroke-width 0.2s ease-in-out, opacity 0.2s ease-in-out;
|
||||
filter: drop-shadow(0 1px 3px rgba(0, 0, 0, 30%));
|
||||
}
|
||||
|
||||
.cvat_canvas_removable_interaction_point {
|
||||
cursor:
|
||||
url(
|
||||
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K'
|
||||
) 10 10,
|
||||
auto;
|
||||
}
|
||||
|
||||
.svg_select_boundingRect {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.svg_select_points_lb:hover,
|
||||
.svg_select_points_rt:hover {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.svg_select_points_lt:hover,
|
||||
.svg_select_points_rb:hover {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.svg_select_points_l:hover,
|
||||
.svg_select_points_r:hover,
|
||||
.svg_select_points_ew:hover {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.svg_select_points_t:hover,
|
||||
.svg_select_points_b:hover {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_draggable:hover {
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.cvat_canvas_first_poly_point {
|
||||
fill: lightgray;
|
||||
}
|
||||
|
||||
.cvat_canvas_poly_direction {
|
||||
fill: lightgray;
|
||||
stroke: black;
|
||||
|
||||
&:hover {
|
||||
fill: black;
|
||||
stroke: lightgray;
|
||||
}
|
||||
|
||||
&:active {
|
||||
fill: lightgray;
|
||||
stroke: black;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat_canvas_skeleton_wrapping_rect {
|
||||
// wrapping rect must not apply transform attribute from selectize.js
|
||||
// otherwise it rotated twice, because we apply the same rotation value to parent element (skeleton itself)
|
||||
transform: none !important;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape > .cvat_canvas_skeleton_wrapping_rect {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape.cvat_canvas_shape_activated > .cvat_canvas_skeleton_wrapping_rect {
|
||||
visibility: initial;
|
||||
}
|
||||
|
||||
.cvat_canvas_pixelized {
|
||||
image-rendering: optimizeSpeed;
|
||||
image-rendering: optimize-contrast;
|
||||
image-rendering: crisp-edges;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.cvat_canvas_removed_image {
|
||||
filter: saturate(0) brightness(1.2) contrast(0.75) !important;
|
||||
}
|
||||
|
||||
#cvat_canvas_wrapper {
|
||||
width: calc(100% - 10px);
|
||||
height: calc(100% - 10px);
|
||||
margin: 5px;
|
||||
border-radius: 5px;
|
||||
background-color: inherit;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cvat-canvas-highlight-enabled {
|
||||
svg {
|
||||
>rect:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned),
|
||||
>ellipse:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned),
|
||||
>polygon:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned),
|
||||
>polyline:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned),
|
||||
>line:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned) {
|
||||
fill: gray;
|
||||
stroke: gray;
|
||||
}
|
||||
|
||||
>circle:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned) {
|
||||
fill: gray;
|
||||
}
|
||||
|
||||
>g:not(.cvat_canvas_issue_region,.cvat_canvas_conflicted,.cvat_canvas_warned) {
|
||||
rect,
|
||||
ellipse,
|
||||
polygon,
|
||||
polyline,
|
||||
line {
|
||||
fill: gray;
|
||||
stroke: gray;
|
||||
}
|
||||
|
||||
circle {
|
||||
fill: gray;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#cvat_canvas_text_content {
|
||||
text-rendering: optimizeSpeed;
|
||||
position: absolute;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#cvat_canvas_background {
|
||||
position: absolute;
|
||||
z-index: 0;
|
||||
background-repeat: no-repeat;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 75%);
|
||||
}
|
||||
|
||||
#cvat_canvas_bitmap {
|
||||
@extend .cvat_canvas_pixelized;
|
||||
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
background: black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-shadow: 2px 2px 5px 0 rgba(0, 0, 0, 75%);
|
||||
}
|
||||
|
||||
#cvat_canvas_grid {
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#cvat_canvas_grid_pattern {
|
||||
opacity: 1;
|
||||
stroke: white;
|
||||
}
|
||||
|
||||
#cvat_canvas_content {
|
||||
@extend .cvat_canvas_pixelized;
|
||||
|
||||
filter: contrast(120%) saturate(150%);
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
outline: 10px solid black;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.cvat_masks_canvas_wrapper {
|
||||
@extend .cvat_canvas_pixelized;
|
||||
|
||||
z-index: 3;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#cvat_canvas_attachment_board {
|
||||
position: absolute;
|
||||
z-index: 4;
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.cvat_canvas_shape_darken {
|
||||
fill: #838383;
|
||||
stroke: #838383;
|
||||
}
|
||||
|
||||
.cvat_canvas_sliced_contour {
|
||||
fill-opacity: 0.01;
|
||||
}
|
||||
|
||||
.cvat_canvas_slicing_line {
|
||||
pointer-events: none;
|
||||
fill-opacity: 0;
|
||||
}
|
||||
|
||||
.cvat-canvas-notification-list-warning {
|
||||
color: orange;
|
||||
}
|
||||
|
||||
.cvat-canvas-notification-list-shortcuts {
|
||||
color: yellow;
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
// Copyright (C) 2020-2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
|
||||
import consts from './consts';
|
||||
import { Configuration, Geometry } from './canvasModel';
|
||||
import { translateToSVG } from './shared';
|
||||
|
||||
interface TransformedShape {
|
||||
points: number[][];
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface AutoborderHandler {
|
||||
autoborder(enabled: boolean, currentShape?: SVG.Shape, excludedClientId?: number): void;
|
||||
configure(configuration: Configuration): void;
|
||||
transform(geometry: Geometry): void;
|
||||
updateObjects(): void;
|
||||
}
|
||||
|
||||
function collectSegmentPoints(
|
||||
points: number[][],
|
||||
startPointID: number,
|
||||
endPointID: number,
|
||||
direction: -1 | 1 | null,
|
||||
): number[][] {
|
||||
if (startPointID === endPointID) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (
|
||||
Math.abs(startPointID - endPointID) === 1 ||
|
||||
Math.abs(startPointID - endPointID) === points.length - 1
|
||||
) {
|
||||
// adjacent points, no need to calculate anything
|
||||
return [points[endPointID]];
|
||||
}
|
||||
|
||||
const walk = (step: number): {
|
||||
length: number;
|
||||
points: number[][];
|
||||
} => {
|
||||
let length = 0;
|
||||
let previousPoint = points[startPointID];
|
||||
const curvePoints: number[][] = [];
|
||||
|
||||
for (let i = startPointID + step; ; i += step) {
|
||||
if (i < 0) {
|
||||
i = points.length - 1;
|
||||
} else if (i === points.length) {
|
||||
i = 0;
|
||||
}
|
||||
|
||||
const currentPoint = points[i];
|
||||
const dx = currentPoint[0] - previousPoint[0];
|
||||
const dy = currentPoint[1] - previousPoint[1];
|
||||
|
||||
length += Math.hypot(dx, dy);
|
||||
curvePoints.push([...currentPoint]);
|
||||
|
||||
if (i === endPointID) {
|
||||
break;
|
||||
}
|
||||
|
||||
previousPoint = currentPoint;
|
||||
}
|
||||
|
||||
return {
|
||||
length,
|
||||
points: curvePoints,
|
||||
};
|
||||
};
|
||||
|
||||
if (direction === 1) {
|
||||
return walk(1).points;
|
||||
}
|
||||
|
||||
if (direction === -1) {
|
||||
return walk(-1).points;
|
||||
}
|
||||
|
||||
const forward = walk(1);
|
||||
const backward = walk(-1);
|
||||
return forward.length <= backward.length ? forward.points : backward.points;
|
||||
}
|
||||
|
||||
type PointIndexesAndDirection = {
|
||||
addedPointIndexes: Set<number>;
|
||||
direction: -1 | 1 | null;
|
||||
};
|
||||
|
||||
function collectAddedPointIndexesAndDirection(
|
||||
points: number[][],
|
||||
pointsToRevertPreview: number[][],
|
||||
): PointIndexesAndDirection {
|
||||
const pointKey = (point: number[]): string => `${point[0]},${point[1]}`;
|
||||
const addedPointIndexes = new Set<number>();
|
||||
const indexesByPointKey = new Map<string, number[]>();
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const key = pointKey(points[i]);
|
||||
const indexes = indexesByPointKey.get(key);
|
||||
|
||||
if (indexes) {
|
||||
indexes.push(i);
|
||||
} else {
|
||||
indexesByPointKey.set(key, [i]);
|
||||
}
|
||||
}
|
||||
|
||||
const originalPointKeys = new Set(pointsToRevertPreview.map(pointKey));
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
if (originalPointKeys.has(pointKey(points[i]))) {
|
||||
addedPointIndexes.add(i);
|
||||
}
|
||||
}
|
||||
|
||||
if (points.length < 2 || pointsToRevertPreview.length < 2) {
|
||||
return {
|
||||
addedPointIndexes,
|
||||
direction: null,
|
||||
};
|
||||
}
|
||||
|
||||
const previousKey = pointKey(pointsToRevertPreview[pointsToRevertPreview.length - 2]);
|
||||
const lastKey = pointKey(pointsToRevertPreview[pointsToRevertPreview.length - 1]);
|
||||
const previousIndexes = indexesByPointKey.get(previousKey) || [];
|
||||
const lastIndexes = new Set(indexesByPointKey.get(lastKey) || []);
|
||||
|
||||
for (const previousIndex of previousIndexes) {
|
||||
const nextIndex = (previousIndex + 1) % points.length;
|
||||
const prevIndex = (previousIndex - 1 + points.length) % points.length;
|
||||
|
||||
if (lastIndexes.has(nextIndex)) {
|
||||
return {
|
||||
addedPointIndexes,
|
||||
direction: 1,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastIndexes.has(prevIndex)) {
|
||||
return {
|
||||
addedPointIndexes,
|
||||
direction: -1,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
addedPointIndexes,
|
||||
direction: null,
|
||||
};
|
||||
}
|
||||
|
||||
export class AutoborderHandlerImpl implements AutoborderHandler {
|
||||
private currentShape: SVG.Shape | null;
|
||||
private excludedClientId?: number;
|
||||
private container: SVGSVGElement;
|
||||
private pointGroups: SVGGElement[];
|
||||
private enabled: boolean;
|
||||
private scale: number;
|
||||
private controlPointsSize: number;
|
||||
private visibleShapes: TransformedShape[];
|
||||
private currentClick: { groupIdx: number; pointIdx: number; } | null;
|
||||
private currentPreview: { pointIdx: number; shapePoints: number[][]; } | null;
|
||||
private pointsToRevertPreview: number[][] | null;
|
||||
private listeners: Map<SVGCircleElement, { mousedown: (event: MouseEvent) => void; }>;
|
||||
private isCtrlKeyDown: (() => boolean) | null;
|
||||
|
||||
public constructor(container: SVGSVGElement, isCtrlKeyDown: () => boolean) {
|
||||
this.currentShape = null;
|
||||
this.excludedClientId = undefined;
|
||||
this.container = container;
|
||||
this.pointGroups = [];
|
||||
this.enabled = false;
|
||||
this.scale = 1;
|
||||
this.controlPointsSize = consts.BASE_POINT_SIZE;
|
||||
this.visibleShapes = [];
|
||||
this.currentClick = null;
|
||||
this.currentPreview = null;
|
||||
this.pointsToRevertPreview = null;
|
||||
this.listeners = new Map();
|
||||
this.isCtrlKeyDown = isCtrlKeyDown;
|
||||
}
|
||||
|
||||
private removeMarkers(): void {
|
||||
this.pointGroups.forEach((group: SVGGElement): void => {
|
||||
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
|
||||
const listeners = this.listeners.get(circle);
|
||||
if (listeners) {
|
||||
circle.removeEventListener('mousedown', listeners.mousedown);
|
||||
}
|
||||
circle.remove();
|
||||
});
|
||||
|
||||
group.remove();
|
||||
});
|
||||
|
||||
this.pointGroups = [];
|
||||
this.currentClick = null;
|
||||
this.listeners.clear();
|
||||
}
|
||||
|
||||
private readCurrentShapePoints(): number[][] {
|
||||
return (this.currentShape as any).array().valueOf().map(
|
||||
(shapePoint: number[]): number[] => [...shapePoint],
|
||||
);
|
||||
}
|
||||
|
||||
private replaceCurrentShapePoints(points: number[][]): void {
|
||||
const paintHandler = this.currentShape?.remember('_paintHandler');
|
||||
if (!paintHandler) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lastPoint = (this.currentShape as any).array().valueOf().slice(-1)[0];
|
||||
(this.currentShape as any).plot([...points, lastPoint]);
|
||||
|
||||
paintHandler.drawCircles();
|
||||
paintHandler.set.members.forEach((el: SVG.Circle): void => {
|
||||
el.attr('stroke-width', 1 / this.scale).attr('r', 2.5 / this.scale);
|
||||
el.attr('r', `${this.controlPointsSize / this.scale}`);
|
||||
});
|
||||
}
|
||||
|
||||
private raiseMarkers(): void {
|
||||
for (let i = 0; i < this.pointGroups.length; i++) {
|
||||
this.container.appendChild(this.pointGroups[i]);
|
||||
}
|
||||
|
||||
if (this.currentClick !== null) {
|
||||
this.container.appendChild(this.pointGroups[this.currentClick.groupIdx]);
|
||||
}
|
||||
}
|
||||
|
||||
private drawMarkers(): void {
|
||||
this.removeMarkers();
|
||||
|
||||
const ns = 'http://www.w3.org/2000/svg';
|
||||
this.pointGroups = this.visibleShapes.map(
|
||||
(shape: TransformedShape, groupIdx: number): SVGGElement => {
|
||||
const group = document.createElementNS(ns, 'g');
|
||||
|
||||
const circles = shape.points.map(
|
||||
(point: number[], pointIdx: number): SVGCircleElement => {
|
||||
const [x, y] = point;
|
||||
const circle = document.createElementNS(ns, 'circle');
|
||||
circle.classList.add('cvat_canvas_autoborder_point');
|
||||
circle.setAttribute('fill', shape.color);
|
||||
circle.setAttribute('stroke', 'black');
|
||||
circle.setAttribute('stroke-width', `${consts.POINTS_STROKE_WIDTH / this.scale}`);
|
||||
circle.setAttribute('cx', `${x}`);
|
||||
circle.setAttribute('cy', `${y}`);
|
||||
circle.setAttribute('r', `${this.controlPointsSize / this.scale}`);
|
||||
|
||||
const mousedown = (event: MouseEvent): void => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.isCtrlKeyDown && this.isCtrlKeyDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.stopPropagation();
|
||||
if (this.currentClick?.groupIdx !== groupIdx) {
|
||||
// first click on this group of points
|
||||
|
||||
// svg.draw.js initializes the internal paint handler lazily,
|
||||
// so the first autoborder click needs to bootstrap it first.
|
||||
const handler = this.currentShape.remember('_paintHandler');
|
||||
if (!handler || !handler.startPoint) {
|
||||
(this.currentShape as any).draw('point', event);
|
||||
(this.currentShape as any).draw('undo');
|
||||
}
|
||||
|
||||
this.container.appendChild(group); // raise in DOM over other groups
|
||||
const originalPoints = this.readCurrentShapePoints().slice(0, -1);
|
||||
this.replaceCurrentShapePoints([...originalPoints, [x, y]]);
|
||||
this.currentClick = { groupIdx, pointIdx };
|
||||
}
|
||||
};
|
||||
|
||||
circle.addEventListener('mousedown', mousedown);
|
||||
this.listeners.set(circle, { mousedown });
|
||||
return circle;
|
||||
},
|
||||
);
|
||||
|
||||
group.append(...circles);
|
||||
return group;
|
||||
},
|
||||
);
|
||||
|
||||
this.container.append(...this.pointGroups);
|
||||
this.raiseMarkers();
|
||||
}
|
||||
|
||||
private findClosestPointInClickedGroup(e: MouseEvent): number {
|
||||
let closestPointIdx = -1;
|
||||
if (this.currentClick === null) {
|
||||
// group not selected, do nothing
|
||||
return closestPointIdx;
|
||||
}
|
||||
|
||||
const { points } = this.visibleShapes[this.currentClick.groupIdx];
|
||||
const [x, y] = translateToSVG(this.container, [e.clientX, e.clientY]);
|
||||
let distance = (consts.BASE_POINT_SIZE * 2) / this.scale;
|
||||
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
const point = points[i];
|
||||
const dx = point[0] - x;
|
||||
const dy = point[1] - y;
|
||||
const currentDistance = Math.hypot(dx, dy);
|
||||
if (currentDistance <= distance) {
|
||||
distance = currentDistance;
|
||||
closestPointIdx = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestPointIdx;
|
||||
}
|
||||
|
||||
private onContainerMouseMove = (e: MouseEvent): void => {
|
||||
if (this.isCtrlKeyDown && this.isCtrlKeyDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closestPointIdx = this.findClosestPointInClickedGroup(e);
|
||||
|
||||
if (this.pointsToRevertPreview) {
|
||||
this.replaceCurrentShapePoints(this.pointsToRevertPreview);
|
||||
this.pointsToRevertPreview = null;
|
||||
this.currentPreview = null;
|
||||
}
|
||||
|
||||
if (closestPointIdx === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { points } = this.visibleShapes[this.currentClick.groupIdx];
|
||||
const {
|
||||
addedPointIndexes,
|
||||
direction,
|
||||
} = collectAddedPointIndexesAndDirection(points, this.readCurrentShapePoints().slice(0, -1));
|
||||
|
||||
if (addedPointIndexes.has(closestPointIdx)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const curvePoints = collectSegmentPoints(points, this.currentClick.pointIdx, closestPointIdx, direction);
|
||||
this.pointsToRevertPreview = this.readCurrentShapePoints().slice(0, -1);
|
||||
this.currentPreview = {
|
||||
pointIdx: closestPointIdx,
|
||||
shapePoints: [...this.pointsToRevertPreview, ...curvePoints],
|
||||
};
|
||||
this.replaceCurrentShapePoints(this.currentPreview.shapePoints);
|
||||
};
|
||||
|
||||
private onContainerMouseDown = (e: MouseEvent): void => {
|
||||
if (this.isCtrlKeyDown && this.isCtrlKeyDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0 || this.currentPreview === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closestPointIdx = this.findClosestPointInClickedGroup(e);
|
||||
if (closestPointIdx === this.currentPreview.pointIdx) {
|
||||
e.stopPropagation();
|
||||
this.currentPreview = null;
|
||||
this.pointsToRevertPreview = null;
|
||||
this.currentClick.pointIdx = closestPointIdx;
|
||||
}
|
||||
};
|
||||
|
||||
private setObjectsFromContainer(): void {
|
||||
const shapes = Array.from(this.container.getElementsByClassName('cvat_canvas_shape')).filter(
|
||||
(shape: HTMLElement): boolean => !shape.classList.contains('cvat_canvas_hidden'),
|
||||
);
|
||||
|
||||
this.visibleShapes = shapes.map((shape: HTMLElement): TransformedShape | null => {
|
||||
const color = shape.getAttribute('fill');
|
||||
const clientId = shape.getAttribute('clientID');
|
||||
|
||||
const isSupportedType =
|
||||
shape.tagName === 'polyline' ||
|
||||
shape.tagName === 'polygon' ||
|
||||
shape.tagName === 'rect';
|
||||
|
||||
if (
|
||||
color === null || clientId === null || !isSupportedType ||
|
||||
(typeof this.excludedClientId === 'number' && +clientId === this.excludedClientId)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let points = '';
|
||||
if (shape.tagName === 'polyline' || shape.tagName === 'polygon') {
|
||||
points = shape.getAttribute('points');
|
||||
} else if (shape.tagName === 'rect') {
|
||||
const x = +shape.getAttribute('x');
|
||||
const y = +shape.getAttribute('y');
|
||||
const width = +shape.getAttribute('width');
|
||||
const height = +shape.getAttribute('height');
|
||||
|
||||
if (Number.isNaN(x) || Number.isNaN(y) || Number.isNaN(width) || Number.isNaN(height)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const svgElement = shape as unknown as SVGGraphicsElement;
|
||||
const ctm = svgElement.getCTM();
|
||||
|
||||
const localCorners = [
|
||||
{ x, y },
|
||||
{ x: x + width, y },
|
||||
{ x: x + width, y: y + height },
|
||||
{ x, y: y + height },
|
||||
];
|
||||
|
||||
if (ctm && (ctm.a !== 1 || ctm.b !== 0 || ctm.c !== 0 || ctm.d !== 1)) {
|
||||
const transformedCorners = localCorners.map((corner) => {
|
||||
const transformedX = ctm.a * corner.x + ctm.c * corner.y + ctm.e;
|
||||
const transformedY = ctm.b * corner.x + ctm.d * corner.y + ctm.f;
|
||||
return [transformedX, transformedY];
|
||||
});
|
||||
|
||||
return {
|
||||
color,
|
||||
points: transformedCorners,
|
||||
};
|
||||
}
|
||||
|
||||
points = `${x},${y} ${x + width},${y} ${x + width},${y + height} ${x},${y + height}`;
|
||||
}
|
||||
|
||||
return {
|
||||
color,
|
||||
points: points.trim().split(/\s/).map(
|
||||
(shapePoint: string): number[] => shapePoint.split(',')
|
||||
.map((coordinate: string): number => +coordinate),
|
||||
),
|
||||
};
|
||||
}).filter((state: TransformedShape | null): boolean => state !== null);
|
||||
}
|
||||
|
||||
public updateObjects(): void {
|
||||
if (this.enabled) {
|
||||
this.setObjectsFromContainer();
|
||||
this.drawMarkers();
|
||||
}
|
||||
}
|
||||
|
||||
public autoborder(enabled: boolean, currentShape?: SVG.Shape, excludedClientId?: number): void {
|
||||
if (enabled && !this.enabled && currentShape) {
|
||||
this.enabled = true;
|
||||
this.currentShape = currentShape;
|
||||
this.excludedClientId = excludedClientId;
|
||||
this.updateObjects();
|
||||
|
||||
this.container.addEventListener('mousemove', this.onContainerMouseMove);
|
||||
this.container.addEventListener('mousedown', this.onContainerMouseDown, { capture: true });
|
||||
this.currentShape.on('undopoint', (): void => {
|
||||
this.currentClick = null;
|
||||
this.currentPreview = null;
|
||||
|
||||
if (this.pointsToRevertPreview) {
|
||||
this.replaceCurrentShapePoints(this.pointsToRevertPreview);
|
||||
this.pointsToRevertPreview = null;
|
||||
}
|
||||
});
|
||||
|
||||
this.currentShape.on('drawpoint', (): void => {
|
||||
this.currentClick = null;
|
||||
this.currentPreview = null;
|
||||
this.pointsToRevertPreview = null;
|
||||
});
|
||||
} else {
|
||||
this.container.removeEventListener('mousemove', this.onContainerMouseMove);
|
||||
this.container.removeEventListener('mousedown', this.onContainerMouseDown);
|
||||
this.removeMarkers();
|
||||
this.enabled = false;
|
||||
this.currentShape = null;
|
||||
}
|
||||
}
|
||||
|
||||
public transform(geometry: Geometry): void {
|
||||
this.scale = geometry.scale;
|
||||
|
||||
this.pointGroups.forEach((group: SVGGElement): void => {
|
||||
Array.from(group.children).forEach((circle: SVGCircleElement): void => {
|
||||
circle.setAttribute('r', `${this.controlPointsSize / this.scale}`);
|
||||
circle.setAttribute('stroke-width', `${consts.BASE_STROKE_WIDTH / this.scale}`);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public configure(configuration: Configuration): void {
|
||||
this.controlPointsSize = configuration.controlPointsSize ?? consts.BASE_POINT_SIZE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
// Copyright (C) 2019-2022 Intel Corporation
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {
|
||||
DrawData, MergeData, SplitData, GroupData,
|
||||
JoinData, SliceData, MasksEditData,
|
||||
InteractionData as _InteractionData,
|
||||
InteractionResult as _InteractionResult,
|
||||
CanvasModel, CanvasModelImpl, RectDrawingMethod,
|
||||
CuboidDrawingMethod, Configuration, Geometry, Mode,
|
||||
HighlightSeverity as _HighlightSeverity, CanvasHint as _CanvasHint,
|
||||
PolyEditData, RenderData as _RenderData,
|
||||
} from './canvasModel';
|
||||
import { Master } from './master';
|
||||
import { CanvasController, CanvasControllerImpl } from './canvasController';
|
||||
import { CanvasView, CanvasViewImpl } from './canvasView';
|
||||
|
||||
import '../scss/canvas.scss';
|
||||
|
||||
interface Canvas {
|
||||
html(): HTMLDivElement;
|
||||
setup(frameData: any, objectStates: any[], renderData?: RenderData): void;
|
||||
setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void;
|
||||
translateFromSVG(points: number[]): number[];
|
||||
setupConflictRegions(clientID: number): number[];
|
||||
activate(clientID: number | null, attributeID?: number): void;
|
||||
highlight(clientIDs: number[] | null, severity: HighlightSeverity | null): void;
|
||||
rotate(rotationAngle: number): void;
|
||||
focus(clientID: number, padding?: number): void;
|
||||
fit(): void;
|
||||
grid(stepX: number, stepY: number): void;
|
||||
|
||||
interact(interactionData: InteractionData): void;
|
||||
draw(drawData: DrawData): void;
|
||||
edit(editData: MasksEditData | PolyEditData): void;
|
||||
group(groupData: GroupData): void;
|
||||
join(joinData: JoinData): void;
|
||||
slice(sliceData: SliceData): void;
|
||||
split(splitData: SplitData): void;
|
||||
merge(mergeData: MergeData): void;
|
||||
select(objectState: any): void;
|
||||
|
||||
fitCanvas(): void;
|
||||
bitmap(enable: boolean): void;
|
||||
selectRegion(enable: boolean): void;
|
||||
dragCanvas(enable: boolean): void;
|
||||
zoomCanvas(enable: boolean): void;
|
||||
|
||||
mode(): Mode;
|
||||
cancel(): void;
|
||||
configure(configuration: Configuration): void;
|
||||
isAbleToChangeFrame(): boolean;
|
||||
destroy(): void;
|
||||
|
||||
readonly geometry: Geometry;
|
||||
}
|
||||
|
||||
class CanvasImpl implements Canvas {
|
||||
private model: CanvasModel & Master;
|
||||
private controller: CanvasController;
|
||||
private view: CanvasView;
|
||||
|
||||
public constructor() {
|
||||
this.model = new CanvasModelImpl();
|
||||
this.controller = new CanvasControllerImpl(this.model);
|
||||
this.view = new CanvasViewImpl(this.model, this.controller);
|
||||
}
|
||||
|
||||
public html(): HTMLDivElement {
|
||||
return this.view.html();
|
||||
}
|
||||
|
||||
public setup(frameData: any, objectStates: any[], renderData?: RenderData): void {
|
||||
this.model.setup(frameData, objectStates, renderData);
|
||||
}
|
||||
|
||||
public setupIssueRegions(issueRegions: Record<number, { hidden: boolean; points: number[] }>): void {
|
||||
this.model.setupIssueRegions(issueRegions);
|
||||
}
|
||||
|
||||
public translateFromSVG(points: number[]): number[] {
|
||||
return this.view.translateFromSVG(points);
|
||||
}
|
||||
|
||||
public setupConflictRegions(clientID: number): number[] {
|
||||
return this.view.setupConflictRegions(clientID);
|
||||
}
|
||||
|
||||
public fitCanvas(): void {
|
||||
this.model.fitCanvas(this.view.html().clientWidth, this.view.html().clientHeight);
|
||||
}
|
||||
|
||||
public bitmap(enable: boolean): void {
|
||||
this.model.bitmap(enable);
|
||||
}
|
||||
|
||||
public selectRegion(enable: boolean): void {
|
||||
this.model.selectRegion(enable);
|
||||
}
|
||||
|
||||
public dragCanvas(enable: boolean): void {
|
||||
this.model.dragCanvas(enable);
|
||||
}
|
||||
|
||||
public zoomCanvas(enable: boolean): void {
|
||||
this.model.zoomCanvas(enable);
|
||||
}
|
||||
|
||||
public activate(clientID: number | null, attributeID: number | null = null): void {
|
||||
this.model.activate(clientID, attributeID);
|
||||
}
|
||||
|
||||
public highlight(clientIDs: number[], severity: HighlightSeverity | null = null): void {
|
||||
this.model.highlight(clientIDs, severity);
|
||||
}
|
||||
|
||||
public rotate(rotationAngle: number): void {
|
||||
this.model.rotate(rotationAngle);
|
||||
}
|
||||
|
||||
public focus(clientID: number, padding = 0): void {
|
||||
this.model.focus(clientID, padding);
|
||||
}
|
||||
|
||||
public fit(): void {
|
||||
this.model.fit();
|
||||
}
|
||||
|
||||
public grid(stepX: number, stepY: number): void {
|
||||
this.model.grid(stepX, stepY);
|
||||
}
|
||||
|
||||
public interact(interactionData: InteractionData): void {
|
||||
this.model.interact(interactionData);
|
||||
}
|
||||
|
||||
public draw(drawData: DrawData): void {
|
||||
this.model.draw(drawData);
|
||||
}
|
||||
|
||||
public edit(editData: MasksEditData | PolyEditData): void {
|
||||
this.model.edit(editData);
|
||||
}
|
||||
|
||||
public split(splitData: SplitData): void {
|
||||
this.model.split(splitData);
|
||||
}
|
||||
|
||||
public group(groupData: GroupData): void {
|
||||
this.model.group(groupData);
|
||||
}
|
||||
|
||||
public join(joinData: JoinData): void {
|
||||
this.model.join(joinData);
|
||||
}
|
||||
|
||||
public slice(sliceData: SliceData): void {
|
||||
this.model.slice(sliceData);
|
||||
}
|
||||
|
||||
public merge(mergeData: MergeData): void {
|
||||
this.model.merge(mergeData);
|
||||
}
|
||||
|
||||
public select(objectState: any): void {
|
||||
this.model.select(objectState);
|
||||
}
|
||||
|
||||
public mode(): Mode {
|
||||
return this.model.mode;
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.model.cancel();
|
||||
}
|
||||
|
||||
public configure(configuration: Configuration): void {
|
||||
this.model.configure(configuration);
|
||||
}
|
||||
|
||||
public isAbleToChangeFrame(): boolean {
|
||||
return this.model.isAbleToChangeFrame();
|
||||
}
|
||||
|
||||
public get geometry(): Geometry {
|
||||
return this.model.geometry;
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
this.model.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
export type InteractionData = _InteractionData;
|
||||
export type CanvasHint = _CanvasHint;
|
||||
export type InteractionResult = _InteractionResult;
|
||||
export type HighlightSeverity = _HighlightSeverity;
|
||||
export type RenderData = _RenderData;
|
||||
|
||||
export {
|
||||
CanvasImpl as Canvas, RectDrawingMethod, CuboidDrawingMethod, Mode as CanvasMode,
|
||||
};
|
||||
@@ -0,0 +1,189 @@
|
||||
// Copyright (C) 2019-2022 Intel Corporation
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import {
|
||||
CanvasModel,
|
||||
Geometry,
|
||||
Position,
|
||||
FocusData,
|
||||
ActiveElement,
|
||||
DrawData,
|
||||
MergeData,
|
||||
SplitData,
|
||||
GroupData,
|
||||
JoinData,
|
||||
SliceData,
|
||||
Mode,
|
||||
InteractionData,
|
||||
Configuration,
|
||||
MasksEditData,
|
||||
HighlightedElements,
|
||||
PolyEditData,
|
||||
RenderData,
|
||||
} from './canvasModel';
|
||||
|
||||
export interface CanvasController {
|
||||
readonly objects: any[];
|
||||
readonly renderData: RenderData;
|
||||
readonly issueRegions: Record<number, { hidden: boolean; points: number[] }>;
|
||||
readonly focusData: FocusData;
|
||||
readonly activeElement: ActiveElement;
|
||||
readonly highlightedElements: HighlightedElements;
|
||||
readonly drawData: DrawData;
|
||||
readonly editData: MasksEditData | PolyEditData;
|
||||
readonly interactionData: InteractionData;
|
||||
readonly mergeData: MergeData;
|
||||
readonly splitData: SplitData;
|
||||
readonly groupData: GroupData;
|
||||
readonly joinData: JoinData;
|
||||
readonly sliceData: SliceData;
|
||||
readonly selected: any;
|
||||
readonly configuration: Configuration;
|
||||
mode: Mode;
|
||||
geometry: Geometry;
|
||||
|
||||
zoom(x: number, y: number, deltaY: number): void;
|
||||
draw(drawData: DrawData): void;
|
||||
edit(editData: MasksEditData | PolyEditData): void;
|
||||
enableDrag(x: number, y: number): void;
|
||||
drag(x: number, y: number): void;
|
||||
disableDrag(): void;
|
||||
fit(): void;
|
||||
focus(clientId: number, padding: number): void;
|
||||
}
|
||||
|
||||
export class CanvasControllerImpl implements CanvasController {
|
||||
private model: CanvasModel;
|
||||
private lastDragPosition: Position;
|
||||
private isDragging: boolean;
|
||||
|
||||
public constructor(model: CanvasModel) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
public zoom(x: number, y: number, deltaY: number): void {
|
||||
this.model.zoom(x, y, deltaY);
|
||||
}
|
||||
|
||||
public fit(): void {
|
||||
this.model.fit();
|
||||
}
|
||||
|
||||
public enableDrag(x: number, y: number): void {
|
||||
this.lastDragPosition = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
this.isDragging = true;
|
||||
}
|
||||
|
||||
public drag(x: number, y: number): void {
|
||||
if (this.isDragging) {
|
||||
const topOffset: number = y - this.lastDragPosition.y;
|
||||
const leftOffset: number = x - this.lastDragPosition.x;
|
||||
this.lastDragPosition = {
|
||||
x,
|
||||
y,
|
||||
};
|
||||
this.model.move(topOffset, leftOffset);
|
||||
}
|
||||
}
|
||||
|
||||
public disableDrag(): void {
|
||||
this.isDragging = false;
|
||||
}
|
||||
|
||||
public draw(drawData: DrawData): void {
|
||||
this.model.draw(drawData);
|
||||
}
|
||||
|
||||
public edit(editData: MasksEditData | PolyEditData): void {
|
||||
this.model.edit(editData);
|
||||
}
|
||||
|
||||
public focus(clientID: number, padding: number): void {
|
||||
this.model.focus(clientID, padding);
|
||||
}
|
||||
|
||||
public get geometry(): Geometry {
|
||||
return this.model.geometry;
|
||||
}
|
||||
|
||||
public set geometry(geometry: Geometry) {
|
||||
this.model.geometry = geometry;
|
||||
}
|
||||
|
||||
public get issueRegions(): Record<number, { hidden: boolean; points: number[] }> {
|
||||
return this.model.issueRegions;
|
||||
}
|
||||
|
||||
public get objects(): any[] {
|
||||
return this.model.objects;
|
||||
}
|
||||
|
||||
public get renderData(): RenderData {
|
||||
return this.model.renderData;
|
||||
}
|
||||
|
||||
public get focusData(): FocusData {
|
||||
return this.model.focusData;
|
||||
}
|
||||
|
||||
public get activeElement(): ActiveElement {
|
||||
return this.model.activeElement;
|
||||
}
|
||||
|
||||
public get highlightedElements(): HighlightedElements {
|
||||
return this.model.highlightedElements;
|
||||
}
|
||||
|
||||
public get drawData(): DrawData {
|
||||
return this.model.drawData;
|
||||
}
|
||||
|
||||
public get editData(): MasksEditData | PolyEditData {
|
||||
return this.model.editData;
|
||||
}
|
||||
|
||||
public get interactionData(): InteractionData {
|
||||
return this.model.interactionData;
|
||||
}
|
||||
|
||||
public get mergeData(): MergeData {
|
||||
return this.model.mergeData;
|
||||
}
|
||||
|
||||
public get splitData(): SplitData {
|
||||
return this.model.splitData;
|
||||
}
|
||||
|
||||
public get groupData(): GroupData {
|
||||
return this.model.groupData;
|
||||
}
|
||||
|
||||
public get joinData(): JoinData {
|
||||
return this.model.joinData;
|
||||
}
|
||||
|
||||
public get sliceData(): SliceData {
|
||||
return this.model.sliceData;
|
||||
}
|
||||
|
||||
public get selected(): any {
|
||||
return this.model.selected;
|
||||
}
|
||||
|
||||
public get configuration(): Configuration {
|
||||
return this.model.configuration;
|
||||
}
|
||||
|
||||
public set mode(value: Mode) {
|
||||
this.model.mode = value;
|
||||
}
|
||||
|
||||
public get mode(): Mode {
|
||||
return this.model.mode;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,59 @@
|
||||
// Copyright (C) 2019-2022 Intel Corporation
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
const BASE_STROKE_WIDTH = 1.25;
|
||||
const BASE_GRID_WIDTH = 2;
|
||||
const BASE_POINT_SIZE = 4;
|
||||
const TEXT_MARGIN = 10;
|
||||
const SIZE_THRESHOLD = 1;
|
||||
const POINTS_STROKE_WIDTH = 1;
|
||||
const POINTS_SELECTED_STROKE_WIDTH = 4;
|
||||
const MIN_EDGE_LENGTH = 3;
|
||||
const CUBOID_ACTIVE_EDGE_STROKE_WIDTH = 2.5;
|
||||
const CUBOID_UNACTIVE_EDGE_STROKE_WIDTH = 1.75;
|
||||
const UNDEFINED_ATTRIBUTE_VALUE = '__undefined__';
|
||||
const ARROW_PATH = 'M13.162 6.284L.682.524a.483.483 0 0 0-.574.134.477.477 0 ' +
|
||||
'0 0-.012.59L4.2 6.72.096 12.192a.479.479 0 0 0 .585.724l12.48-5.76a.48.48 0 0 0 0-.872z';
|
||||
const BASE_PATTERN_SIZE = 5;
|
||||
const SNAP_TO_ANGLE_RESIZE_DEFAULT = 0.1;
|
||||
const SNAP_TO_ANGLE_RESIZE_SHIFT = 15;
|
||||
const MINIMUM_TEXT_FONT_SIZE = 8;
|
||||
const SKELETON_RECT_MARGIN = 20;
|
||||
|
||||
const DEFAULT_SHAPE_TEXT_SIZE = 12;
|
||||
const DEFAULT_SHAPE_TEXT_CONTENT = 'id,label,attributes,source,descriptions';
|
||||
const DEFAULT_SHAPE_TEXT_POSITION: 'auto' | 'center' = 'auto';
|
||||
const DEFAULT_UNDEFINED_ATTR_VALUE = '__undefined__';
|
||||
|
||||
const CONFLICT_COLOR = '#ff4800';
|
||||
const WARNING_COLOR = '#ff7301';
|
||||
const SHADED_COLOR = '#808080';
|
||||
|
||||
export default {
|
||||
BASE_STROKE_WIDTH,
|
||||
BASE_GRID_WIDTH,
|
||||
BASE_POINT_SIZE,
|
||||
TEXT_MARGIN,
|
||||
SIZE_THRESHOLD,
|
||||
POINTS_STROKE_WIDTH,
|
||||
POINTS_SELECTED_STROKE_WIDTH,
|
||||
MIN_EDGE_LENGTH,
|
||||
CUBOID_ACTIVE_EDGE_STROKE_WIDTH,
|
||||
CUBOID_UNACTIVE_EDGE_STROKE_WIDTH,
|
||||
UNDEFINED_ATTRIBUTE_VALUE,
|
||||
ARROW_PATH,
|
||||
BASE_PATTERN_SIZE,
|
||||
SNAP_TO_ANGLE_RESIZE_DEFAULT,
|
||||
SNAP_TO_ANGLE_RESIZE_SHIFT,
|
||||
DEFAULT_SHAPE_TEXT_SIZE,
|
||||
DEFAULT_SHAPE_TEXT_CONTENT,
|
||||
DEFAULT_SHAPE_TEXT_POSITION,
|
||||
DEFAULT_UNDEFINED_ATTR_VALUE,
|
||||
MINIMUM_TEXT_FONT_SIZE,
|
||||
SKELETON_RECT_MARGIN,
|
||||
CONFLICT_COLOR,
|
||||
WARNING_COLOR,
|
||||
SHADED_COLOR,
|
||||
};
|
||||
@@ -0,0 +1,81 @@
|
||||
// Copyright (C) 2020-2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
import consts from './consts';
|
||||
|
||||
export default class Crosshair {
|
||||
private x: SVG.Line | null;
|
||||
private y: SVG.Line | null;
|
||||
private canvas: SVG.Container | null;
|
||||
|
||||
public constructor() {
|
||||
this.x = null;
|
||||
this.y = null;
|
||||
this.canvas = null;
|
||||
}
|
||||
|
||||
public show(canvas: SVG.Container, x: number, y: number, scale: number): void {
|
||||
if (this.canvas && this.canvas !== canvas) {
|
||||
if (this.x) this.x.remove();
|
||||
if (this.y) this.y.remove();
|
||||
this.x = null;
|
||||
this.y = null;
|
||||
}
|
||||
|
||||
if (this.x && this.y) {
|
||||
this.move(x, y);
|
||||
return;
|
||||
}
|
||||
|
||||
this.canvas = canvas;
|
||||
this.x = this.canvas
|
||||
.line(0, y, this.canvas.node.clientWidth, y)
|
||||
.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
|
||||
})
|
||||
.addClass('cvat_canvas_crosshair');
|
||||
|
||||
this.y = this.canvas
|
||||
.line(x, 0, x, this.canvas.node.clientHeight)
|
||||
.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / (2 * scale),
|
||||
})
|
||||
.addClass('cvat_canvas_crosshair');
|
||||
}
|
||||
|
||||
public hide(): void {
|
||||
if (this.x) {
|
||||
this.x.remove();
|
||||
this.x = null;
|
||||
}
|
||||
|
||||
if (this.y) {
|
||||
this.y.remove();
|
||||
this.y = null;
|
||||
}
|
||||
|
||||
this.canvas = null;
|
||||
}
|
||||
|
||||
public move(x: number, y: number): void {
|
||||
if (this.x) {
|
||||
this.x.attr({ y1: y, y2: y });
|
||||
}
|
||||
|
||||
if (this.y) {
|
||||
this.y.attr({ x1: x, x2: x });
|
||||
}
|
||||
}
|
||||
|
||||
public scale(scale: number): void {
|
||||
if (this.x) {
|
||||
this.x.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
|
||||
}
|
||||
|
||||
if (this.y) {
|
||||
this.y.attr('stroke-width', consts.BASE_STROKE_WIDTH / (2 * scale));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
// Copyright (C) 2021-2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import consts from './consts';
|
||||
import { Point } from './shared';
|
||||
|
||||
export enum Orientation {
|
||||
LEFT = 'left',
|
||||
RIGHT = 'right',
|
||||
}
|
||||
|
||||
export function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
|
||||
// Check if none of the lines are of length 0
|
||||
const { x: x1, y: y1 } = p1;
|
||||
const { x: x2, y: y2 } = p2;
|
||||
const { x: x3, y: y3 } = p3;
|
||||
const { x: x4, y: y4 } = p4;
|
||||
|
||||
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
|
||||
|
||||
// Lines are parallel
|
||||
if (Math.abs(denominator) < Number.EPSILON) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
|
||||
|
||||
// Return a object with the x and y coordinates of the intersection
|
||||
return { x: x1 + ua * (x2 - x1), y: y1 + ua * (y2 - y1) };
|
||||
}
|
||||
|
||||
export class Equation {
|
||||
private a: number;
|
||||
private b: number;
|
||||
private c: number;
|
||||
|
||||
public constructor(p1: Point, p2: Point) {
|
||||
this.a = p1.y - p2.y;
|
||||
this.b = p2.x - p1.x;
|
||||
this.c = this.b * p1.y + this.a * p1.x;
|
||||
}
|
||||
|
||||
// get the line equation in actual coordinates
|
||||
public getY(x: number): number {
|
||||
return (this.c - this.a * x) / this.b;
|
||||
}
|
||||
}
|
||||
|
||||
export class Figure {
|
||||
private indices: number[];
|
||||
private allPoints: Point[];
|
||||
|
||||
public constructor(indices: number[], points: Point[]) {
|
||||
this.indices = indices;
|
||||
this.allPoints = points;
|
||||
}
|
||||
|
||||
public get points(): Point[] {
|
||||
const points = [];
|
||||
for (const index of this.indices) {
|
||||
points.push(this.allPoints[index]);
|
||||
}
|
||||
return points;
|
||||
}
|
||||
|
||||
// sets the point for a given edge, points must be given in
|
||||
// array form in the same ordering as the getter
|
||||
// if you only need to update a subset of the points,
|
||||
// simply put null for the points you want to keep
|
||||
public set points(newPoints) {
|
||||
const oldPoints = this.allPoints;
|
||||
for (let i = 0; i < newPoints.length; i += 1) {
|
||||
if (newPoints[i] !== null) {
|
||||
oldPoints[this.indices[i]] = { x: newPoints[i].x, y: newPoints[i].y };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Edge extends Figure {
|
||||
public getEquation(): Equation {
|
||||
return new Equation(this.points[0], this.points[1]);
|
||||
}
|
||||
}
|
||||
|
||||
export class CuboidModel {
|
||||
public points: Point[];
|
||||
private fr: Edge;
|
||||
private fl: Edge;
|
||||
private dr: Edge;
|
||||
private dl: Edge;
|
||||
private ft: Edge;
|
||||
private rt: Edge;
|
||||
private lt: Edge;
|
||||
private dt: Edge;
|
||||
private fb: Edge;
|
||||
private rb: Edge;
|
||||
private lb: Edge;
|
||||
private db: Edge;
|
||||
public edgeList: Edge[];
|
||||
private front: Figure;
|
||||
private right: Figure;
|
||||
private dorsal: Figure;
|
||||
private left: Figure;
|
||||
private top: Figure;
|
||||
private bot: Figure;
|
||||
public facesList: Figure[];
|
||||
public vpl: Point | null;
|
||||
public vpr: Point | null;
|
||||
public orientation: Orientation;
|
||||
|
||||
public constructor(points?: Point[]) {
|
||||
this.points = points;
|
||||
this.initEdges();
|
||||
this.initFaces();
|
||||
this.updateVanishingPoints(false);
|
||||
this.buildBackEdge(false);
|
||||
this.updatePoints();
|
||||
this.updateOrientation();
|
||||
}
|
||||
|
||||
public getPoints(): Point[] {
|
||||
return this.points;
|
||||
}
|
||||
|
||||
public setPoints(points: (Point | null)[]): void {
|
||||
points.forEach((point: Point | null, i: number): void => {
|
||||
if (point !== null) {
|
||||
this.points[i].x = point.x;
|
||||
this.points[i].y = point.y;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public updateOrientation(): void {
|
||||
if (this.dl.points[0].x > this.fl.points[0].x) {
|
||||
this.orientation = Orientation.LEFT;
|
||||
} else {
|
||||
this.orientation = Orientation.RIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
public updatePoints(): void {
|
||||
// making sure that the edges are vertical
|
||||
this.fr.points[0].x = this.fr.points[1].x;
|
||||
this.fl.points[0].x = this.fl.points[1].x;
|
||||
this.dr.points[0].x = this.dr.points[1].x;
|
||||
this.dl.points[0].x = this.dl.points[1].x;
|
||||
}
|
||||
|
||||
public computeSideEdgeConstraints(edge: any): any {
|
||||
const midLength = this.fr.points[1].y - this.fr.points[0].y - 1;
|
||||
|
||||
const minY = edge.points[1].y - midLength;
|
||||
const maxY = edge.points[0].y + midLength;
|
||||
|
||||
const y1 = edge.points[0].y;
|
||||
const y2 = edge.points[1].y;
|
||||
|
||||
const miny1 = y2 - midLength;
|
||||
const maxy1 = y2 - consts.MIN_EDGE_LENGTH;
|
||||
|
||||
const miny2 = y1 + consts.MIN_EDGE_LENGTH;
|
||||
const maxy2 = y1 + midLength;
|
||||
|
||||
return {
|
||||
constraint: {
|
||||
minY,
|
||||
maxY,
|
||||
},
|
||||
y1Range: {
|
||||
max: maxy1,
|
||||
min: miny1,
|
||||
},
|
||||
y2Range: {
|
||||
max: maxy2,
|
||||
min: miny2,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// boolean value parameter controls which edges should be used to recalculate vanishing points
|
||||
private updateVanishingPoints(buildright: boolean): void {
|
||||
let leftEdge = [];
|
||||
let rightEdge = [];
|
||||
let midEdge = [];
|
||||
if (buildright) {
|
||||
leftEdge = this.fr.points;
|
||||
rightEdge = this.dl.points;
|
||||
midEdge = this.fl.points;
|
||||
} else {
|
||||
leftEdge = this.fl.points;
|
||||
rightEdge = this.dr.points;
|
||||
midEdge = this.fr.points;
|
||||
}
|
||||
|
||||
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
|
||||
this.vpr = intersection(rightEdge[0], midEdge[0], rightEdge[1], midEdge[1]);
|
||||
if (this.vpl === null) {
|
||||
// shift the edge slightly to avoid edge case
|
||||
leftEdge[0].y -= 0.001;
|
||||
leftEdge[0].x += 0.001;
|
||||
leftEdge[1].x += 0.001;
|
||||
this.vpl = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
|
||||
}
|
||||
if (this.vpr === null) {
|
||||
// shift the edge slightly to avoid edge case
|
||||
rightEdge[0].y -= 0.001;
|
||||
rightEdge[0].x -= 0.001;
|
||||
rightEdge[1].x -= 0.001;
|
||||
this.vpr = intersection(leftEdge[0], midEdge[0], leftEdge[1], midEdge[1]);
|
||||
}
|
||||
}
|
||||
|
||||
private initEdges(): void {
|
||||
this.fl = new Edge([0, 1], this.points);
|
||||
this.fr = new Edge([2, 3], this.points);
|
||||
this.dr = new Edge([4, 5], this.points);
|
||||
this.dl = new Edge([6, 7], this.points);
|
||||
|
||||
this.ft = new Edge([0, 2], this.points);
|
||||
this.lt = new Edge([0, 6], this.points);
|
||||
this.rt = new Edge([2, 4], this.points);
|
||||
this.dt = new Edge([6, 4], this.points);
|
||||
|
||||
this.fb = new Edge([1, 3], this.points);
|
||||
this.lb = new Edge([1, 7], this.points);
|
||||
this.rb = new Edge([3, 5], this.points);
|
||||
this.db = new Edge([7, 5], this.points);
|
||||
|
||||
this.edgeList = [
|
||||
this.fl,
|
||||
this.fr,
|
||||
this.dl,
|
||||
this.dr,
|
||||
this.ft,
|
||||
this.lt,
|
||||
this.rt,
|
||||
this.dt,
|
||||
this.fb,
|
||||
this.lb,
|
||||
this.rb,
|
||||
this.db,
|
||||
];
|
||||
}
|
||||
|
||||
private initFaces(): void {
|
||||
this.front = new Figure([0, 1, 3, 2], this.points);
|
||||
this.right = new Figure([2, 3, 5, 4], this.points);
|
||||
this.dorsal = new Figure([4, 5, 7, 6], this.points);
|
||||
this.left = new Figure([6, 7, 1, 0], this.points);
|
||||
this.top = new Figure([0, 2, 4, 6], this.points);
|
||||
this.bot = new Figure([1, 3, 5, 7], this.points);
|
||||
|
||||
this.facesList = [this.front, this.right, this.dorsal, this.left];
|
||||
}
|
||||
|
||||
private buildBackEdge(buildright: boolean): void {
|
||||
this.updateVanishingPoints(buildright);
|
||||
let leftPoints = [];
|
||||
let rightPoints = [];
|
||||
|
||||
let topIndex = 0;
|
||||
let botIndex = 0;
|
||||
|
||||
if (buildright) {
|
||||
leftPoints = this.dl.points;
|
||||
rightPoints = this.fr.points;
|
||||
topIndex = 4;
|
||||
botIndex = 5;
|
||||
} else {
|
||||
leftPoints = this.dr.points;
|
||||
rightPoints = this.fl.points;
|
||||
topIndex = 6;
|
||||
botIndex = 7;
|
||||
}
|
||||
|
||||
const vpLeft = this.vpl;
|
||||
const vpRight = this.vpr;
|
||||
|
||||
let p1 = intersection(vpLeft, leftPoints[0], vpRight, rightPoints[0]);
|
||||
let p2 = intersection(vpLeft, leftPoints[1], vpRight, rightPoints[1]);
|
||||
|
||||
if (p1 === null) {
|
||||
p1 = { x: p2.x, y: vpLeft.y };
|
||||
} else if (p2 === null) {
|
||||
p2 = { x: p1.x, y: vpLeft.y };
|
||||
}
|
||||
|
||||
this.points[topIndex] = { x: p1.x, y: p1.y };
|
||||
this.points[botIndex] = { x: p2.x, y: p2.y };
|
||||
|
||||
// Making sure that the vertical edges stay vertical
|
||||
this.updatePoints();
|
||||
}
|
||||
}
|
||||
|
||||
function sortPointsClockwise(points: any[]): any[] {
|
||||
points.sort((a, b): number => a.y - b.y);
|
||||
// Get center y
|
||||
const cy = (points[0].y + points[points.length - 1].y) / 2;
|
||||
|
||||
// Sort from right to left
|
||||
points.sort((a, b): number => b.x - a.x);
|
||||
|
||||
// Get center x
|
||||
const cx = (points[0].x + points[points.length - 1].x) / 2;
|
||||
|
||||
// Center point
|
||||
const center = {
|
||||
x: cx,
|
||||
y: cy,
|
||||
};
|
||||
|
||||
// Starting angle used to reference other angles
|
||||
let startAng: number | undefined;
|
||||
points.forEach((point): void => {
|
||||
let ang = Math.atan2(point.y - center.y, point.x - center.x);
|
||||
if (!startAng) {
|
||||
startAng = ang;
|
||||
// ensure that all points are clockwise of the start point
|
||||
} else if (ang < startAng) {
|
||||
ang += Math.PI * 2;
|
||||
}
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
point.angle = ang; // add the angle to the point
|
||||
});
|
||||
|
||||
// first sort clockwise
|
||||
points.sort((a, b): number => a.angle - b.angle);
|
||||
return points.reverse();
|
||||
}
|
||||
|
||||
function setupCuboidPoints(points: Point[]): any[] {
|
||||
let left;
|
||||
let right;
|
||||
let left2;
|
||||
let right2;
|
||||
let p1;
|
||||
let p2;
|
||||
let p3;
|
||||
let p4;
|
||||
|
||||
const height = Math.abs(points[0].x - points[1].x) < Math.abs(points[1].x - points[2].x) ?
|
||||
Math.abs(points[1].y - points[0].y) : Math.abs(points[1].y - points[2].y);
|
||||
|
||||
// separate into left and right point
|
||||
// we pick the first and third point because we know assume they will be on
|
||||
// opposite corners
|
||||
if (points[0].x < points[2].x) {
|
||||
[left, , right] = points;
|
||||
} else {
|
||||
[right, , left] = points;
|
||||
}
|
||||
|
||||
// get other 2 points using the given height
|
||||
if (left.y < right.y) {
|
||||
left2 = { x: left.x, y: left.y + height };
|
||||
right2 = { x: right.x, y: right.y - height };
|
||||
} else {
|
||||
left2 = { x: left.x, y: left.y - height };
|
||||
right2 = { x: right.x, y: right.y + height };
|
||||
}
|
||||
|
||||
// get the vector for the last point relative to the previous point
|
||||
const vec = {
|
||||
x: points[3].x - points[2].x,
|
||||
y: points[3].y - points[2].y,
|
||||
};
|
||||
|
||||
if (left.y < left2.y) {
|
||||
p1 = left;
|
||||
p2 = left2;
|
||||
} else {
|
||||
p1 = left2;
|
||||
p2 = left;
|
||||
}
|
||||
|
||||
if (right.y < right2.y) {
|
||||
p3 = right;
|
||||
p4 = right2;
|
||||
} else {
|
||||
p3 = right2;
|
||||
p4 = right;
|
||||
}
|
||||
|
||||
const p5 = { x: p3.x + vec.x, y: p3.y + vec.y + 0.1 };
|
||||
const p6 = { x: p4.x + vec.x, y: p4.y + vec.y - 0.1 };
|
||||
const p7 = { x: p1.x + vec.x, y: p1.y + vec.y + 0.1 };
|
||||
const p8 = { x: p2.x + vec.x, y: p2.y + vec.y - 0.1 };
|
||||
|
||||
p1.y += 0.1;
|
||||
return [p1, p2, p3, p4, p5, p6, p7, p8];
|
||||
}
|
||||
|
||||
export function cuboidFrom4Points(flattenedPoints: any[]): any[] {
|
||||
const points: Point[] = [];
|
||||
for (let i = 0; i < 4; i++) {
|
||||
const [x, y] = flattenedPoints.slice(i * 2, i * 2 + 2);
|
||||
points.push({ x, y });
|
||||
}
|
||||
const unsortedPlanePoints = points.slice(0, 3);
|
||||
function rotate(array: any[], times: number): void {
|
||||
let t = times;
|
||||
while (t--) {
|
||||
const temp = array.shift();
|
||||
array.push(temp);
|
||||
}
|
||||
}
|
||||
|
||||
const plane2 = {
|
||||
p1: points[0],
|
||||
p2: points[0],
|
||||
p3: points[0],
|
||||
p4: points[0],
|
||||
};
|
||||
|
||||
// completing the plane
|
||||
const vector = {
|
||||
x: points[2].x - points[1].x,
|
||||
y: points[2].y - points[1].y,
|
||||
};
|
||||
|
||||
// sorting the first plane
|
||||
unsortedPlanePoints.push({
|
||||
x: points[0].x + vector.x,
|
||||
y: points[0].y + vector.y,
|
||||
});
|
||||
const sortedPlanePoints = sortPointsClockwise(unsortedPlanePoints);
|
||||
let leftIndex = 0;
|
||||
for (let i = 0; i < 4; i++) {
|
||||
leftIndex = sortedPlanePoints[i].x < sortedPlanePoints[leftIndex].x ? i : leftIndex;
|
||||
}
|
||||
rotate(sortedPlanePoints, leftIndex);
|
||||
const plane1 = {
|
||||
p1: sortedPlanePoints[0],
|
||||
p2: sortedPlanePoints[1],
|
||||
p3: sortedPlanePoints[2],
|
||||
p4: sortedPlanePoints[3],
|
||||
};
|
||||
|
||||
const vec = {
|
||||
x: points[3].x - points[2].x,
|
||||
y: points[3].y - points[2].y,
|
||||
};
|
||||
// determine the orientation
|
||||
const angle = Math.atan2(vec.y, vec.x);
|
||||
|
||||
// making the other plane
|
||||
plane2.p1 = { x: plane1.p1.x + vec.x, y: plane1.p1.y + vec.y };
|
||||
plane2.p2 = { x: plane1.p2.x + vec.x, y: plane1.p2.y + vec.y };
|
||||
plane2.p3 = { x: plane1.p3.x + vec.x, y: plane1.p3.y + vec.y };
|
||||
plane2.p4 = { x: plane1.p4.x + vec.x, y: plane1.p4.y + vec.y };
|
||||
|
||||
let cuboidPoints;
|
||||
// right
|
||||
if (Math.abs(angle) < Math.PI / 2 - 0.1) {
|
||||
cuboidPoints = setupCuboidPoints(points);
|
||||
// left
|
||||
} else if (Math.abs(angle) > Math.PI / 2 + 0.1) {
|
||||
cuboidPoints = setupCuboidPoints(points);
|
||||
// down
|
||||
} else if (angle > 0) {
|
||||
cuboidPoints = [plane1.p1, plane2.p1, plane1.p2, plane2.p2, plane1.p3, plane2.p3, plane1.p4, plane2.p4];
|
||||
cuboidPoints[0].y += 0.1;
|
||||
cuboidPoints[4].y += 0.1;
|
||||
// up
|
||||
} else {
|
||||
cuboidPoints = [plane2.p1, plane1.p1, plane2.p2, plane1.p2, plane2.p3, plane1.p3, plane2.p4, plane1.p4];
|
||||
cuboidPoints[0].y += 0.1;
|
||||
cuboidPoints[4].y += 0.1;
|
||||
}
|
||||
|
||||
return cuboidPoints.reduce((arr: number[], point: any): number[] => {
|
||||
arr.push(point.x);
|
||||
arr.push(point.y);
|
||||
return arr;
|
||||
}, []);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,479 @@
|
||||
// Copyright (C) 2019-2022 Intel Corporation
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import * as SVG from 'svg.js';
|
||||
import 'svg.select.js';
|
||||
|
||||
import consts from './consts';
|
||||
import { translateFromSVG, pointsToNumberArray } from './shared';
|
||||
import { PolyEditData, Geometry, Configuration } from './canvasModel';
|
||||
import { AutoborderHandler } from './autoborderHandler';
|
||||
|
||||
export interface EditHandler {
|
||||
edit(editData: PolyEditData): void;
|
||||
transform(geometry: Geometry): void;
|
||||
configure(configuration: Configuration): void;
|
||||
cancel(): void;
|
||||
enabled: boolean;
|
||||
shapeType: string;
|
||||
}
|
||||
|
||||
export class EditHandlerImpl implements EditHandler {
|
||||
private onEditDone: (state: any, points: number[]) => void;
|
||||
private autoborderHandler: AutoborderHandler;
|
||||
private geometry: Geometry | null;
|
||||
private canvas: SVG.Container;
|
||||
private editData: PolyEditData | null;
|
||||
private editedShape: SVG.Shape | null;
|
||||
private editLine: SVG.PolyLine | null;
|
||||
private clones: SVG.Polygon[];
|
||||
private controlPointsSize: number;
|
||||
private autobordersEnabled: boolean;
|
||||
private intelligentCutEnabled: boolean;
|
||||
private outlinedBorders: string;
|
||||
private isEditing: boolean;
|
||||
|
||||
private setupTrailingPoint(circle: SVG.Circle): void {
|
||||
circle.on('mouseenter', (): void => {
|
||||
circle.attr({
|
||||
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
});
|
||||
|
||||
circle.on('mouseleave', (): void => {
|
||||
circle.attr({
|
||||
'stroke-width': consts.POINTS_STROKE_WIDTH / this.geometry.scale,
|
||||
});
|
||||
});
|
||||
|
||||
circle.on('mousedown', (e: MouseEvent): void => {
|
||||
if (e.button !== 0) return;
|
||||
this.edit({ enabled: false });
|
||||
});
|
||||
}
|
||||
|
||||
private startEdit(): void {
|
||||
// get started coordinates
|
||||
const [clientX, clientY] = translateFromSVG(
|
||||
(this.canvas.node as any) as SVGSVGElement,
|
||||
this.editedShape.attr('points').split(' ')[this.editData.pointID].split(','),
|
||||
);
|
||||
|
||||
// generate mouse event
|
||||
const dummyEvent = new MouseEvent('mousedown', {
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
clientX,
|
||||
clientY,
|
||||
});
|
||||
|
||||
// Add ability to edit shapes by sliding
|
||||
// We need to remember last drawn point
|
||||
// to implementation of slide drawing
|
||||
const lastDrawnPoint: {
|
||||
x: number;
|
||||
y: number;
|
||||
} = {
|
||||
x: null,
|
||||
y: null,
|
||||
};
|
||||
|
||||
this.canvas.on('mousemove.edit', (e: MouseEvent): void => {
|
||||
if (e.shiftKey && ['polygon', 'polyline'].includes(this.editData.state.shapeType)) {
|
||||
if (lastDrawnPoint.x === null || lastDrawnPoint.y === null) {
|
||||
(this.editLine as any).draw('point', e);
|
||||
} else {
|
||||
const deltaThreshold = 15;
|
||||
const dxsqr = (e.clientX - lastDrawnPoint.x) ** 2;
|
||||
const dysqr = (e.clientY - lastDrawnPoint.y) ** 2;
|
||||
const delta = Math.sqrt(dxsqr + dysqr);
|
||||
if (delta > deltaThreshold) {
|
||||
(this.editLine as any).draw('point', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.editLine = (this.canvas as any).polyline();
|
||||
if (this.editData.state.shapeType === 'polyline') {
|
||||
(this.editLine as any).on('drawupdate', (e: CustomEvent): void => {
|
||||
const circle = (e.target as any).instance.remember('_paintHandler').set.last();
|
||||
if (circle) this.setupTrailingPoint(circle);
|
||||
});
|
||||
}
|
||||
|
||||
(this.editLine as any)
|
||||
.addClass('cvat_canvas_shape_drawing')
|
||||
.style({
|
||||
'pointer-events': 'none',
|
||||
'fill-opacity': 0,
|
||||
})
|
||||
.attr({
|
||||
'data-origin-client-id': this.editData.state.clientID,
|
||||
stroke: this.editedShape.attr('stroke'),
|
||||
})
|
||||
.on('drawstart drawpoint', (e: CustomEvent): void => {
|
||||
this.transform(this.geometry);
|
||||
lastDrawnPoint.x = e.detail.event.clientX;
|
||||
lastDrawnPoint.y = e.detail.event.clientY;
|
||||
})
|
||||
.on('drawupdate', (): void => this.transform(this.geometry))
|
||||
.draw(dummyEvent, { snapToGrid: 0.1 });
|
||||
|
||||
if (this.editData.state.shapeType === 'points') {
|
||||
this.editLine.attr('stroke-width', 0);
|
||||
(this.editLine as any).draw('undo');
|
||||
}
|
||||
|
||||
this.setupEditEvents();
|
||||
if (this.autobordersEnabled) {
|
||||
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
|
||||
}
|
||||
}
|
||||
|
||||
private setupEditEvents(): void {
|
||||
this.canvas.on('mousedown.edit', (e: MouseEvent): void => {
|
||||
if (e.button === 0 && !e.altKey) {
|
||||
(this.editLine as any).draw('point', e);
|
||||
} else if (e.button === 2 && this.editLine) {
|
||||
if (this.editData.state.shapeType === 'points' || this.editLine.attr('points').split(' ').length > 2) {
|
||||
(this.editLine as any).draw('undo');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private selectPolygon(shape: SVG.Polygon): void {
|
||||
const { offset } = this.geometry;
|
||||
const points = pointsToNumberArray(shape.attr('points')).map((coord: number): number => coord - offset);
|
||||
|
||||
const { state } = this.editData;
|
||||
this.edit({
|
||||
enabled: false,
|
||||
});
|
||||
this.onEditDone(state, points);
|
||||
}
|
||||
|
||||
private stopEdit(e: MouseEvent): void {
|
||||
if (!this.editLine) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get stop point and all points
|
||||
const stopPointID = Array.prototype.indexOf.call((e.target as HTMLElement).parentElement.children, e.target);
|
||||
const oldPoints = this.editedShape.attr('points').trim().split(' ');
|
||||
const linePoints = this.editLine.attr('points').trim().split(' ');
|
||||
|
||||
if (this.editLine.attr('points') === '0,0') {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute new point array
|
||||
const [start, stop] = [this.editData.pointID, stopPointID].sort((a, b): number => +a - +b);
|
||||
|
||||
if (this.editData.state.shapeType !== 'polygon') {
|
||||
let points = null;
|
||||
const { offset } = this.geometry;
|
||||
|
||||
if (this.editData.state.shapeType === 'polyline') {
|
||||
if (start !== this.editData.pointID) {
|
||||
linePoints.reverse();
|
||||
}
|
||||
points = oldPoints
|
||||
.slice(0, start)
|
||||
.concat(linePoints)
|
||||
.concat(oldPoints.slice(stop + 1));
|
||||
} else {
|
||||
points = oldPoints.concat(linePoints.slice(0, -1));
|
||||
}
|
||||
|
||||
points = pointsToNumberArray(points.join(' ')).map((coord: number): number => coord - offset);
|
||||
|
||||
const { state } = this.editData;
|
||||
this.edit({
|
||||
enabled: false,
|
||||
});
|
||||
this.onEditDone(state, points);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const cutIndexes1 = oldPoints.reduce(
|
||||
(acc: number[], _: string, i: number): number[] => (i >= stop || i <= start ? [...acc, i] : acc),
|
||||
[],
|
||||
);
|
||||
const cutIndexes2 = oldPoints.reduce(
|
||||
(acc: number[], _: string, i: number): number[] => (i <= stop && i >= start ? [...acc, i] : acc),
|
||||
[],
|
||||
);
|
||||
|
||||
const curveLength = (indexes: number[]): number => {
|
||||
const points = indexes
|
||||
.map((index: number): string => oldPoints[index])
|
||||
.map((point: string): string[] => point.split(','))
|
||||
.map((point: string[]): number[] => [+point[0], +point[1]]);
|
||||
let length = 0;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const dxsqr = (points[i][0] - points[i - 1][0]) ** 2;
|
||||
const dysqr = (points[i][1] - points[i - 1][1]) ** 2;
|
||||
length += Math.sqrt(dxsqr + dysqr);
|
||||
}
|
||||
|
||||
return length;
|
||||
};
|
||||
|
||||
const pointsCriteria = cutIndexes1.length > cutIndexes2.length;
|
||||
const lengthCriteria = curveLength(cutIndexes1) > curveLength(cutIndexes2);
|
||||
|
||||
if (start !== this.editData.pointID) {
|
||||
linePoints.reverse();
|
||||
}
|
||||
|
||||
const firstPart = oldPoints
|
||||
.slice(0, start)
|
||||
.concat(linePoints)
|
||||
.concat(oldPoints.slice(stop + 1));
|
||||
const secondPart = oldPoints.slice(start, stop).concat(linePoints.slice(1).reverse());
|
||||
|
||||
if (firstPart.length < 3 || secondPart.length < 3) {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// We do not need these events any more
|
||||
this.canvas.off('mousedown.edit');
|
||||
this.canvas.off('mousemove.edit');
|
||||
|
||||
(this.editLine as any).draw('stop');
|
||||
this.editLine.remove();
|
||||
this.editLine = null;
|
||||
|
||||
if (pointsCriteria && lengthCriteria && this.intelligentCutEnabled) {
|
||||
this.clones.push(this.canvas.polygon(firstPart.join(' ')));
|
||||
this.selectPolygon(this.clones[0]);
|
||||
// left indexes1 and
|
||||
} else if (!pointsCriteria && !lengthCriteria && this.intelligentCutEnabled) {
|
||||
this.clones.push(this.canvas.polygon(secondPart.join(' ')));
|
||||
this.selectPolygon(this.clones[0]);
|
||||
} else {
|
||||
for (const points of [firstPart, secondPart]) {
|
||||
this.clones.push(
|
||||
this.canvas
|
||||
.polygon(points.join(' '))
|
||||
.attr('fill', this.editedShape.attr('fill'))
|
||||
.attr('fill-opacity', '0.5')
|
||||
.addClass('cvat_canvas_shape'),
|
||||
);
|
||||
}
|
||||
|
||||
for (const clone of this.clones) {
|
||||
clone.on('click', (): void => this.selectPolygon(clone));
|
||||
clone
|
||||
.on('mouseenter', (): void => {
|
||||
clone.addClass('cvat_canvas_shape_splitting');
|
||||
})
|
||||
.on('mouseleave', (): void => {
|
||||
clone.removeClass('cvat_canvas_shape_splitting');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setupPoints(enabled: boolean): void {
|
||||
const stopEdit = this.stopEdit.bind(this);
|
||||
const getGeometry = (): Geometry => this.geometry;
|
||||
const fill = this.editedShape.attr('fill') || 'inherit';
|
||||
|
||||
if (enabled) {
|
||||
(this.editedShape as any).selectize(true, {
|
||||
deepSelect: true,
|
||||
pointSize: (2 * this.controlPointsSize) / getGeometry().scale,
|
||||
rotationPoint: false,
|
||||
pointType(cx: number, cy: number): SVG.Circle {
|
||||
const circle: SVG.Circle = this.nested
|
||||
.circle(this.options.pointSize)
|
||||
.stroke('black')
|
||||
.fill(fill)
|
||||
.center(cx, cy)
|
||||
.attr({
|
||||
'stroke-width': consts.POINTS_STROKE_WIDTH / getGeometry().scale,
|
||||
});
|
||||
|
||||
circle.node.addEventListener('mouseenter', (): void => {
|
||||
circle.attr({
|
||||
'stroke-width': consts.POINTS_SELECTED_STROKE_WIDTH / getGeometry().scale,
|
||||
});
|
||||
|
||||
circle.node.addEventListener('click', stopEdit);
|
||||
circle.addClass('cvat_canvas_selected_point');
|
||||
});
|
||||
|
||||
circle.node.addEventListener('mouseleave', (): void => {
|
||||
circle.attr({
|
||||
'stroke-width': consts.POINTS_STROKE_WIDTH / getGeometry().scale,
|
||||
});
|
||||
|
||||
circle.node.removeEventListener('click', stopEdit);
|
||||
circle.removeClass('cvat_canvas_selected_point');
|
||||
});
|
||||
|
||||
return circle;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
(this.editedShape as any).selectize(false, {
|
||||
deepSelect: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private release(): void {
|
||||
this.canvas.off('mousedown.edit');
|
||||
this.canvas.off('mousemove.edit');
|
||||
this.autoborderHandler.autoborder(false);
|
||||
this.isEditing = false;
|
||||
|
||||
if (this.editedShape) {
|
||||
this.setupPoints(false);
|
||||
this.editedShape.remove();
|
||||
this.editedShape = null;
|
||||
}
|
||||
|
||||
if (this.editLine) {
|
||||
(this.editLine as any).draw('stop');
|
||||
this.editLine.remove();
|
||||
this.editLine = null;
|
||||
}
|
||||
|
||||
if (this.clones.length) {
|
||||
for (const clone of this.clones) {
|
||||
clone.remove();
|
||||
}
|
||||
this.clones = [];
|
||||
}
|
||||
}
|
||||
|
||||
private initEditing(): void {
|
||||
this.editedShape = this.canvas
|
||||
.select(`#cvat_canvas_shape_${this.editData.state.clientID}`).first()
|
||||
.clone().attr('stroke', this.outlinedBorders);
|
||||
this.setupPoints(true);
|
||||
this.startEdit();
|
||||
this.isEditing = true;
|
||||
// draw points for this with selected and start editing till another point is clicked
|
||||
// click one of two parts to remove (in case of polygon only)
|
||||
|
||||
// else we can start draw polyline
|
||||
// after we have got shape and points, we are waiting for second point pressed on this shape
|
||||
}
|
||||
|
||||
private closeEditing(): void {
|
||||
if (this.isEditing && this.editData.state.shapeType === 'polyline') {
|
||||
const { offset } = this.geometry;
|
||||
const head = this.editedShape.attr('points').split(' ').slice(0, this.editData.pointID).join(' ');
|
||||
const stringifiedPoints = `${head} ${this.editLine.node.getAttribute('points').slice(0, -2)}`;
|
||||
const points = pointsToNumberArray(stringifiedPoints)
|
||||
.slice(0, -2)
|
||||
.map((coord: number): number => coord - offset);
|
||||
if (points.length >= 2 * 2) { // minimumPoints * 2
|
||||
const { state } = this.editData;
|
||||
this.onEditDone(state, points);
|
||||
}
|
||||
}
|
||||
this.release();
|
||||
}
|
||||
|
||||
public constructor(
|
||||
onEditDone: EditHandlerImpl['onEditDone'],
|
||||
canvas: SVG.Container,
|
||||
autoborderHandler: AutoborderHandler,
|
||||
) {
|
||||
this.autoborderHandler = autoborderHandler;
|
||||
this.autobordersEnabled = false;
|
||||
this.intelligentCutEnabled = false;
|
||||
this.controlPointsSize = consts.BASE_POINT_SIZE;
|
||||
this.outlinedBorders = 'black';
|
||||
this.onEditDone = onEditDone;
|
||||
this.canvas = canvas;
|
||||
this.editData = null;
|
||||
this.editedShape = null;
|
||||
this.editLine = null;
|
||||
this.geometry = null;
|
||||
this.clones = [];
|
||||
this.isEditing = false;
|
||||
}
|
||||
|
||||
public edit(editData: any): void {
|
||||
if (editData.enabled) {
|
||||
if (['polygon', 'polyline', 'points'].includes(editData.state.shapeType)) {
|
||||
this.editData = editData;
|
||||
this.initEditing();
|
||||
} else {
|
||||
this.cancel();
|
||||
}
|
||||
} else {
|
||||
this.closeEditing();
|
||||
this.editData = editData;
|
||||
}
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onEditDone(null, null);
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.isEditing;
|
||||
}
|
||||
|
||||
get shapeType(): string {
|
||||
return this.editData.state.shapeType;
|
||||
}
|
||||
|
||||
public configure(configuration: Configuration): void {
|
||||
this.autobordersEnabled = configuration.autoborders;
|
||||
this.outlinedBorders = configuration.outlinedBorders || 'black';
|
||||
|
||||
if (this.editedShape) {
|
||||
this.editedShape.attr('stroke', this.outlinedBorders);
|
||||
}
|
||||
|
||||
if (this.editLine) {
|
||||
this.editLine.attr('stroke', this.outlinedBorders);
|
||||
if (this.autobordersEnabled) {
|
||||
this.autoborderHandler.autoborder(true, this.editLine, this.editData.state.clientID);
|
||||
} else {
|
||||
this.autoborderHandler.autoborder(false);
|
||||
}
|
||||
}
|
||||
this.controlPointsSize = configuration.controlPointsSize || consts.BASE_POINT_SIZE;
|
||||
this.intelligentCutEnabled = configuration.intelligentPolygonCrop;
|
||||
}
|
||||
|
||||
public transform(geometry: Geometry): void {
|
||||
this.geometry = geometry;
|
||||
|
||||
if (this.editedShape) {
|
||||
this.editedShape.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editLine) {
|
||||
if (this.editData.state.shapeType !== 'points') {
|
||||
this.editLine.attr({
|
||||
'stroke-width': consts.BASE_STROKE_WIDTH / geometry.scale,
|
||||
});
|
||||
}
|
||||
|
||||
const paintHandler = this.editLine.remember('_paintHandler');
|
||||
for (const point of paintHandler.set.members) {
|
||||
point.attr('stroke-width', `${consts.POINTS_STROKE_WIDTH / geometry.scale}`);
|
||||
point.attr('r', `${this.controlPointsSize / geometry.scale}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (C) 2019-2022 Intel Corporation
|
||||
// Copyright (C) CVAT.ai Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { GroupData } from './canvasModel';
|
||||
import { ObjectSelector, SelectionFilter } from './objectSelector';
|
||||
|
||||
export interface GroupHandler {
|
||||
group(groupData: GroupData, selectionFilter: SelectionFilter): void;
|
||||
select(state: any): void;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export class GroupHandlerImpl implements GroupHandler {
|
||||
private onSelectDone: (objects?: any[], duration?: number) => void;
|
||||
private selector: ObjectSelector;
|
||||
private initialized: boolean;
|
||||
private statesToBeGrouped: any[];
|
||||
private startTimestamp: number;
|
||||
|
||||
private release(): void {
|
||||
this.selector.disable();
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
private initGrouping(selectionFilter: SelectionFilter): void {
|
||||
this.statesToBeGrouped = [];
|
||||
this.selector.enable((selected) => {
|
||||
this.statesToBeGrouped = selected;
|
||||
}, selectionFilter);
|
||||
this.initialized = true;
|
||||
this.startTimestamp = Date.now();
|
||||
}
|
||||
|
||||
private closeGrouping(): void {
|
||||
if (this.initialized) {
|
||||
const { statesToBeGrouped } = this;
|
||||
this.release();
|
||||
if (statesToBeGrouped.length) {
|
||||
this.onSelectDone(statesToBeGrouped, Date.now() - this.startTimestamp);
|
||||
} else {
|
||||
this.onSelectDone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public constructor(
|
||||
onSelectDone: GroupHandlerImpl['onSelectDone'],
|
||||
selector: ObjectSelector,
|
||||
) {
|
||||
this.onSelectDone = onSelectDone;
|
||||
this.selector = selector;
|
||||
this.statesToBeGrouped = [];
|
||||
this.initialized = false;
|
||||
this.startTimestamp = Date.now();
|
||||
}
|
||||
|
||||
public group(groupData: GroupData, selectionFilter: SelectionFilter): void {
|
||||
if (groupData.enabled) {
|
||||
this.initGrouping(selectionFilter);
|
||||
} else {
|
||||
this.closeGrouping();
|
||||
}
|
||||
}
|
||||
|
||||
public select(objectState: any): void {
|
||||
this.selector.push(objectState);
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.release();
|
||||
this.onSelectDone();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user