Git flexibility after task creation (#3886)

main
PMazarovich 4 years ago committed by GitHub
parent eac5b60d9b
commit cca258656c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for working with ellipses (<https://github.com/openvinotoolkit/cvat/pull/4062>) - Support for working with ellipses (<https://github.com/openvinotoolkit/cvat/pull/4062>)
- Add several flags to task creation CLI (<https://github.com/openvinotoolkit/cvat/pull/4119>) - Add several flags to task creation CLI (<https://github.com/openvinotoolkit/cvat/pull/4119>)
- Add YOLOv5 serverless function for automatic annotation (<https://github.com/openvinotoolkit/cvat/pull/4178>) - Add YOLOv5 serverless function for automatic annotation (<https://github.com/openvinotoolkit/cvat/pull/4178>)
- Add possibility to change git repository and git export format from already created task (<https://github.com/openvinotoolkit/cvat/pull/3886>)
- Basic page with jobs list, basic filtration to this list (<https://github.com/openvinotoolkit/cvat/pull/4258>) - Basic page with jobs list, basic filtration to this list (<https://github.com/openvinotoolkit/cvat/pull/4258>)
- Added OpenCV.js TrackerMIL as tracking tool (<https://github.com/openvinotoolkit/cvat/pull/4200>) - Added OpenCV.js TrackerMIL as tracking tool (<https://github.com/openvinotoolkit/cvat/pull/4200>)
- Ability to continue working from the latest frame where an annotator was before (<https://github.com/openvinotoolkit/cvat/pull/4297>) - Ability to continue working from the latest frame where an annotator was before (<https://github.com/openvinotoolkit/cvat/pull/4297>)

@ -11,17 +11,22 @@ import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import Title from 'antd/lib/typography/Title'; import Title from 'antd/lib/typography/Title';
import moment from 'moment'; import moment from 'moment';
import Paragraph from 'antd/lib/typography/Paragraph';
import Descriptions from 'antd/lib/descriptions'; import Select from 'antd/lib/select';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox';
import getCore from 'cvat-core-wrapper'; 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 { ActiveInference } from 'reducers/interfaces';
import AutomaticAnnotationProgress from 'components/tasks-page/automatic-annotation-progress'; 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 UserSelector, { User } from './user-selector';
import BugTrackerEditor from './bug-tracker-editor'; import BugTrackerEditor from './bug-tracker-editor';
import LabelsEditorComponent from '../labels-editor/labels-editor'; import LabelsEditorComponent from '../labels-editor/labels-editor';
import ProjectSubsetField from '../create-task-page/project-subset-field'; import ProjectSubsetField from '../create-task-page/project-subset-field';
const { Option } = Select;
const core = getCore(); const core = getCore();
interface Props { interface Props {
@ -32,6 +37,8 @@ interface Props {
projectSubsets: string[]; projectSubsets: string[];
cancelAutoAnnotation(): void; cancelAutoAnnotation(): void;
onTaskUpdate: (taskInstance: any) => void; onTaskUpdate: (taskInstance: any) => void;
dumpers: any[];
user: any;
} }
interface State { interface State {
@ -40,13 +47,13 @@ interface State {
repository: string; repository: string;
repositoryStatus: string; repositoryStatus: string;
format: string; format: string;
lfs: boolean;
updatingRepository: boolean;
} }
export default class DetailsComponent extends React.PureComponent<Props, State> { export default class DetailsComponent extends React.PureComponent<Props, State> {
private mounted: boolean; private mounted: boolean;
private previewImageElement: HTMLImageElement; private previewImageElement: HTMLImageElement;
private previewWrapperRef: React.RefObject<HTMLDivElement>; private previewWrapperRef: React.RefObject<HTMLDivElement>;
constructor(props: Props) { constructor(props: Props) {
@ -63,6 +70,8 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
repository: '', repository: '',
format: '', format: '',
repositoryStatus: '', repositoryStatus: '',
lfs: false,
updatingRepository: false,
}; };
} }
@ -103,6 +112,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
this.setState({ this.setState({
repository: data.url, repository: data.url,
format: data.format, format: data.format,
lfs: !!data.lfs,
}); });
} }
}) })
@ -130,6 +140,54 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
this.mounted = false; 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 { private renderTaskName(): JSX.Element {
const { name } = this.state; const { name } = this.state;
const { taskInstance, onTaskUpdate } = this.props; const { taskInstance, onTaskUpdate } = this.props;
@ -205,9 +263,10 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
} }
private renderDatasetRepository(): JSX.Element | boolean { private renderDatasetRepository(): JSX.Element | boolean {
const { taskInstance } = this.props; const { taskInstance, dumpers } = this.props;
const { repository, repositoryStatus, format } = this.state; const {
repository, repositoryStatus, format, lfs, updatingRepository,
} = this.state;
return ( return (
!!repository && ( !!repository && (
<Row> <Row>
@ -215,71 +274,78 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
<Text strong className='cvat-text-color'> <Text strong className='cvat-text-color'>
Dataset Repository Dataset Repository
</Text> </Text>
<br /> <Paragraph>
<a href={repository} rel='noopener noreferrer' target='_blank'> <Text editable={{ onChange: this.onChangeRepoValue }} disabled={updatingRepository}>
{repository} {repository}
</a>
<br />
<p>
Using format
{' '}
<Text strong>
{format}
</Text> </Text>
</p> {repositoryStatus === 'sync' && (
{repositoryStatus === 'sync' && ( <Tag color='blue'>
<Tag color='blue'> <CheckCircleOutlined />
<CheckCircleOutlined /> Synchronized
Synchronized </Tag>
</Tag> )}
)} {repositoryStatus === 'merged' && (
{repositoryStatus === 'merged' && ( <Tag color='green'>
<Tag color='green'> <CheckCircleOutlined />
<CheckCircleOutlined /> Merged
Merged </Tag>
</Tag> )}
)} {repositoryStatus === 'syncing' && (
{repositoryStatus === 'syncing' && ( <Tag color='purple'>
<Tag color='purple'> <LoadingOutlined />
<LoadingOutlined /> Syncing
Syncing </Tag>
</Tag> )}
)} {repositoryStatus === '!sync' && (
{repositoryStatus === '!sync' && ( <Tag
<Tag color='red'
color='red' onClick={(): void => {
onClick={(): void => { this.setState({
this.setState({ repositoryStatus: 'syncing',
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',
});
}
}); });
}}
> syncRepos(taskInstance.id)
<ExclamationCircleOutlined /> .then((): void => {
Synchronize if (this.mounted) {
</Tag> 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',
});
}
});
}}
>
<ExclamationCircleOutlined />
Synchronize
</Tag>
)}
</Paragraph>
<Text strong className='cvat-text-color'>Using format: </Text>
<Space>
<Select disabled={updatingRepository} onChange={this.onChangeFormatValue} className='cvat-repository-format-select' value={format}>
{
dumpers.map((dumper: any) => (
<Option key={dumper.name} value={dumper.name}>{dumper.name}</Option>
))
}
</Select>
<Checkbox disabled={updatingRepository} onChange={this.onChangeLFSValue} checked={lfs}>
Large file support
</Checkbox>
{updatingRepository && <LoadingOutlined style={{ fontSize: 14 }} spin />}
</Space>
</Col> </Col>
</Row> </Row>
) )

@ -47,25 +47,35 @@
.cvat-dataset-repository-url { .cvat-dataset-repository-url {
> a { > a {
margin-right: 10px; margin-right: $grid-unit-size;
} }
> .ant-tag-red { > .ant-typography {
user-select: none; > .ant-tag {
opacity: 0.4; margin-left: $grid-unit-size;
&:hover { > span[role='img'] {
opacity: 0.8; margin-right: $grid-unit-size;
}
} }
&:active { > .ant-tag-red {
opacity: 1; user-select: none;
opacity: 0.4;
&:hover {
opacity: 0.8;
}
&:active {
opacity: 1;
}
} }
} }
}
> .ant-tag > span[role='img'] { .cvat-repository-format-select {
margin-right: 5px; width: 100%;
}
} }
.cvat-task-job-list { .cvat-task-job-list {

@ -1,4 +1,4 @@
// Copyright (C) 2019-2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -18,6 +18,8 @@ interface StateToProps {
activeInference: ActiveInference | null; activeInference: ActiveInference | null;
installedGit: boolean; installedGit: boolean;
projectSubsets: string[]; projectSubsets: string[];
dumpers: any[];
user: any;
} }
interface DispatchToProps { 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); const [taskProject] = state.projects.current.filter((project) => project.id === own.task.instance.projectId);
return { return {
dumpers: state.formats.annotationFormats.dumpers,
user: state.auth.user,
installedGit: list.GIT_INTEGRATION, installedGit: list.GIT_INTEGRATION,
activeInference: state.models.inferences[own.task.instance.id] || null, activeInference: state.models.inferences[own.task.instance.id] || null,
projectSubsets: taskProject ? projectSubsets: taskProject ?
@ -53,11 +57,13 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element { function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const { const {
task, installedGit, activeInference, projectSubsets, cancelAutoAnnotation, onTaskUpdate, task, installedGit, activeInference, projectSubsets, cancelAutoAnnotation, onTaskUpdate, dumpers, user,
} = props; } = props;
return ( return (
<DetailsComponent <DetailsComponent
dumpers={dumpers}
user={user}
previewImage={task.preview} previewImage={task.preview}
taskInstance={task.instance} taskInstance={task.instance}
installedGit={installedGit} installedGit={installedGit}

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2019-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -39,6 +39,7 @@ interface ReposData {
error: string | null; error: string | null;
}; };
format: string format: string
lfs: boolean
} }
function waitForClone(cloneResponse: any): Promise<void> { function waitForClone(cloneResponse: any): Promise<void> {
@ -157,6 +158,7 @@ export async function getReposData(tid: number): Promise<ReposData | null> {
error: response.status.error, error: response.status.error,
}, },
format: response.format, format: response.format,
lfs: response.lfs,
}; };
} }
@ -193,3 +195,20 @@ export function syncRepos(tid: number): Promise<void> {
}); });
}); });
} }
export async function changeRepo(taskId: number, type: string, value: any): Promise<void> {
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);
});
});
}

@ -223,10 +223,12 @@ class Git:
if self._ssh_url() != self._rep.git.remote('get-url', '--all', 'origin'): if self._ssh_url() != self._rep.git.remote('get-url', '--all', 'origin'):
slogger.task[self._tid].info("Local repository URL is obsolete.") slogger.task[self._tid].info("Local repository URL is obsolete.")
# We need reinitialize repository if it's false # 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: except git.exc.GitError:
if wo_remote: 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..") slogger.task[self._tid].info("Local repository initialization..")
shutil.rmtree(self._cwd, True) shutil.rmtree(self._cwd, True)
self._clone() self._clone()
@ -418,6 +420,7 @@ def get(tid, user):
response["url"] = {"value": None} response["url"] = {"value": None}
response["status"] = {"value": None, "error": None} response["status"] = {"value": None, "error": None}
response["format"] = {"format": None} response["format"] = {"format": None}
response["lfs"] = {"lfs": None}
db_task = Task.objects.get(pk = tid) db_task = Task.objects.get(pk = tid)
if GitData.objects.filter(pk = db_task).exists(): if GitData.objects.filter(pk = db_task).exists():
db_git = GitData.objects.select_for_update().get(pk = db_task) db_git = GitData.objects.select_for_update().get(pk = db_task)
@ -430,6 +433,7 @@ def get(tid, user):
db_git.status = GitStatusChoice.SYNCING db_git.status = GitStatusChoice.SYNCING
response['status']['value'] = str(db_git.status) response['status']['value'] = str(db_git.status)
response['format'] = str(db_git.format) response['format'] = str(db_git.format)
response["lfs"] = db_git.lfs
else: else:
try: try:
_git = Git(db_git, db_task, user) _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) db_git.status = _git.remote_status(db_task.updated_date)
response['status']['value'] = str(db_git.status) response['status']['value'] = str(db_git.status)
response['format'] = str(db_git.format) response['format'] = str(db_git.format)
response["lfs"] = db_git.lfs
except git.exc.GitCommandError as ex: except git.exc.GitCommandError as ex:
_have_no_access_exception(ex) _have_no_access_exception(ex)
db_git.save() db_git.save()

@ -13,4 +13,5 @@ urlpatterns = [
path('push/<int:tid>', views.push_repository), path('push/<int:tid>', views.push_repository),
path('check/<str:rq_id>', views.check_process), path('check/<str:rq_id>', views.check_process),
path('meta/get', views.get_meta_info), path('meta/get', views.get_meta_info),
path('<int:tid>', views.update_git_repo)
] ]

@ -1,8 +1,9 @@
# Copyright (C) 2018-2021 Intel Corporation # Copyright (C) 2018-2021 Intel Corporation
# #
# SPDX-License-Identifier: MIT # 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 rules.contrib.views import permission_required, objectgetter
from cvat.apps.iam.decorators import login_required from cvat.apps.iam.decorators import login_required
@ -85,6 +86,35 @@ def get_repository(request, tid):
return HttpResponseBadRequest(str(ex)) 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 @login_required
def get_meta_info(request): def get_meta_info(request):

Loading…
Cancel
Save