Commit 4cf5de97 authored by Max Woolf's avatar Max Woolf

Merge branch '346284-leverage-pipeline-counts-resolver' into 'master'

Count on-demand scans with PipelineScopeCounts

See merge request gitlab-org/gitlab!79851
parents db2bd47a 9bdccd69
......@@ -7,13 +7,7 @@ import {
toggleQueryPollingByVisibility,
} from '~/pipelines/components/graph/utils';
import onDemandScanCounts from '../graphql/on_demand_scan_counts.query.graphql';
import {
HELP_PAGE_PATH,
PIPELINE_TABS_KEYS,
PIPELINES_COUNT_POLL_INTERVAL,
PIPELINES_SCOPE_RUNNING,
PIPELINES_SCOPE_FINISHED,
} from '../constants';
import { HELP_PAGE_PATH, PIPELINE_TABS_KEYS, PIPELINES_COUNT_POLL_INTERVAL } from '../constants';
import AllTab from './tabs/all.vue';
import RunningTab from './tabs/running.vue';
import FinishedTab from './tabs/finished.vue';
......@@ -42,8 +36,6 @@ export default {
variables() {
return {
fullPath: this.projectPath,
runningScope: PIPELINES_SCOPE_RUNNING,
finishedScope: PIPELINES_SCOPE_FINISHED,
};
},
context() {
......@@ -52,7 +44,7 @@ export default {
update(data) {
return Object.fromEntries(
PIPELINE_TABS_KEYS.map((key) => {
const { count } = data[key].pipelines;
const count = data?.project?.pipelineCounts?.[key] ?? data[key]?.pipelines?.count ?? 0;
return [key, count];
}),
);
......
......@@ -21,7 +21,12 @@ import {
import handlesErrors from '../../mixins/handles_errors';
import Actions from '../actions.vue';
import EmptyState from '../empty_state.vue';
import { PIPELINES_PER_PAGE, PIPELINES_POLL_INTERVAL, ACTION_COLUMN } from '../../constants';
import {
PIPELINES_PER_PAGE,
MAX_PIPELINES_COUNT,
PIPELINES_POLL_INTERVAL,
ACTION_COLUMN,
} from '../../constants';
const defaultCursor = {
first: PIPELINES_PER_PAGE,
......@@ -72,6 +77,11 @@ export default {
type: Number,
required: true,
},
maxItemsCount: {
type: Number,
required: false,
default: MAX_PIPELINES_COUNT,
},
emptyStateTitle: {
type: String,
required: false,
......@@ -137,6 +147,10 @@ export default {
};
},
computed: {
formattedCount() {
const { itemsCount, maxItemsCount } = this;
return itemsCount === maxItemsCount ? `${itemsCount}+` : itemsCount;
},
pipelineNodes() {
return this.pipelines?.nodes ?? [];
},
......@@ -214,7 +228,7 @@ export default {
<template #title>
<span class="gl-white-space-nowrap">
{{ title }}
<gl-badge size="sm" class="gl-tab-counter-badge">{{ itemsCount }}</gl-badge>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ formattedCount }}</gl-badge>
</span>
</template>
<template v-if="$apollo.queries.pipelines.loading || hasPipelines">
......
......@@ -15,7 +15,7 @@ import dastProfileDelete from '../../graphql/dast_profile_delete.mutation.graphq
import handlesErrors from '../../mixins/handles_errors';
import { removeProfile } from '../../graphql/cache_utils';
import dastProfilesQuery from '../../graphql/dast_profiles.query.graphql';
import { SAVED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT } from '../../constants';
import { SAVED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT, MAX_DAST_PROFILES_COUNT } from '../../constants';
import BaseTab from './base_tab.vue';
export default {
......@@ -34,6 +34,7 @@ export default {
},
mixins: [handlesErrors],
inject: ['projectPath'],
maxItemsCount: MAX_DAST_PROFILES_COUNT,
tableFields: SAVED_TAB_TABLE_FIELDS,
deleteScanModalId: `delete-scan-modal`,
i18n: {
......@@ -145,6 +146,7 @@ export default {
<template>
<base-tab
:max-items-count="$options.maxItemsCount"
:query="$options.query"
:query-variables="$options.queryVariables"
:title="$options.i18n.title"
......
......@@ -3,7 +3,11 @@ import { GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import DastScanSchedule from 'ee/security_configuration/dast_profiles/components/dast_scan_schedule.vue';
import scheduledDastProfilesQuery from '../../graphql/scheduled_dast_profiles.query.graphql';
import { SCHEDULED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT } from '../../constants';
import {
SCHEDULED_TAB_TABLE_FIELDS,
LEARN_MORE_TEXT,
MAX_DAST_PROFILES_COUNT,
} from '../../constants';
import BaseTab from './base_tab.vue';
export default {
......@@ -14,6 +18,7 @@ export default {
DastScanSchedule,
},
inject: ['timezones'],
maxItemsCount: MAX_DAST_PROFILES_COUNT,
tableFields: SCHEDULED_TAB_TABLE_FIELDS,
i18n: {
title: __('Scheduled'),
......@@ -30,6 +35,7 @@ export default {
<template>
<base-tab
:max-items-count="$options.maxItemsCount"
:query="$options.query"
:title="$options.i18n.title"
:fields="$options.tableFields"
......
......@@ -13,6 +13,8 @@ export const PIPELINE_TABS_KEYS = ['all', 'running', 'finished', 'scheduled', 's
export const PIPELINES_PER_PAGE = 20;
export const PIPELINES_POLL_INTERVAL = 3000;
export const PIPELINES_COUNT_POLL_INTERVAL = 3000;
export const MAX_PIPELINES_COUNT = 1000;
export const MAX_DAST_PROFILES_COUNT = 100;
// Pipeline scopes
export const PIPELINES_SCOPE_RUNNING = 'RUNNING';
......
query onDemandScanCounts(
$fullPath: ID!
$runningScope: PipelineScopeEnum
$finishedScope: PipelineScopeEnum
) {
all: project(fullPath: $fullPath) {
query onDemandScanCounts($fullPath: ID!) {
project(fullPath: $fullPath) {
id
pipelines(source: "ondemand_dast_scan") {
count
}
}
running: project(fullPath: $fullPath) {
id
pipelines(source: "ondemand_dast_scan", scope: $runningScope) {
count
}
}
finished: project(fullPath: $fullPath) {
id
pipelines(source: "ondemand_dast_scan", scope: $finishedScope) {
count
pipelineCounts(source: "ondemand_dast_scan") {
all
running
finished
}
}
scheduled: project(fullPath: $fullPath) {
......
......@@ -2,20 +2,21 @@
module Projects::OnDemandScansHelper
# rubocop: disable CodeReuse/ActiveRecord
def on_demand_scans_data(project)
on_demand_scans = project.all_pipelines.where(source: Enums::Ci::Pipeline.sources[:ondemand_dast_scan])
running_scans_count, finished_scans_count = count_running_and_finished_scans(on_demand_scans)
saved_scans = ::Dast::ProfilesFinder.new({ project_id: project.id }).execute
scheduled_scans_count = saved_scans.count { |scan| scan.dast_profile_schedule }
def on_demand_scans_data(current_user, project)
pipelines_counter = Gitlab::PipelineScopeCounts.new(current_user, project, {
source: "ondemand_dast_scan"
})
scheduled_scans_count = ::Dast::ProfilesFinder.new({ project_id: project.id, has_dast_profile_schedule: true }).execute.count
saved_scans_count = ::Dast::ProfilesFinder.new({ project_id: project.id }).execute.count
common_data(project).merge({
'project-on-demand-scan-counts-etag' => graphql_etag_project_on_demand_scan_counts_path(project),
'on-demand-scan-counts' => {
all: on_demand_scans.length,
running: running_scans_count,
finished: finished_scans_count,
all: pipelines_counter.all,
running: pipelines_counter.running,
finished: pipelines_counter.finished,
scheduled: scheduled_scans_count,
saved: saved_scans.count
saved: saved_scans_count
}.to_json,
'new-dast-scan-path' => new_project_on_demand_scan_path(project),
'empty-state-svg-path' => image_path('illustrations/empty-state/ondemand-scan-empty.svg'),
......@@ -43,19 +44,4 @@ module Projects::OnDemandScansHelper
'project-path' => project.path_with_namespace
}
end
def count_running_and_finished_scans(on_demand_scans)
running_scans_count = 0
finished_scans_count = 0
on_demand_scans.each do |pipeline|
if %w[success failed canceled].include?(pipeline.status)
finished_scans_count += 1
elsif pipeline.status == "running"
running_scans_count += 1
end
end
[running_scans_count, finished_scans_count]
end
end
......@@ -2,4 +2,4 @@
- page_title s_('OnDemandScans|On-demand Scans')
- add_page_specific_style 'page_bundles/ci_status'
#js-on-demand-scans{ data: on_demand_scans_data(@project) }
#js-on-demand-scans{ data: on_demand_scans_data(@current_user, @project) }
......@@ -39,9 +39,9 @@ RSpec.describe 'On-demand DAST scans (GraphQL fixtures)' do
})
expect_graphql_errors_to_be_empty
expect(graphql_data_at(:all, :pipelines, :count)).to be(4)
expect(graphql_data_at(:running, :pipelines, :count)).to be(2)
expect(graphql_data_at(:finished, :pipelines, :count)).to be(2)
expect(graphql_data_at(:project, :pipelineCounts, :all)).to be(4)
expect(graphql_data_at(:project, :pipelineCounts, :running)).to be(2)
expect(graphql_data_at(:project, :pipelineCounts, :finished)).to be(2)
end
end
......
......@@ -122,6 +122,18 @@ describe('BaseTab', () => {
});
describe('when the app loads', () => {
it('formats the items count if it hit its max value', () => {
const itemsCount = 10;
createComponent({
propsData: {
itemsCount,
maxItemsCount: itemsCount,
},
});
expect(findTitle().text()).toMatchInterpolatedText(`All ${itemsCount}+`);
});
it('controls the pipelines query with a visibility check', () => {
jest.spyOn(graphQlUtils, 'toggleQueryPollingByVisibility');
createComponent();
......
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { merge } from 'lodash';
import { merge, cloneDeep } from 'lodash';
import dastProfilesMock from 'test_fixtures/graphql/on_demand_scans/graphql/dast_profiles.query.graphql.json';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SavedTab from 'ee/on_demand_scans/components/tabs/saved.vue';
......@@ -11,7 +11,11 @@ import dastProfilesQuery from 'ee/on_demand_scans/graphql/dast_profiles.query.gr
import dastProfileRunMutation from 'ee/on_demand_scans/graphql/dast_profile_run.mutation.graphql';
import dastProfileDeleteMutation from 'ee/on_demand_scans/graphql/dast_profile_delete.mutation.graphql';
import { createRouter } from 'ee/on_demand_scans/router';
import { SAVED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT } from 'ee/on_demand_scans/constants';
import {
SAVED_TAB_TABLE_FIELDS,
LEARN_MORE_TEXT,
MAX_DAST_PROFILES_COUNT,
} from 'ee/on_demand_scans/constants';
import { s__ } from '~/locale';
import ScanTypeBadge from 'ee/security_configuration/dast_profiles/components/dast_scan_type_badge.vue';
import flushPromises from 'helpers/flush_promises';
......@@ -119,14 +123,17 @@ describe('Saved tab', () => {
it('renders the base tab with the correct props', () => {
createComponent();
expect(findBaseTab().props('title')).toBe(s__('OnDemandScans|Scan library'));
expect(findBaseTab().props('itemsCount')).toBe(itemsCount);
expect(findBaseTab().props('query')).toBe(dastProfilesQuery);
expect(findBaseTab().props('emptyStateTitle')).toBe(
s__('OnDemandScans|There are no saved scans.'),
);
expect(findBaseTab().props('emptyStateText')).toBe(LEARN_MORE_TEXT);
expect(findBaseTab().props('fields')).toBe(SAVED_TAB_TABLE_FIELDS);
expect(cloneDeep(findBaseTab().props())).toEqual({
isActive: true,
title: s__('OnDemandScans|Scan library'),
itemsCount,
maxItemsCount: MAX_DAST_PROFILES_COUNT,
query: dastProfilesQuery,
queryVariables: {},
emptyStateTitle: s__('OnDemandScans|There are no saved scans.'),
emptyStateText: LEARN_MORE_TEXT,
fields: SAVED_TAB_TABLE_FIELDS,
});
});
it('fetches the profiles', () => {
......
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { merge } from 'lodash';
import { merge, cloneDeep } from 'lodash';
import scheduledDastProfilesMock from 'test_fixtures/graphql/on_demand_scans/graphql/scheduled_dast_profiles.query.graphql.json';
import mockTimezones from 'test_fixtures/timezones/abbr.json';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
......@@ -10,7 +10,11 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import scheduledDastProfilesQuery from 'ee/on_demand_scans/graphql/scheduled_dast_profiles.query.graphql';
import { createRouter } from 'ee/on_demand_scans/router';
import { SCHEDULED_TAB_TABLE_FIELDS, LEARN_MORE_TEXT } from 'ee/on_demand_scans/constants';
import {
SCHEDULED_TAB_TABLE_FIELDS,
LEARN_MORE_TEXT,
MAX_DAST_PROFILES_COUNT,
} from 'ee/on_demand_scans/constants';
import { __, s__ } from '~/locale';
import { stripTimezoneFromISODate } from '~/lib/utils/datetime/date_format_utility';
import DastScanSchedule from 'ee/security_configuration/dast_profiles/components/dast_scan_schedule.vue';
......@@ -81,14 +85,17 @@ describe('Scheduled tab', () => {
it('renders the base tab with the correct props', () => {
createComponent();
expect(findBaseTab().props('title')).toBe(__('Scheduled'));
expect(findBaseTab().props('itemsCount')).toBe(itemsCount);
expect(findBaseTab().props('query')).toBe(scheduledDastProfilesQuery);
expect(findBaseTab().props('emptyStateTitle')).toBe(
s__('OnDemandScans|There are no scheduled scans.'),
);
expect(findBaseTab().props('emptyStateText')).toBe(LEARN_MORE_TEXT);
expect(findBaseTab().props('fields')).toBe(SCHEDULED_TAB_TABLE_FIELDS);
expect(cloneDeep(findBaseTab().props())).toEqual({
isActive: true,
title: __('Scheduled'),
itemsCount,
maxItemsCount: MAX_DAST_PROFILES_COUNT,
query: scheduledDastProfilesQuery,
queryVariables: {},
emptyStateTitle: s__('OnDemandScans|There are no scheduled scans.'),
emptyStateText: LEARN_MORE_TEXT,
fields: SCHEDULED_TAB_TABLE_FIELDS,
});
});
it('fetches the profiles', () => {
......
......@@ -13,6 +13,7 @@ RSpec.describe Projects::OnDemandScansHelper do
end
describe '#on_demand_scans_data' do
let_it_be(:current_user) { create(:user) }
let_it_be(:dast_profile) { create(:dast_profile, project: project) }
let_it_be(:dast_profile_with_schedule) { create(:dast_profile, project: project) }
let_it_be(:dast_profile_schedule) { create(:dast_profile_schedule, project: project, dast_profile: dast_profile_with_schedule)}
......@@ -22,10 +23,11 @@ RSpec.describe Projects::OnDemandScansHelper do
create_list(:ci_pipeline, 8, :success, project: project, ref: 'master', source: :ondemand_dast_scan)
create_list(:ci_pipeline, 4, :running, project: project, ref: 'master', source: :ondemand_dast_scan)
allow(helper).to receive(:graphql_etag_project_on_demand_scan_counts_path).and_return(graphql_etag_project_on_demand_scan_counts_path)
project.add_developer(current_user)
end
it 'returns proper data' do
expect(helper.on_demand_scans_data(project)).to match(
expect(helper.on_demand_scans_data(current_user, project)).to match(
'project-path' => "foo/bar",
'new-dast-scan-path' => "/#{project.full_path}/-/on_demand_scans/new",
'empty-state-svg-path' => match_asset_path('/assets/illustrations/empty-state/ondemand-scan-empty.svg'),
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment