зеркало из https://github.com/mozilla/treeherder.git
Bug 1630712 - Single try push view show Push Health Summary (#6314)
This commit is contained in:
Родитель
a965e7b62f
Коммит
96bf8370e1
|
@ -6,21 +6,16 @@ from cache_memoize import cache_memoize
|
|||
from rest_framework import viewsets
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import (HTTP_400_BAD_REQUEST,
|
||||
HTTP_404_NOT_FOUND)
|
||||
from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
|
||||
|
||||
from treeherder.model.models import (Job,
|
||||
JobType,
|
||||
Push,
|
||||
Repository)
|
||||
from treeherder.model.models import Job, JobType, Push, Repository
|
||||
from treeherder.push_health.builds import get_build_failures
|
||||
from treeherder.push_health.compare import get_commit_history
|
||||
from treeherder.push_health.linting import get_lint_failures
|
||||
from treeherder.push_health.tests import get_test_failures
|
||||
from treeherder.push_health.usage import get_usage
|
||||
from treeherder.webapp.api.serializers import PushSerializer
|
||||
from treeherder.webapp.api.utils import (to_datetime,
|
||||
to_timestamp)
|
||||
from treeherder.webapp.api.utils import to_datetime, to_timestamp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -44,7 +39,14 @@ class PushViewSet(viewsets.ViewSet):
|
|||
meta = {}
|
||||
|
||||
# support ranges for date as well as revisions(changes) like old tbpl
|
||||
for param in ["fromchange", "tochange", "startdate", "enddate", "revision", "commit_revision"]:
|
||||
for param in [
|
||||
"fromchange",
|
||||
"tochange",
|
||||
"startdate",
|
||||
"enddate",
|
||||
"revision",
|
||||
"commit_revision",
|
||||
]:
|
||||
v = filter_params.get(param, None)
|
||||
if v:
|
||||
del filter_params[param]
|
||||
|
@ -53,9 +55,9 @@ class PushViewSet(viewsets.ViewSet):
|
|||
try:
|
||||
repository = Repository.objects.get(name=project)
|
||||
except Repository.DoesNotExist:
|
||||
return Response({
|
||||
"detail": "No project with name {}".format(project)
|
||||
}, status=HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
{"detail": "No project with name {}".format(project)}, status=HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
pushes = Push.objects.filter(repository=repository).order_by('-time')
|
||||
|
||||
|
@ -63,43 +65,36 @@ class PushViewSet(viewsets.ViewSet):
|
|||
if param == 'fromchange':
|
||||
revision_field = 'revision__startswith' if len(value) < 40 else 'revision'
|
||||
filter_kwargs = {revision_field: value, 'repository': repository}
|
||||
frompush_time = Push.objects.values_list('time', flat=True).get(
|
||||
**filter_kwargs)
|
||||
frompush_time = Push.objects.values_list('time', flat=True).get(**filter_kwargs)
|
||||
pushes = pushes.filter(time__gte=frompush_time)
|
||||
filter_params.update({
|
||||
"push_timestamp__gte": to_timestamp(frompush_time)
|
||||
})
|
||||
filter_params.update({"push_timestamp__gte": to_timestamp(frompush_time)})
|
||||
self.report_if_short_revision(param, value)
|
||||
|
||||
elif param == 'tochange':
|
||||
revision_field = 'revision__startswith' if len(value) < 40 else 'revision'
|
||||
filter_kwargs = {revision_field: value, 'repository': repository}
|
||||
topush_time = Push.objects.values_list('time', flat=True).get(
|
||||
**filter_kwargs)
|
||||
topush_time = Push.objects.values_list('time', flat=True).get(**filter_kwargs)
|
||||
pushes = pushes.filter(time__lte=topush_time)
|
||||
filter_params.update({
|
||||
"push_timestamp__lte": to_timestamp(topush_time)
|
||||
})
|
||||
filter_params.update({"push_timestamp__lte": to_timestamp(topush_time)})
|
||||
self.report_if_short_revision(param, value)
|
||||
|
||||
elif param == 'startdate':
|
||||
pushes = pushes.filter(time__gte=to_datetime(value))
|
||||
filter_params.update({
|
||||
"push_timestamp__gte": to_timestamp(to_datetime(value))
|
||||
})
|
||||
filter_params.update({"push_timestamp__gte": to_timestamp(to_datetime(value))})
|
||||
elif param == 'enddate':
|
||||
real_end_date = to_datetime(value) + datetime.timedelta(days=1)
|
||||
pushes = pushes.filter(time__lte=real_end_date)
|
||||
filter_params.update({
|
||||
"push_timestamp__lt": to_timestamp(real_end_date)
|
||||
})
|
||||
filter_params.update({"push_timestamp__lt": to_timestamp(real_end_date)})
|
||||
elif param == 'revision':
|
||||
# revision must be the tip revision of the push itself
|
||||
revision_field = 'revision__startswith' if len(value) < 40 else 'revision'
|
||||
filter_kwargs = {revision_field: value}
|
||||
pushes = pushes.filter(**filter_kwargs)
|
||||
rev_key = "revisions_long_revision" \
|
||||
if len(meta['revision']) == 40 else "revisions_short_revision"
|
||||
rev_key = (
|
||||
"revisions_long_revision"
|
||||
if len(meta['revision']) == 40
|
||||
else "revisions_short_revision"
|
||||
)
|
||||
filter_params.update({rev_key: meta['revision']})
|
||||
self.report_if_short_revision(param, value)
|
||||
elif param == 'commit_revision':
|
||||
|
@ -108,30 +103,31 @@ class PushViewSet(viewsets.ViewSet):
|
|||
pushes = pushes.filter(commits__revision=value)
|
||||
self.report_if_short_revision(param, value)
|
||||
|
||||
for param in ['push_timestamp__lt', 'push_timestamp__lte',
|
||||
'push_timestamp__gt', 'push_timestamp__gte']:
|
||||
for param in [
|
||||
'push_timestamp__lt',
|
||||
'push_timestamp__lte',
|
||||
'push_timestamp__gt',
|
||||
'push_timestamp__gte',
|
||||
]:
|
||||
if filter_params.get(param):
|
||||
# translate push timestamp directly into a filter
|
||||
try:
|
||||
value = datetime.datetime.fromtimestamp(
|
||||
float(filter_params.get(param)))
|
||||
value = datetime.datetime.fromtimestamp(float(filter_params.get(param)))
|
||||
except ValueError:
|
||||
return Response({
|
||||
"detail": "Invalid timestamp specified for {}".format(
|
||||
param)
|
||||
}, status=HTTP_400_BAD_REQUEST)
|
||||
pushes = pushes.filter(**{
|
||||
param.replace('push_timestamp', 'time'): value
|
||||
})
|
||||
return Response(
|
||||
{"detail": "Invalid timestamp specified for {}".format(param)},
|
||||
status=HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
pushes = pushes.filter(**{param.replace('push_timestamp', 'time'): value})
|
||||
|
||||
for param in ['id__lt', 'id__lte', 'id__gt', 'id__gte', 'id']:
|
||||
try:
|
||||
value = int(filter_params.get(param, 0))
|
||||
except ValueError:
|
||||
return Response({
|
||||
"detail": "Invalid timestamp specified for {}".format(
|
||||
param)
|
||||
}, status=HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"detail": "Invalid timestamp specified for {}".format(param)},
|
||||
status=HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
if value:
|
||||
pushes = pushes.filter(**{param: value})
|
||||
|
||||
|
@ -140,8 +136,9 @@ class PushViewSet(viewsets.ViewSet):
|
|||
try:
|
||||
id_in_list = [int(id) for id in id_in.split(',')]
|
||||
except ValueError:
|
||||
return Response({"detail": "Invalid id__in specification"},
|
||||
status=HTTP_400_BAD_REQUEST)
|
||||
return Response(
|
||||
{"detail": "Invalid id__in specification"}, status=HTTP_400_BAD_REQUEST
|
||||
)
|
||||
pushes = pushes.filter(id__in=id_in_list)
|
||||
|
||||
author = filter_params.get("author")
|
||||
|
@ -151,8 +148,7 @@ class PushViewSet(viewsets.ViewSet):
|
|||
try:
|
||||
count = int(filter_params.get("count", 10))
|
||||
except ValueError:
|
||||
return Response({"detail": "Valid count value required"},
|
||||
status=HTTP_400_BAD_REQUEST)
|
||||
return Response({"detail": "Valid count value required"}, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
if count > MAX_PUSH_COUNT:
|
||||
msg = "Specified count exceeds api limit: {}".format(MAX_PUSH_COUNT)
|
||||
|
@ -170,10 +166,7 @@ class PushViewSet(viewsets.ViewSet):
|
|||
meta['repository'] = project
|
||||
meta['filter_params'] = filter_params
|
||||
|
||||
resp = {
|
||||
'meta': meta,
|
||||
'results': serializer.data
|
||||
}
|
||||
resp = {'meta': meta, 'results': serializer.data}
|
||||
|
||||
return Response(resp)
|
||||
|
||||
|
@ -182,13 +175,11 @@ class PushViewSet(viewsets.ViewSet):
|
|||
GET method implementation for detail view of ``push``
|
||||
"""
|
||||
try:
|
||||
push = Push.objects.get(repository__name=project,
|
||||
id=pk)
|
||||
push = Push.objects.get(repository__name=project, id=pk)
|
||||
serializer = PushSerializer(push)
|
||||
return Response(serializer.data)
|
||||
except Push.DoesNotExist:
|
||||
return Response("No push with id: {0}".format(pk),
|
||||
status=HTTP_404_NOT_FOUND)
|
||||
return Response("No push with id: {0}".format(pk), status=HTTP_404_NOT_FOUND)
|
||||
|
||||
@action(detail=True)
|
||||
def status(self, request, project, pk=None):
|
||||
|
@ -199,8 +190,7 @@ class PushViewSet(viewsets.ViewSet):
|
|||
try:
|
||||
push = Push.objects.get(id=pk)
|
||||
except Push.DoesNotExist:
|
||||
return Response("No push with id: {0}".format(pk),
|
||||
status=HTTP_404_NOT_FOUND)
|
||||
return Response("No push with id: {0}".format(pk), status=HTTP_404_NOT_FOUND)
|
||||
return Response(push.get_status())
|
||||
|
||||
@action(detail=False)
|
||||
|
@ -213,20 +203,26 @@ class PushViewSet(viewsets.ViewSet):
|
|||
try:
|
||||
push = Push.objects.get(revision=revision, repository__name=project)
|
||||
except Push.DoesNotExist:
|
||||
return Response("No push with revision: {0}".format(revision),
|
||||
status=HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
"No push with revision: {0}".format(revision), status=HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
push_health_test_failures = get_test_failures(push)
|
||||
push_health_lint_failures = get_lint_failures(push)
|
||||
push_health_build_failures = get_build_failures(push)
|
||||
test_failure_count = len(push_health_test_failures['needInvestigation'])
|
||||
build_failure_count = len(push_health_build_failures)
|
||||
lint_failure_count = len(push_health_lint_failures)
|
||||
|
||||
return Response({
|
||||
'needInvestigation':
|
||||
len(push_health_test_failures['needInvestigation']) +
|
||||
len(push_health_build_failures) +
|
||||
len(push_health_lint_failures),
|
||||
'unsupported': len(push_health_test_failures['unsupported']),
|
||||
})
|
||||
return Response(
|
||||
{
|
||||
'testFailureCount': test_failure_count,
|
||||
'buildFailureCount': build_failure_count,
|
||||
'lintFailureCount': lint_failure_count,
|
||||
'needInvestigation': test_failure_count + build_failure_count + lint_failure_count,
|
||||
'unsupported': len(push_health_test_failures['unsupported']),
|
||||
}
|
||||
)
|
||||
|
||||
@action(detail=False)
|
||||
def health_usage(self, request, project):
|
||||
|
@ -244,8 +240,9 @@ class PushViewSet(viewsets.ViewSet):
|
|||
repository = Repository.objects.get(name=project)
|
||||
push = Push.objects.get(revision=revision, repository=repository)
|
||||
except Push.DoesNotExist:
|
||||
return Response("No push with revision: {0}".format(revision),
|
||||
status=HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
"No push with revision: {0}".format(revision), status=HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
commit_history_details = None
|
||||
parent_push = None
|
||||
|
@ -277,53 +274,53 @@ class PushViewSet(viewsets.ViewSet):
|
|||
elif metric_result == 'fail':
|
||||
push_result = metric_result
|
||||
|
||||
newrelic.agent.record_custom_event('push_health_need_investigation', {
|
||||
'revision': revision,
|
||||
'repo': repository.name,
|
||||
'needInvestigation': len(push_health_test_failures['needInvestigation']),
|
||||
'unsupported': len(push_health_test_failures['unsupported']),
|
||||
'author': push.author,
|
||||
})
|
||||
|
||||
return Response({
|
||||
'revision': revision,
|
||||
'id': push.id,
|
||||
'result': push_result,
|
||||
'metrics': {
|
||||
'commitHistory': {
|
||||
'name': 'Commit History',
|
||||
'result': 'none',
|
||||
'details': commit_history_details,
|
||||
},
|
||||
'linting': {
|
||||
'name': 'Linting',
|
||||
'result': lint_result,
|
||||
'details': lint_failures,
|
||||
},
|
||||
'tests': {
|
||||
'name': 'Tests',
|
||||
'result': test_result,
|
||||
'details': push_health_test_failures,
|
||||
},
|
||||
'builds': {
|
||||
'name': 'Builds',
|
||||
'result': build_result,
|
||||
'details': build_failures,
|
||||
},
|
||||
newrelic.agent.record_custom_event(
|
||||
'push_health_need_investigation',
|
||||
{
|
||||
'revision': revision,
|
||||
'repo': repository.name,
|
||||
'needInvestigation': len(push_health_test_failures['needInvestigation']),
|
||||
'unsupported': len(push_health_test_failures['unsupported']),
|
||||
'author': push.author,
|
||||
},
|
||||
'status': push.get_status(),
|
||||
})
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
'revision': revision,
|
||||
'id': push.id,
|
||||
'result': push_result,
|
||||
'metrics': {
|
||||
'commitHistory': {
|
||||
'name': 'Commit History',
|
||||
'result': 'none',
|
||||
'details': commit_history_details,
|
||||
},
|
||||
'linting': {
|
||||
'name': 'Linting',
|
||||
'result': lint_result,
|
||||
'details': lint_failures,
|
||||
},
|
||||
'tests': {
|
||||
'name': 'Tests',
|
||||
'result': test_result,
|
||||
'details': push_health_test_failures,
|
||||
},
|
||||
'builds': {
|
||||
'name': 'Builds',
|
||||
'result': build_result,
|
||||
'details': build_failures,
|
||||
},
|
||||
},
|
||||
'status': push.get_status(),
|
||||
}
|
||||
)
|
||||
|
||||
@cache_memoize(60 * 60)
|
||||
def get_decision_jobs(self, push_ids):
|
||||
job_types = JobType.objects.filter(
|
||||
name__endswith='Decision Task',
|
||||
symbol='D'
|
||||
)
|
||||
job_types = JobType.objects.filter(name__endswith='Decision Task', symbol='D')
|
||||
return Job.objects.filter(
|
||||
push_id__in=push_ids,
|
||||
job_type__in=job_types,
|
||||
result='success',
|
||||
push_id__in=push_ids, job_type__in=job_types, result='success',
|
||||
).select_related('taskcluster_metadata')
|
||||
|
||||
@action(detail=False)
|
||||
|
@ -336,20 +333,24 @@ class PushViewSet(viewsets.ViewSet):
|
|||
|
||||
if decision_jobs:
|
||||
return Response(
|
||||
{job.push_id: {
|
||||
'id': job.taskcluster_metadata.task_id,
|
||||
'run': job.guid.split('/')[1],
|
||||
} for job in decision_jobs}
|
||||
{
|
||||
job.push_id: {
|
||||
'id': job.taskcluster_metadata.task_id,
|
||||
'run': job.guid.split('/')[1],
|
||||
}
|
||||
for job in decision_jobs
|
||||
}
|
||||
)
|
||||
logger.error('/decisiontask/ found no decision jobs for {}'.format(push_ids))
|
||||
self.get_decision_jobs.invalidate(push_ids)
|
||||
return Response("No decision tasks found for pushes: {}".format(push_ids),
|
||||
status=HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
"No decision tasks found for pushes: {}".format(push_ids), status=HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# TODO: Remove when we no longer support short revisions: Bug 1306707
|
||||
def report_if_short_revision(self, param, revision):
|
||||
if len(revision) < 40:
|
||||
newrelic.agent.record_custom_event(
|
||||
'short_revision_push_api',
|
||||
{'error': 'Revision <40 chars', 'param': param, 'revision': revision}
|
||||
{'error': 'Revision <40 chars', 'param': param, 'revision': revision},
|
||||
)
|
||||
|
|
|
@ -54,10 +54,6 @@ input {
|
|||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.mg-chart-title {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,10 @@
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
.small-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* this is better for drop down menu items because
|
||||
display: none will change text alignment */
|
||||
.hide {
|
||||
|
@ -376,6 +380,10 @@
|
|||
max-width: 890px;
|
||||
}
|
||||
|
||||
.row-height-tight {
|
||||
line-height: 15px !important;
|
||||
}
|
||||
|
||||
/* ReactTable overriding row, button and link color style for contrast pass */
|
||||
.ReactTable .-pagination .-btn:focus {
|
||||
box-shadow: 0 0 0 0.2rem rgba(130, 138, 145, 0.5);
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
} from '../../taskcluster-auth-callback/constants';
|
||||
import { RevisionList } from '../../shared/RevisionList';
|
||||
import { Revision } from '../../shared/Revision';
|
||||
import PushHealthSummary from '../../shared/PushHealthSummary';
|
||||
|
||||
import FuzzyJobFinder from './FuzzyJobFinder';
|
||||
import PushHeader from './PushHeader';
|
||||
|
@ -92,6 +93,8 @@ class Push extends React.PureComponent {
|
|||
jobCounts: { pending: 0, running: 0, completed: 0, fixedByCommit: 0 },
|
||||
pushGroupState: 'collapsed',
|
||||
collapsed: collapsedPushes.includes(push.id),
|
||||
singleTryPush: false,
|
||||
pushHealthStatus: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -107,12 +110,15 @@ class Push extends React.PureComponent {
|
|||
await this.fetchTestManifests();
|
||||
}
|
||||
|
||||
this.testForSingleTry();
|
||||
|
||||
window.addEventListener(thEvents.applyNewJobs, this.handleApplyNewJobs);
|
||||
window.addEventListener('hashchange', this.handleUrlChanges);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.showUpdateNotifications(prevState);
|
||||
this.testForSingleTry();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -170,6 +176,14 @@ class Push extends React.PureComponent {
|
|||
)}`;
|
||||
}
|
||||
|
||||
testForSingleTry = () => {
|
||||
const { currentRepo } = this.props;
|
||||
const revision = getUrlParam('revision');
|
||||
const singleTryPush = !!revision && currentRepo.name === 'try';
|
||||
|
||||
this.setState({ singleTryPush });
|
||||
};
|
||||
|
||||
handleUrlChanges = async () => {
|
||||
const { push } = this.props;
|
||||
const allParams = getAllUrlParams();
|
||||
|
@ -523,6 +537,10 @@ class Push extends React.PureComponent {
|
|||
}));
|
||||
};
|
||||
|
||||
pushHealthStatusCallback = pushHealthStatus => {
|
||||
this.setState({ pushHealthStatus });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
push,
|
||||
|
@ -548,6 +566,8 @@ class Push extends React.PureComponent {
|
|||
jobCounts,
|
||||
selectedRunnableJobs,
|
||||
collapsed,
|
||||
singleTryPush,
|
||||
pushHealthStatus,
|
||||
} = this.state;
|
||||
const {
|
||||
id,
|
||||
|
@ -606,6 +626,7 @@ class Push extends React.PureComponent {
|
|||
notificationSupported={notificationSupported}
|
||||
pushHealthVisibility={pushHealthVisibility}
|
||||
groupCountsExpanded={groupCountsExpanded}
|
||||
pushHealthStatusCallback={this.pushHealthStatusCallback}
|
||||
/>
|
||||
<div className="push-body-divider" />
|
||||
{!collapsed ? (
|
||||
|
@ -617,7 +638,17 @@ class Push extends React.PureComponent {
|
|||
revisionCount={revisionCount}
|
||||
repo={currentRepo}
|
||||
widthClass="col-5"
|
||||
/>
|
||||
>
|
||||
{singleTryPush && (
|
||||
<div className="ml-3 mt-4">
|
||||
<PushHealthSummary
|
||||
healthStatus={pushHealthStatus}
|
||||
revision={revision}
|
||||
repoName={currentRepo.name}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</RevisionList>
|
||||
)}
|
||||
<span className="job-list job-list-pad col-7">
|
||||
<PushJobs
|
||||
|
|
|
@ -274,6 +274,7 @@ class PushHeader extends React.Component {
|
|||
collapsed,
|
||||
pushHealthVisibility,
|
||||
currentRepo,
|
||||
pushHealthStatusCallback,
|
||||
} = this.props;
|
||||
const cancelJobsTitle = isLoggedIn
|
||||
? 'Cancel all jobs'
|
||||
|
@ -320,6 +321,7 @@ class PushHeader extends React.Component {
|
|||
repoName={currentRepo.name}
|
||||
revision={revision}
|
||||
jobCounts={jobCounts}
|
||||
statusCallback={pushHealthStatusCallback}
|
||||
/>
|
||||
)}
|
||||
<PushCounts
|
||||
|
@ -426,10 +428,12 @@ PushHeader.propTypes = {
|
|||
decisionTaskMap: PropTypes.object.isRequired,
|
||||
watchState: PropTypes.string,
|
||||
currentRepo: PropTypes.object.isRequired,
|
||||
pushHealthStatusCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
PushHeader.defaultProps = {
|
||||
watchState: 'none',
|
||||
pushHealthStatusCallback: null,
|
||||
};
|
||||
|
||||
const mapStateToProps = ({ pushes: { decisionTaskMap } }) => ({
|
||||
|
|
|
@ -42,13 +42,14 @@ class PushHealthStatus extends Component {
|
|||
}
|
||||
|
||||
async loadLatestStatus() {
|
||||
const { repoName, revision } = this.props;
|
||||
const { repoName, revision, statusCallback } = this.props;
|
||||
const { data, failureStatus } = await PushModel.getHealthSummary(
|
||||
repoName,
|
||||
revision,
|
||||
);
|
||||
|
||||
if (!failureStatus) {
|
||||
statusCallback(data);
|
||||
this.setState({ ...data });
|
||||
}
|
||||
}
|
||||
|
@ -123,6 +124,11 @@ PushHealthStatus.propTypes = {
|
|||
running: PropTypes.number.isRequired,
|
||||
completed: PropTypes.number.isRequired,
|
||||
}).isRequired,
|
||||
statusCallback: PropTypes.func,
|
||||
};
|
||||
|
||||
PushHealthStatus.defaultProps = {
|
||||
statusCallback: () => {},
|
||||
};
|
||||
|
||||
export default PushHealthStatus;
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Spinner, Table } from 'reactstrap';
|
||||
import { faHeart, faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
|
||||
import broken from '../img/push-health-broken.png';
|
||||
import ok from '../img/push-health-ok.png';
|
||||
import { getPushHealthUrl } from '../helpers/url';
|
||||
|
||||
class PushHealthSummary extends PureComponent {
|
||||
render() {
|
||||
const { healthStatus, revision, repoName } = this.props;
|
||||
const status = healthStatus || {};
|
||||
const {
|
||||
needInvestigation,
|
||||
testFailureCount,
|
||||
buildFailureCount,
|
||||
lintFailureCount,
|
||||
unsupported,
|
||||
} = status;
|
||||
const heartSize = 25;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<a
|
||||
href={getPushHealthUrl({ revision, repo: repoName })}
|
||||
title="View Push Health details for this push"
|
||||
>
|
||||
<div>
|
||||
{healthStatus !== null ? (
|
||||
<img
|
||||
src={needInvestigation ? broken : ok}
|
||||
alt={needInvestigation ? 'Broken' : 'OK'}
|
||||
width={heartSize}
|
||||
height={heartSize}
|
||||
className="mr-1"
|
||||
/>
|
||||
) : (
|
||||
<span className="ml-1 text-darker-secondary">
|
||||
<FontAwesomeIcon
|
||||
icon={faHeart}
|
||||
height={heartSize}
|
||||
width={heartSize}
|
||||
color="darker-secondary"
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
Push Health Summary
|
||||
<FontAwesomeIcon
|
||||
icon={faExternalLinkAlt}
|
||||
className="ml-1 icon-superscript"
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
{healthStatus ? (
|
||||
<Table className="ml-3 w-100 small-text row-height-tight">
|
||||
<tbody>
|
||||
<tr className={`${buildFailureCount ? 'font-weight-bold' : ''}`}>
|
||||
<td className="py-1">Build Failures</td>
|
||||
<td className="py-1">{buildFailureCount}</td>
|
||||
</tr>
|
||||
<tr
|
||||
className={`${testFailureCount ? 'font-weight-bold' : ''} py-1`}
|
||||
>
|
||||
<td className="py-1">Test Failures</td>
|
||||
<td className="py-1">{testFailureCount}</td>
|
||||
</tr>
|
||||
<tr
|
||||
className={`${lintFailureCount ? 'font-weight-bold' : ''} py-1`}
|
||||
>
|
||||
<td className="py-1">Linting Failures</td>
|
||||
<td className="py-1">{lintFailureCount}</td>
|
||||
</tr>
|
||||
<tr className={`${unsupported ? 'font-weight-bold' : ''} py-1`}>
|
||||
<td className="py-1">Unsupported</td>
|
||||
<td className="py-1">{unsupported}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</Table>
|
||||
) : (
|
||||
<Spinner />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PushHealthSummary.propTypes = {
|
||||
revision: PropTypes.string.isRequired,
|
||||
repoName: PropTypes.string.isRequired,
|
||||
healthStatus: PropTypes.shape({
|
||||
needInvestigation: PropTypes.number,
|
||||
testFailureCount: PropTypes.number,
|
||||
buildFailureCount: PropTypes.number,
|
||||
lintFailureCount: PropTypes.number,
|
||||
unsupported: PropTypes.number,
|
||||
}),
|
||||
};
|
||||
|
||||
PushHealthSummary.defaultProps = {
|
||||
healthStatus: {},
|
||||
};
|
||||
|
||||
export default PushHealthSummary;
|
|
@ -8,7 +8,14 @@ import { Revision } from './Revision';
|
|||
|
||||
export class RevisionList extends React.PureComponent {
|
||||
render() {
|
||||
const { revision, revisions, revisionCount, repo, widthClass } = this.props;
|
||||
const {
|
||||
revision,
|
||||
revisions,
|
||||
revisionCount,
|
||||
repo,
|
||||
widthClass,
|
||||
children,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<Col className={`${widthClass} mb-3`}>
|
||||
|
@ -18,6 +25,7 @@ export class RevisionList extends React.PureComponent {
|
|||
{revisionCount > revisions.length && (
|
||||
<MoreRevisionsLink key="more" href={repo.getPushLogHref(revision)} />
|
||||
)}
|
||||
{children}
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче