diff --git a/CHANGELOG.md b/CHANGELOG.md index 62372d0614d0..da55f5dcd034 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for working with ellipses () - Add several flags to task creation CLI () - Add YOLOv5 serverless function for automatic annotation () +- Add possibility to change git repository and git export format from already created task () - Basic page with jobs list, basic filtration to this list () - Added OpenCV.js TrackerMIL as tracking tool () - Ability to continue working from the latest frame where an annotator was before () diff --git a/cvat-ui/src/components/task-page/details.tsx b/cvat-ui/src/components/task-page/details.tsx index 322f3ca6bc50..a2d2c55e9f7d 100644 --- a/cvat-ui/src/components/task-page/details.tsx +++ b/cvat-ui/src/components/task-page/details.tsx @@ -11,17 +11,22 @@ import notification from 'antd/lib/notification'; import Text from 'antd/lib/typography/Text'; import Title from 'antd/lib/typography/Title'; import moment from 'moment'; - -import Descriptions from 'antd/lib/descriptions'; +import Paragraph from 'antd/lib/typography/Paragraph'; +import Select from 'antd/lib/select'; +import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox'; import getCore from 'cvat-core-wrapper'; -import { getReposData, syncRepos } from 'utils/git-utils'; +import { getReposData, syncRepos, changeRepo } from 'utils/git-utils'; import { ActiveInference } from 'reducers/interfaces'; import AutomaticAnnotationProgress from 'components/tasks-page/automatic-annotation-progress'; +import Descriptions from 'antd/lib/descriptions'; +import Space from 'antd/lib/space'; import UserSelector, { User } from './user-selector'; import BugTrackerEditor from './bug-tracker-editor'; import LabelsEditorComponent from '../labels-editor/labels-editor'; import ProjectSubsetField from '../create-task-page/project-subset-field'; +const { Option } = Select; + const core = getCore(); interface Props { @@ -32,6 +37,8 @@ interface Props { projectSubsets: string[]; cancelAutoAnnotation(): void; onTaskUpdate: (taskInstance: any) => void; + dumpers: any[]; + user: any; } interface State { @@ -40,13 +47,13 @@ interface State { repository: string; repositoryStatus: string; format: string; + lfs: boolean; + updatingRepository: boolean; } export default class DetailsComponent extends React.PureComponent { private mounted: boolean; - private previewImageElement: HTMLImageElement; - private previewWrapperRef: React.RefObject; constructor(props: Props) { @@ -63,6 +70,8 @@ export default class DetailsComponent extends React.PureComponent repository: '', format: '', repositoryStatus: '', + lfs: false, + updatingRepository: false, }; } @@ -103,6 +112,7 @@ export default class DetailsComponent extends React.PureComponent this.setState({ repository: data.url, format: data.format, + lfs: !!data.lfs, }); } }) @@ -130,6 +140,54 @@ export default class DetailsComponent extends React.PureComponent this.mounted = false; } + private onChangeRepoValue = (value: string): void => { + const { taskInstance } = this.props; + const { repository } = this.state; + const old = repository; + this.setState({ repository: value, updatingRepository: true }); + changeRepo(taskInstance.id, 'url', value) + .catch((error) => { + this.setState({ repository: old }); + notification.error({ + message: 'Could not update repository', + description: error, + }); + }) + .finally(() => this.setState({ updatingRepository: false })); + }; + + private onChangeLFSValue = (event: CheckboxChangeEvent): void => { + const { taskInstance } = this.props; + const { lfs } = this.state; + const old = lfs; + this.setState({ lfs: event.target.checked, updatingRepository: true }); + changeRepo(taskInstance.id, 'lfs', event.target.checked) + .catch((error) => { + this.setState({ lfs: old }); + notification.error({ + message: 'Could not update LFS', + description: error, + }); + }) + .finally(() => this.setState({ updatingRepository: false })); + }; + + private onChangeFormatValue = (value: string): void => { + const { taskInstance } = this.props; + const { format } = this.state; + const old = format; + this.setState({ format: value, updatingRepository: true }); + changeRepo(taskInstance.id, 'format', value) + .catch((error) => { + this.setState({ format: old }); + notification.error({ + message: 'Could not update format', + description: error, + }); + }) + .finally(() => this.setState({ updatingRepository: false })); + }; + private renderTaskName(): JSX.Element { const { name } = this.state; const { taskInstance, onTaskUpdate } = this.props; @@ -205,9 +263,10 @@ export default class DetailsComponent extends React.PureComponent } private renderDatasetRepository(): JSX.Element | boolean { - const { taskInstance } = this.props; - const { repository, repositoryStatus, format } = this.state; - + const { taskInstance, dumpers } = this.props; + const { + repository, repositoryStatus, format, lfs, updatingRepository, + } = this.state; return ( !!repository && ( @@ -215,71 +274,78 @@ export default class DetailsComponent extends React.PureComponent Dataset Repository -
- - {repository} - -
-

- Using format - {' '} - - {format} + + + {repository} -

- {repositoryStatus === 'sync' && ( - - - Synchronized - - )} - {repositoryStatus === 'merged' && ( - - - Merged - - )} - {repositoryStatus === 'syncing' && ( - - - Syncing - - )} - {repositoryStatus === '!sync' && ( - { - this.setState({ - repositoryStatus: 'syncing', - }); - - syncRepos(taskInstance.id) - .then((): void => { - if (this.mounted) { - this.setState({ - repositoryStatus: 'sync', - }); - } - }) - .catch((error): void => { - if (this.mounted) { - Modal.error({ - width: 800, - title: 'Could not synchronize the repository', - content: error.toString(), - }); - - this.setState({ - repositoryStatus: '!sync', - }); - } + {repositoryStatus === 'sync' && ( + + + Synchronized + + )} + {repositoryStatus === 'merged' && ( + + + Merged + + )} + {repositoryStatus === 'syncing' && ( + + + Syncing + + )} + {repositoryStatus === '!sync' && ( + { + this.setState({ + repositoryStatus: 'syncing', }); - }} - > - - Synchronize - - )} + + syncRepos(taskInstance.id) + .then((): void => { + if (this.mounted) { + this.setState({ + repositoryStatus: 'sync', + }); + } + }) + .catch((error): void => { + if (this.mounted) { + Modal.error({ + width: 800, + title: 'Could not synchronize the repository', + content: error.toString(), + }); + + this.setState({ + repositoryStatus: '!sync', + }); + } + }); + }} + > + + Synchronize + + )} + + Using format: + + + + Large file support + + {updatingRepository && } +
) diff --git a/cvat-ui/src/components/task-page/styles.scss b/cvat-ui/src/components/task-page/styles.scss index 335c0f19b511..c5296c3acce8 100644 --- a/cvat-ui/src/components/task-page/styles.scss +++ b/cvat-ui/src/components/task-page/styles.scss @@ -47,25 +47,35 @@ .cvat-dataset-repository-url { > a { - margin-right: 10px; + margin-right: $grid-unit-size; } - > .ant-tag-red { - user-select: none; - opacity: 0.4; + > .ant-typography { + > .ant-tag { + margin-left: $grid-unit-size; - &:hover { - opacity: 0.8; + > span[role='img'] { + margin-right: $grid-unit-size; + } } - &:active { - opacity: 1; + > .ant-tag-red { + user-select: none; + opacity: 0.4; + + &:hover { + opacity: 0.8; + } + + &:active { + opacity: 1; + } } } +} - > .ant-tag > span[role='img'] { - margin-right: 5px; - } +.cvat-repository-format-select { + width: 100%; } .cvat-task-job-list { diff --git a/cvat-ui/src/containers/task-page/details.tsx b/cvat-ui/src/containers/task-page/details.tsx index 4033e6e2b2c9..84d2d2fd8066 100644 --- a/cvat-ui/src/containers/task-page/details.tsx +++ b/cvat-ui/src/containers/task-page/details.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2021 Intel Corporation +// Copyright (C) 2019-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -18,6 +18,8 @@ interface StateToProps { activeInference: ActiveInference | null; installedGit: boolean; projectSubsets: string[]; + dumpers: any[]; + user: any; } interface DispatchToProps { @@ -30,6 +32,8 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps { const [taskProject] = state.projects.current.filter((project) => project.id === own.task.instance.projectId); return { + dumpers: state.formats.annotationFormats.dumpers, + user: state.auth.user, installedGit: list.GIT_INTEGRATION, activeInference: state.models.inferences[own.task.instance.id] || null, projectSubsets: taskProject ? @@ -53,11 +57,13 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps { function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element { const { - task, installedGit, activeInference, projectSubsets, cancelAutoAnnotation, onTaskUpdate, + task, installedGit, activeInference, projectSubsets, cancelAutoAnnotation, onTaskUpdate, dumpers, user, } = props; return ( { @@ -157,6 +158,7 @@ export async function getReposData(tid: number): Promise { error: response.status.error, }, format: response.format, + lfs: response.lfs, }; } @@ -193,3 +195,20 @@ export function syncRepos(tid: number): Promise { }); }); } + +export async function changeRepo(taskId: number, type: string, value: any): Promise { + return new Promise((resolve, reject): void => { + core.server + .request(`${baseURL}/git/repository/${taskId}`, { + method: 'PATCH', + data: JSON.stringify({ + type, + value, + }), + }) + .then(resolve) + .catch((error: any): void => { + reject(error); + }); + }); +} diff --git a/cvat/apps/dataset_repo/dataset_repo.py b/cvat/apps/dataset_repo/dataset_repo.py index 2b9bdb320cd1..2786532fbc57 100644 --- a/cvat/apps/dataset_repo/dataset_repo.py +++ b/cvat/apps/dataset_repo/dataset_repo.py @@ -223,10 +223,12 @@ def init_repos(self, wo_remote = False): if self._ssh_url() != self._rep.git.remote('get-url', '--all', 'origin'): slogger.task[self._tid].info("Local repository URL is obsolete.") # We need reinitialize repository if it's false - raise git.exc.GitError("Actual and saved repository URLs aren't match") + slogger.task[self._tid].info("Local repository initialization..") + shutil.rmtree(self._cwd, True) + self._clone() except git.exc.GitError: if wo_remote: - raise Exception('Local repository is failed') + slogger.task[self._tid].info("Local repository is failed") slogger.task[self._tid].info("Local repository initialization..") shutil.rmtree(self._cwd, True) self._clone() @@ -418,6 +420,7 @@ def get(tid, user): response["url"] = {"value": None} response["status"] = {"value": None, "error": None} response["format"] = {"format": None} + response["lfs"] = {"lfs": None} db_task = Task.objects.get(pk = tid) if GitData.objects.filter(pk = db_task).exists(): db_git = GitData.objects.select_for_update().get(pk = db_task) @@ -430,6 +433,7 @@ def get(tid, user): db_git.status = GitStatusChoice.SYNCING response['status']['value'] = str(db_git.status) response['format'] = str(db_git.format) + response["lfs"] = db_git.lfs else: try: _git = Git(db_git, db_task, user) @@ -437,6 +441,7 @@ def get(tid, user): db_git.status = _git.remote_status(db_task.updated_date) response['status']['value'] = str(db_git.status) response['format'] = str(db_git.format) + response["lfs"] = db_git.lfs except git.exc.GitCommandError as ex: _have_no_access_exception(ex) db_git.save() diff --git a/cvat/apps/dataset_repo/urls.py b/cvat/apps/dataset_repo/urls.py index 317ddf82ed56..9cb8a5bca6a1 100644 --- a/cvat/apps/dataset_repo/urls.py +++ b/cvat/apps/dataset_repo/urls.py @@ -13,4 +13,5 @@ path('push/', views.push_repository), path('check/', views.check_process), path('meta/get', views.get_meta_info), + path('', views.update_git_repo) ] diff --git a/cvat/apps/dataset_repo/views.py b/cvat/apps/dataset_repo/views.py index 5ac495e64f09..3438a5e16ecb 100644 --- a/cvat/apps/dataset_repo/views.py +++ b/cvat/apps/dataset_repo/views.py @@ -1,8 +1,9 @@ # Copyright (C) 2018-2021 Intel Corporation # # SPDX-License-Identifier: MIT +import http.client -from django.http import HttpResponseBadRequest, JsonResponse +from django.http import HttpResponseBadRequest, JsonResponse, HttpResponse from rules.contrib.views import permission_required, objectgetter from cvat.apps.iam.decorators import login_required @@ -85,6 +86,35 @@ def get_repository(request, tid): return HttpResponseBadRequest(str(ex)) +@login_required +@permission_required(perm=['engine.task.access'], + fn=objectgetter(models.Task, 'tid'), raise_exception=True) +def update_git_repo(request, tid): + try: + body = json.loads(request.body.decode('utf-8')) + req_type = body["type"] + value = body["value"] + git_data_obj = GitData.objects.filter(task_id=tid)[0] + if req_type == "url": + git_data_obj.url = value + git_data_obj.save(update_fields=["url"]) + elif req_type == "lfs": + git_data_obj.lfs = bool(value) + git_data_obj.save(update_fields=["lfs"]) + elif req_type == "format": + git_data_obj.format = value + git_data_obj.save(update_fields=["format"]) + slogger.task[tid].info("get repository request") + return HttpResponse( + status=http.HTTPStatus.OK, + ) + except Exception as ex: + try: + slogger.task[tid].error("error occurred during changing repository request", exc_info=True) + except Exception: + pass + return HttpResponseBadRequest(str(ex)) + @login_required def get_meta_info(request):