From 477c5df022603070b12a62a7beb0f0fed55a6158 Mon Sep 17 00:00:00 2001 From: Jake McDermott Date: Wed, 23 Jan 2019 15:58:46 -0500 Subject: [PATCH] add linked status indicator for scm inventory project updates Signed-off-by: Jake McDermott --- awx/api/serializers.py | 38 +++++++++++++++++++ awx/api/views/__init__.py | 6 +-- awx/main/tests/unit/api/test_views.py | 2 +- .../features/output/details.component.js | 37 +++++++++++++++++- .../features/output/details.partial.html | 21 ++++++++++ .../client/features/output/output.strings.js | 3 ++ .../client/features/output/status.service.js | 17 +++++++++ 7 files changed, 119 insertions(+), 5 deletions(-) diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 829b0fd2f1..2c400943ee 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -2209,6 +2209,44 @@ class InventoryUpdateSerializer(UnifiedJobSerializer, InventorySourceOptionsSeri return res +class InventoryUpdateDetailSerializer(InventoryUpdateSerializer): + + source_project = serializers.SerializerMethodField( + help_text=_('The project used for this job.'), + method_name='get_source_project_id' + ) + + class Meta: + model = InventoryUpdate + fields = ('*', 'source_project',) + + def get_source_project(self, obj): + return getattrd(obj, 'source_project_update.unified_job_template', None) + + def get_source_project_id(self, obj): + return getattrd(obj, 'source_project_update.unified_job_template.id', None) + + def get_related(self, obj): + res = super(InventoryUpdateDetailSerializer, self).get_related(obj) + source_project_id = self.get_source_project_id(obj) + + if source_project_id: + res['source_project'] = self.reverse('api:project_detail', kwargs={'pk': source_project_id}) + return res + + def get_summary_fields(self, obj): + summary_fields = super(InventoryUpdateDetailSerializer, self).get_summary_fields(obj) + summary_obj = self.get_source_project(obj) + + if summary_obj: + summary_fields['source_project'] = {} + for field in SUMMARIZABLE_FK_FIELDS['project']: + value = getattr(summary_obj, field, None) + if value is not None: + summary_fields['source_project'][field] = value + return summary_fields + + class InventoryUpdateListSerializer(InventoryUpdateSerializer, UnifiedJobListSerializer): class Meta: diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index 2a3e85007a..8b1bd4f97d 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -1972,7 +1972,7 @@ class InventoryInventorySourcesUpdate(RetrieveAPIView): details['status'] = None if inventory_source.can_update: update = inventory_source.update() - details.update(InventoryUpdateSerializer(update, context=self.get_serializer_context()).to_representation(update)) + details.update(InventoryUpdateDetailSerializer(update, context=self.get_serializer_context()).to_representation(update)) details['status'] = 'started' details['inventory_update'] = update.id successes += 1 @@ -2135,7 +2135,7 @@ class InventorySourceUpdateView(RetrieveAPIView): headers = {'Location': update.get_absolute_url(request=request)} data = OrderedDict() data['inventory_update'] = update.id - data.update(InventoryUpdateSerializer(update, context=self.get_serializer_context()).to_representation(update)) + data.update(InventoryUpdateDetailSerializer(update, context=self.get_serializer_context()).to_representation(update)) return Response(data, status=status.HTTP_202_ACCEPTED, headers=headers) else: return self.http_method_not_allowed(request, *args, **kwargs) @@ -2150,7 +2150,7 @@ class InventoryUpdateList(ListAPIView): class InventoryUpdateDetail(UnifiedJobDeletionMixin, RetrieveDestroyAPIView): model = InventoryUpdate - serializer_class = InventoryUpdateSerializer + serializer_class = InventoryUpdateDetailSerializer class InventoryUpdateCredentialsList(SubListAPIView): diff --git a/awx/main/tests/unit/api/test_views.py b/awx/main/tests/unit/api/test_views.py index 369dbc6581..e2f1f5150b 100644 --- a/awx/main/tests/unit/api/test_views.py +++ b/awx/main/tests/unit/api/test_views.py @@ -113,7 +113,7 @@ class TestInventoryInventorySourcesUpdate: with mocker.patch.object(InventoryInventorySourcesUpdate, 'get_object', return_value=obj): with mocker.patch.object(InventoryInventorySourcesUpdate, 'get_serializer_context', return_value=None): - with mocker.patch('awx.api.views.InventoryUpdateSerializer') as serializer_class: + with mocker.patch('awx.api.views.InventoryUpdateDetailSerializer') as serializer_class: serializer = serializer_class.return_value serializer.to_representation.return_value = {} diff --git a/awx/ui/client/features/output/details.component.js b/awx/ui/client/features/output/details.component.js index 60b3130a98..3cece3143a 100644 --- a/awx/ui/client/features/output/details.component.js +++ b/awx/ui/client/features/output/details.component.js @@ -187,6 +187,12 @@ function getInventorySourceDetails () { return null; } + if (resource.model.get('summary_fields.inventory_source.source') === 'scm') { + // we already show a SOURCE PROJECT item for scm inventory updates, so we + // skip the display of the standard source details item. + return null; + } + const { source } = resource.model.get('summary_fields.inventory_source'); const choices = mapChoices(resource.model.options('actions.GET.source.choices')); @@ -324,6 +330,33 @@ function getProjectUpdateDetails (updateId) { return { link, tooltip }; } +function getInventoryScmDetails (updateId, updateStatus) { + const projectId = resource.model.get('summary_fields.source_project.id'); + const projectName = resource.model.get('summary_fields.source_project.name'); + const jobId = updateId || resource.model.get('source_project_update'); + const status = updateStatus || resource.model.get('summary_fields.inventory_source.status'); + + if (!projectId || !projectName) { + return null; + } + + const label = strings.get('labels.INVENTORY_SCM'); + const jobLink = `/#/jobs/project/${jobId}`; + const link = `/#/projects/${projectId}`; + const jobTooltip = strings.get('tooltips.INVENTORY_SCM_JOB'); + const tooltip = strings.get('tooltips.INVENTORY_SCM'); + + let icon; + + if (status === 'unknown') { + icon = 'fa icon-job-pending'; + } else { + icon = `fa icon-job-${status}`; + } + + return { label, link, icon, jobLink, jobTooltip, tooltip, value: projectName }; +} + function getSCMRevisionDetails () { const label = strings.get('labels.SCM_REVISION'); const value = resource.model.get('scm_revision'); @@ -718,6 +751,7 @@ function JobDetailsController ( vm.projectUpdate = getProjectUpdateDetails(); vm.projectStatus = getProjectStatusDetails(); vm.scmRevision = getSCMRevisionDetails(); + vm.inventoryScm = getInventoryScmDetails(); vm.playbook = getPlaybookDetails(); vm.resultTraceback = getResultTracebackDetails(); vm.launchedBy = getLaunchedByDetails(); @@ -748,12 +782,13 @@ function JobDetailsController ( vm.toggleLabels = toggleLabels; vm.showLabels = showLabels; - unsubscribe = subscribe(({ status, started, finished, scm, environment }) => { + unsubscribe = subscribe(({ status, started, finished, scm, inventoryScm, environment }) => { vm.started = getStartDetails(started); vm.finished = getFinishDetails(finished); vm.projectUpdate = getProjectUpdateDetails(scm.id); vm.projectStatus = getProjectStatusDetails(scm.status); vm.environment = getEnvironmentDetails(environment); + vm.inventoryScm = getInventoryScmDetails(inventoryScm.id, inventoryScm.status); vm.status = getStatusDetails(status); vm.job.status = status; }); diff --git a/awx/ui/client/features/output/details.partial.html b/awx/ui/client/features/output/details.partial.html index c56772dbee..dc979923dd 100644 --- a/awx/ui/client/features/output/details.partial.html +++ b/awx/ui/client/features/output/details.partial.html @@ -198,6 +198,27 @@ + +
+ + +
+
diff --git a/awx/ui/client/features/output/output.strings.js b/awx/ui/client/features/output/output.strings.js index c1684649c1..8d6042c934 100644 --- a/awx/ui/client/features/output/output.strings.js +++ b/awx/ui/client/features/output/output.strings.js @@ -22,6 +22,8 @@ function OutputStrings (BaseString) { EXPAND_OUTPUT: t.s('Expand Output'), EXTRA_VARS: t.s('Read-only view of extra variables added to the job template'), INVENTORY: t.s('View the Inventory'), + INVENTORY_SCM: t.s('View the Project'), + INVENTORY_SCM_JOB: t.s('View Project checkout results'), JOB_TEMPLATE: t.s('View the Job Template'), SLICE_JOB_DETAILS: t.s('Job is one of several from a JT that slices on inventory'), PROJECT: t.s('View the Project'), @@ -54,6 +56,7 @@ function OutputStrings (BaseString) { FORKS: t.s('Forks'), INSTANCE_GROUP: t.s('Instance Group'), INVENTORY: t.s('Inventory'), + INVENTORY_SCM: t.s('Source Project'), JOB_EXPLANATION: t.s('Explanation'), JOB_TAGS: t.s('Job Tags'), JOB_TEMPLATE: t.s('Job Template'), diff --git a/awx/ui/client/features/output/status.service.js b/awx/ui/client/features/output/status.service.js index 30331f82b6..5dbef7d821 100644 --- a/awx/ui/client/features/output/status.service.js +++ b/awx/ui/client/features/output/status.service.js @@ -44,6 +44,10 @@ function JobStatusService (moment, message) { id: model.get('summary_fields.project_update.id'), status: model.get('summary_fields.project_update.status') }, + inventoryScm: { + id: model.get('source_project_update'), + status: model.get('summary_fields.inventory_source.status') + } }; this.initHostStatusCounts({ model }); @@ -86,6 +90,7 @@ function JobStatusService (moment, message) { this.pushStatusEvent = data => { const isJobStatusEvent = (this.job === data.unified_job_id); const isProjectStatusEvent = (this.project && (this.project === data.project_id)); + const isInventoryScmStatus = (this.model.get('source_project_update') === data.unified_job_id); if (isJobStatusEvent) { this.setJobStatus(data.status); @@ -94,6 +99,10 @@ function JobStatusService (moment, message) { this.setProjectStatus(data.status); this.setProjectUpdateId(data.unified_job_id); this.dispatch(); + } else if (isInventoryScmStatus) { + this.setInventoryScmStatus(data.status); + this.setInventoryScmId(data.unified_job_id); + this.dispatch(); } }; @@ -249,6 +258,14 @@ function JobStatusService (moment, message) { this.state.scm.id = id; }; + this.setInventoryScmStatus = status => { + this.state.inventoryScm.status = status; + }; + + this.setInventoryScmId = id => { + this.state.inventoryScm.id = id; + }; + this.setFinished = time => { if (!time) return;