Skip to content
Snippets Groups Projects
Verified Commit 47f6ef43 authored by Phil Hughes's avatar Phil Hughes
Browse files

Added reports widgets to reports tab

Updates the current reports widget component to allow it to be used in the reports
tab content section.

#512718
parent 43296bf9
No related branches found
No related tags found
3 merge requests!181325Fix ambiguous `created_at` in project.rb,!180187Draft: Update dashboard editing to save visualizations directly to the dashboard file,!178692Added reports widgets to reports tab
Showing
with 176 additions and 95 deletions
......@@ -443,7 +443,11 @@ export default class MergeRequestTabs {
let newStatePathname = pathname.replace(this.actionRegex, '');
// Append the new action if we're on a tab other than 'notes'
if (this.currentAction !== 'show' && this.currentAction !== 'new') {
if (
this.currentAction !== 'show' &&
this.currentAction !== 'new' &&
this.currentAction !== 'reports'
) {
newStatePathname += `/${this.currentAction}`;
}
......
<script>
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
import {
BLOCKERS_ROUTE,
CODE_QUALITY_ROUTE,
......@@ -23,9 +25,21 @@ export default {
inject: ['hasPolicies'],
data() {
return {
blockersCounter: 2,
mr: null,
};
},
created() {
if (
window.gl?.mrWidgetData?.merge_request_cached_widget_path &&
window.gl?.mrWidgetData?.merge_request_widget_path
) {
MRWidgetService.fetchInitialData()
.then(({ data }) => {
this.mr = new MRWidgetStore({ ...window.gl.mrWidgetData, ...data });
})
.catch(() => {});
}
},
};
</script>
......@@ -50,7 +64,7 @@ export default {
</nav>
</aside>
<section class="md:gl-pt-5">
<router-view />
<router-view :mr="mr" />
</section>
</div>
</template>
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import ReportWidgetContainer from 'ee_else_ce/vue_merge_request_widget/components/widget/app.vue';
export default {
name: 'MergeRequestReportsIndexPage',
components: {
GlLoadingIcon,
ReportWidgetContainer,
},
props: {
mr: {
type: Object,
required: false,
default: null,
},
},
};
</script>
<template>
<div></div>
<div v-if="mr">
<report-widget-container :mr="mr" reports-tab-content />
</div>
<gl-loading-icon v-else size="lg" />
</template>
import BlockersPage from 'ee_else_ce/merge_requests/reports/pages/blockers_page.vue';
import IndexComponent from './pages/index.vue';
import {
BLOCKERS_ROUTE,
CODE_QUALITY_ROUTE,
LICENSE_COMPLIANCE_ROUTE,
SECURITY_ROUTE,
} from './constants';
import { BLOCKERS_ROUTE } from './constants';
export default [
{
......@@ -14,18 +9,8 @@ export default [
component: BlockersPage,
},
{
path: '/?type=code-quality',
name: CODE_QUALITY_ROUTE,
component: IndexComponent,
},
{
path: '/?type=security',
name: SECURITY_ROUTE,
component: IndexComponent,
},
{
path: '/?type=license-compliance',
name: LICENSE_COMPLIANCE_ROUTE,
name: 'report',
path: '/:report',
component: IndexComponent,
},
];
......@@ -18,33 +18,51 @@ export default {
import('~/vue_merge_request_widget/widgets/accessibility/index.vue'),
},
mixins: [glFeatureFlagsMixin()],
provide() {
return {
reportsTabContent: this.reportsTabContent,
};
},
props: {
mr: {
type: Object,
required: true,
},
reportsTabContent: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
collapsed: this.glFeatures.mrReportsTab,
collapsed: this.reportsTabContent ? false : this.glFeatures.mrReportsTab,
findingsCount: 0,
loadedCount: 0,
};
},
computed: {
testReportWidget() {
if (!this.isViewingReport('test-summary')) return undefined;
return this.mr.testResultsPath && 'MrTestReportWidget';
},
terraformPlansWidget() {
if (!this.isViewingReport('terraform')) return undefined;
return this.mr.terraformReportsPath && 'MrTerraformWidget';
},
codeQualityWidget() {
if (!this.isViewingReport('code-quality')) return undefined;
return this.mr.codequalityReportsPath ? 'MrCodeQualityWidget' : undefined;
},
accessibilityWidget() {
if (!this.isViewingReport('accessibility')) return undefined;
return this.mr.accessibilityReportPath ? 'MrAccessibilityWidget' : undefined;
},
......@@ -69,7 +87,17 @@ export default {
return false;
},
},
mounted() {
if (this.reportsTabContent && !this.widgets.length) {
this.$router.push({ path: '/' });
}
},
methods: {
isViewingReport(reportName) {
if (!this.reportsTabContent) return true;
return this.$router.currentRoute.params.report === reportName;
},
onLoadedReport(findings) {
this.findingsCount += findings;
this.loadedCount += 1;
......@@ -82,12 +110,14 @@ export default {
<section
v-if="widgets.length"
role="region"
:aria-label="__('Merge request reports')"
:aria-label="reportsTabContent ? null : __('Merge request reports')"
data-testid="mr-widget-app"
class="mr-section-container"
:class="{
'mr-section-container': !reportsTabContent,
}"
>
<state-container
v-if="glFeatures.mrReportsTab"
v-if="glFeatures.mrReportsTab && !reportsTabContent"
:status="statusIcon"
is-collapsible
collapse-on-desktop
......@@ -123,7 +153,8 @@ export default {
data-testid="reports-widgets-container"
class="reports-widgets-container"
:class="{
'gl-border-t gl-relative gl-border-t-section gl-bg-subtle': glFeatures.mrReportsTab,
'gl-border-t gl-relative gl-border-t-section gl-bg-subtle':
glFeatures.mrReportsTab && !reportsTabContent,
}"
>
<component
......@@ -132,7 +163,9 @@ export default {
:key="widget.name || index"
:mr="mr"
class="mr-widget-section"
:class="{ 'gl-border-t gl-border-t-section': index > 0 }"
:class="{
'gl-border-t gl-border-t-section': index > 0 && !reportsTabContent,
}"
@loaded="onLoadedReport"
/>
</div>
......
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import { kebabCase } from 'lodash';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { logError } from '~/lib/logger';
......@@ -8,7 +9,7 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { joinPaths } from '~/lib/utils/url_utility';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { EXTENSION_ICONS } from '../../constants';
......@@ -50,6 +51,7 @@ export default {
SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
inject: { reportsTabContent: { default: false } },
props: {
loadingText: {
type: String,
......@@ -175,7 +177,7 @@ export default {
data() {
return {
isExpandedForTheFirstTime: true,
isCollapsed: true,
isCollapsed: !this.reportsTabContent,
isLoadingCollapsedContent: true,
isLoadingExpandedContent: false,
summaryError: null,
......@@ -212,9 +214,9 @@ export default {
return [
{
text: __('View report'),
href: mergeUrlParams(
{ type: this.widgetName.replace(WIDGET_PREFIX, '') },
href: joinPaths(
window.gl?.mrWidgetData?.reportsTabPath || '',
kebabCase(this.widgetName.replace(WIDGET_PREFIX, '')),
),
onClick(action, e) {
e.preventDefault();
......@@ -254,6 +256,10 @@ export default {
this.summaryError = this.errorText;
}
if (this.reportsTabContent) {
this.fetchExpandedContent();
}
this.isLoadingCollapsedContent = false;
},
methods: {
......@@ -341,7 +347,13 @@ export default {
<template>
<section class="media-section" data-testid="widget-extension">
<div class="gl-flex gl-px-5 gl-py-4 gl-pr-4" :class="{ 'gl-pl-9': glFeatures.mrReportsTab }">
<div
v-if="!reportsTabContent"
:class="{
'gl-pl-9': glFeatures.mrReportsTab,
'gl-flex gl-px-5 gl-py-4 gl-pr-4': !reportsTabContent,
}"
>
<status-icon
:level="glFeatures.mrReportsTab ? 2 : 1"
:name="widgetName"
......@@ -415,11 +427,15 @@ export default {
</div>
</div>
<div
v-if="!glFeatures.mrReportsTab && (!isCollapsed || contentError)"
class="gl-border-t gl-relative gl-border-t-section gl-bg-subtle"
v-if="!isCollapsed || contentError"
:class="{ 'gl-border-t gl-relative gl-border-t-section gl-bg-subtle': !reportsTabContent }"
data-testid="widget-extension-collapsed-section"
>
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
<div
v-if="isLoadingExpandedContent"
class="gl-text-center"
:class="{ 'report-block-container': !reportsTabContent, 'gl-py-5': reportsTabContent }"
>
<gl-loading-icon size="sm" inline /> {{ loadingText }}
</div>
<div v-else class="gl-flex gl-pl-5" :class="{ 'gl-pr-5': $scopedSlots.content }">
......@@ -439,7 +455,8 @@ export default {
v-if="contentWithKeyField"
:items="contentWithKeyField"
:min-item-size="32"
:style="{ maxHeight: '170px' }"
:style="{ maxHeight: reportsTabContent ? null : '170px' }"
:page-mode="glFeatures.mrReportsTab && reportsTabContent"
data-testid="dynamic-content-scroller"
class="gl-pr-5"
>
......
......@@ -44,29 +44,39 @@ export default {
},
computed: {
licenseComplianceWidget() {
if (!this.isViewingReport('license-compliance')) return undefined;
return this.mr?.enabledReports?.licenseScanning ? 'MrLicenseComplianceWidget' : undefined;
},
browserPerformanceWidget() {
if (!this.isViewingReport('browser-performance')) return undefined;
return this.mr.browserPerformance ? 'MrBrowserPerformanceWidget' : undefined;
},
loadPerformanceWidget() {
if (!this.isViewingReport('load-performance')) return undefined;
return this.mr.loadPerformance ? 'MrLoadPerformanceWidget' : undefined;
},
metricsWidget() {
if (!this.isViewingReport('metrics')) return undefined;
return this.mr.metricsReportsPath ? 'MrMetricsWidget' : undefined;
},
statusChecksWidget() {
if (!this.isViewingReport('status-checks')) return undefined;
return this.mr.apiStatusChecksPath && !this.mr.isNothingToMergeState
? 'MrStatusChecksWidget'
: undefined;
},
securityReportsWidget() {
if (this.glFeatures.mrReportsTab && !this.mr.pipelineIid) return undefined;
if (!this.isViewingReport('security-reports')) return undefined;
return this.mr.canReadVulnerabilities ? 'MrSecurityWidgetEE' : 'MrSecurityWidgetCE';
},
......
......@@ -13,10 +13,13 @@ module MergeRequestsController
push_frontend_feature_flag(:merge_trains_skip_train, @project)
push_frontend_feature_flag(:resolve_vulnerability_in_mr, @project)
push_frontend_ability(ability: :resolve_vulnerability_with_ai, resource: @project, user: current_user)
push_frontend_feature_flag(:mr_reports_tab, @project)
push_frontend_ability(ability: :measure_comment_temperature, resource: merge_request, user: current_user)
end
before_action do
push_frontend_feature_flag(:mr_reports_tab, @project)
end
before_action :authorize_read_pipeline!, only: [:metrics_reports]
before_action :authorize_read_security_resource!, only: [
:container_scanning_reports, :dependency_scanning_reports,
......
= render_ce "projects/merge_requests/page"
= javascript_tag do
:plain
// Append static, server-generated data not included in merge request entity (EE-Only)
// Object.assign would be useful here, but it blows up Phantom.js in tests
window.gl.mrWidgetData.is_geo_secondary_node = '#{Gitlab::Geo.secondary?}' === 'true';
window.gl.mrWidgetData.geo_secondary_help_path = '#{help_page_path("administration/geo/replication/configuration.md")}';
window.gl.mrWidgetData.sast_help_path = '#{help_page_path("user/application_security/sast/index.md")}';
window.gl.mrWidgetData.secret_detection_help_path = '#{help_page_path("user/application_security/secret_detection/index.md")}';
window.gl.mrWidgetData.container_scanning_help_path = '#{help_page_path("user/application_security/container_scanning/index.md")}';
window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/application_security/dast/index.md")}';
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index.md")}';
window.gl.mrWidgetData.api_fuzzing_help_path = '#{help_page_path("user/application_security/api_fuzzing/index.md")}';
window.gl.mrWidgetData.coverage_fuzzing_help_path = '#{help_page_path("user/application_security/coverage_fuzzing/index.md")}';
window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}'
window.gl.mrWidgetData.license_scanning_comparison_collapsed_path = '#{license_scanning_reports_collapsed_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}'
window.gl.mrWidgetData.container_scanning_comparison_path = '#{container_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:container_scanning)}'
window.gl.mrWidgetData.dependency_scanning_comparison_path = '#{dependency_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:dependency_scanning)}'
window.gl.mrWidgetData.sast_comparison_path = '#{sast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:sast)}'
window.gl.mrWidgetData.dast_comparison_path = '#{dast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:dast)}'
window.gl.mrWidgetData.secret_detection_comparison_path = '#{secret_detection_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:secret_detection)}'
window.gl.mrWidgetData.coverage_fuzzing_comparison_path = '#{coverage_fuzzing_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:coverage_fuzzing)}'
window.gl.mrWidgetData.api_fuzzing_comparison_path = '#{api_fuzzing_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:api_fuzzing)}'
window.gl.mrWidgetData.new_container_scanning_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :container_scanning) if @project.feature_available?(:container_scanning)}'
window.gl.mrWidgetData.new_dependency_scanning_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :dependency_scanning) if @project.feature_available?(:dependency_scanning)}'
window.gl.mrWidgetData.new_sast_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :sast) if @project.feature_available?(:sast)}'
window.gl.mrWidgetData.new_dast_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :dast) if @project.feature_available?(:dast)}'
window.gl.mrWidgetData.new_secret_detection_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :secret_detection) if @project.feature_available?(:secret_detection)}'
window.gl.mrWidgetData.new_coverage_fuzzing_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :coverage_fuzzing) if @project.feature_available?(:coverage_fuzzing)}'
window.gl.mrWidgetData.new_api_fuzzing_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :api_fuzzing) if @project.feature_available?(:api_fuzzing)}'
window.gl.mrWidgetData.aiCommitMessageEnabled = #{::Llm::GenerateCommitMessageService.new(current_user, @merge_request).valid?.to_s}
window.gl.mrWidgetData.dismissal_descriptions = '#{escape_javascript(dismissal_descriptions.to_json)}';
window.gl.mrWidgetData.commit_path_template = '#{commit_path_template(@project)}';
window.gl.mrWidgetData.merge_trains_path = '#{namespace_project_merge_trains_path}';
- if can?(current_user, :read_security_orchestration_policies, @project)
= javascript_tag do
:plain
window.gl.mrWidgetData.security_policies_path = '#{project_security_policies_path(@project)}';
- if @project.licensed_feature_available?(:file_locks)
= javascript_tag do
:plain
window.gl.mrWidgetData.path_locks_path = '#{project_path_locks_path(@project)}'
- if ::Feature.enabled?(:mr_reports_tab, @project, type: :wip)
= javascript_tag do
:plain
window.gl.mrWidgetData.reportsTabPath = '#{reports_project_merge_request_path(@project, @merge_request)}';
window.gl.mrWidgetData.hasPolicies = #{@project.security_policies.enabled.exists?}
= render_ce "projects/merge_requests/show"
= javascript_tag do
:plain
// Append static, server-generated data not included in merge request entity (EE-Only)
// Object.assign would be useful here, but it blows up Phantom.js in tests
window.gl.mrWidgetData.is_geo_secondary_node = '#{Gitlab::Geo.secondary?}' === 'true';
window.gl.mrWidgetData.geo_secondary_help_path = '#{help_page_path("administration/geo/replication/configuration.md")}';
window.gl.mrWidgetData.sast_help_path = '#{help_page_path("user/application_security/sast/index.md")}';
window.gl.mrWidgetData.secret_detection_help_path = '#{help_page_path("user/application_security/secret_detection/index.md")}';
window.gl.mrWidgetData.container_scanning_help_path = '#{help_page_path("user/application_security/container_scanning/index.md")}';
window.gl.mrWidgetData.dast_help_path = '#{help_page_path("user/application_security/dast/index.md")}';
window.gl.mrWidgetData.dependency_scanning_help_path = '#{help_page_path("user/application_security/dependency_scanning/index.md")}';
window.gl.mrWidgetData.api_fuzzing_help_path = '#{help_page_path("user/application_security/api_fuzzing/index.md")}';
window.gl.mrWidgetData.coverage_fuzzing_help_path = '#{help_page_path("user/application_security/coverage_fuzzing/index.md")}';
window.gl.mrWidgetData.license_scanning_comparison_path = '#{license_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}'
window.gl.mrWidgetData.license_scanning_comparison_collapsed_path = '#{license_scanning_reports_collapsed_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:license_scanning)}'
window.gl.mrWidgetData.container_scanning_comparison_path = '#{container_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:container_scanning)}'
window.gl.mrWidgetData.dependency_scanning_comparison_path = '#{dependency_scanning_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:dependency_scanning)}'
window.gl.mrWidgetData.sast_comparison_path = '#{sast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:sast)}'
window.gl.mrWidgetData.dast_comparison_path = '#{dast_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:dast)}'
window.gl.mrWidgetData.secret_detection_comparison_path = '#{secret_detection_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:secret_detection)}'
window.gl.mrWidgetData.coverage_fuzzing_comparison_path = '#{coverage_fuzzing_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:coverage_fuzzing)}'
window.gl.mrWidgetData.api_fuzzing_comparison_path = '#{api_fuzzing_reports_project_merge_request_path(@project, @merge_request) if @project.feature_available?(:api_fuzzing)}'
window.gl.mrWidgetData.new_container_scanning_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :container_scanning) if @project.feature_available?(:container_scanning)}'
window.gl.mrWidgetData.new_dependency_scanning_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :dependency_scanning) if @project.feature_available?(:dependency_scanning)}'
window.gl.mrWidgetData.new_sast_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :sast) if @project.feature_available?(:sast)}'
window.gl.mrWidgetData.new_dast_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :dast) if @project.feature_available?(:dast)}'
window.gl.mrWidgetData.new_secret_detection_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :secret_detection) if @project.feature_available?(:secret_detection)}'
window.gl.mrWidgetData.new_coverage_fuzzing_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :coverage_fuzzing) if @project.feature_available?(:coverage_fuzzing)}'
window.gl.mrWidgetData.new_api_fuzzing_comparison_path = '#{security_reports_project_merge_request_path(@project, @merge_request, type: :api_fuzzing) if @project.feature_available?(:api_fuzzing)}'
window.gl.mrWidgetData.aiCommitMessageEnabled = #{::Llm::GenerateCommitMessageService.new(current_user, @merge_request).valid?.to_s}
window.gl.mrWidgetData.dismissal_descriptions = '#{escape_javascript(dismissal_descriptions.to_json)}';
window.gl.mrWidgetData.commit_path_template = '#{commit_path_template(@project)}';
window.gl.mrWidgetData.merge_trains_path = '#{namespace_project_merge_trains_path}';
- if can?(current_user, :read_security_orchestration_policies, @project)
= javascript_tag do
:plain
window.gl.mrWidgetData.security_policies_path = '#{project_security_policies_path(@project)}';
- if @project.licensed_feature_available?(:file_locks)
= javascript_tag do
:plain
window.gl.mrWidgetData.path_locks_path = '#{project_path_locks_path(@project)}'
- if ::Feature.enabled?(:mr_reports_tab, @project, type: :wip)
= javascript_tag do
:plain
window.gl.mrWidgetData.reportsTabPath = '#{reports_project_merge_request_path(@project, @merge_request)}';
window.gl.mrWidgetData.hasPolicies = #{@project.security_policies.enabled.exists?}
......@@ -23,6 +23,7 @@
scope action: :show do
get :reports, to: 'merge_requests#reports', defaults: { tab: 'reports' }
get '/reports(/*vueroute)', to: 'merge_requests#reports', defaults: { tab: 'reports' }
end
end
......
......@@ -513,7 +513,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
expect(findActionButtons().props('tertiaryButtons')).toEqual([
expect.objectContaining({ href: 'reportsTabPath?type=Test', text: 'View report' }),
expect.objectContaining({ href: 'reportsTabPath/test', text: 'View report' }),
]);
});
......@@ -531,11 +531,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
await nextTick();
expect(window.mrTabs.tabShown).toHaveBeenCalledWith('reports');
expect(window.history.replaceState).toHaveBeenCalledWith(
null,
null,
'reportsTabPath?type=Test',
);
expect(window.history.replaceState).toHaveBeenCalledWith(null, null, 'reportsTabPath/test');
});
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment