From c78c05af6d5b1d72a9f544417b689805007da84c Mon Sep 17 00:00:00 2001 From: Dave Pisek Date: Wed, 8 Oct 2025 11:40:33 +0200 Subject: [PATCH 1/2] WIP: add new component --- .../shared/group_security_dashboard_new.vue | 2 +- .../shared/project_security_dashboard_new.vue | 2 +- .../filtered_search_with_query_sync.vue | 133 ++++++++++++++++++ 3 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 ee/app/assets/javascripts/security_dashboard/components/shared/security_dashboard_filtered_search/filtered_search_with_query_sync.vue diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/group_security_dashboard_new.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/group_security_dashboard_new.vue index 0362b7a4cba068..64293bff01dffd 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/group_security_dashboard_new.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/group_security_dashboard_new.vue @@ -3,7 +3,7 @@ import { GlDashboardLayout } from '@gitlab/ui'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import { s__ } from '~/locale'; import { markRaw } from '~/lib/utils/vue3compat/mark_raw'; -import FilteredSearch from 'ee/security_dashboard/components/shared/security_dashboard_filtered_search/filtered_search.vue'; +import FilteredSearch from 'ee/security_dashboard/components/shared/security_dashboard_filtered_search/filtered_search_with_query_sync.vue'; import ProjectToken from 'ee/security_dashboard/components/shared/filtered_search/tokens/project_token.vue'; import ReportTypeToken from 'ee/security_dashboard/components/shared/filtered_search/tokens/report_type_token.vue'; import GroupVulnerabilitiesOverTimePanel from 'ee/security_dashboard/components/shared/group_vulnerabilities_over_time_panel.vue'; diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/project_security_dashboard_new.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/project_security_dashboard_new.vue index 7dc609ff38128c..98fbea461b2074 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/project_security_dashboard_new.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/project_security_dashboard_new.vue @@ -3,7 +3,7 @@ import { GlDashboardLayout } from '@gitlab/ui'; import { s__ } from '~/locale'; import { markRaw } from '~/lib/utils/vue3compat/mark_raw'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import FilteredSearch from 'ee/security_dashboard/components/shared/security_dashboard_filtered_search/filtered_search.vue'; +import FilteredSearch from 'ee/security_dashboard/components/shared/security_dashboard_filtered_search/filtered_search_with_query_sync.vue'; import ReportTypeToken from 'ee/security_dashboard/components/shared/filtered_search/tokens/report_type_token.vue'; import ProjectVulnerabilitiesOverTimePanel from 'ee/security_dashboard/components/shared/project_vulnerabilities_over_time_panel.vue'; import ProjectVulnerabilitiesForSeverityPanel from 'ee/security_dashboard/components/shared/project_vulnerabilities_for_severity_panel.vue'; diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/security_dashboard_filtered_search/filtered_search_with_query_sync.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/security_dashboard_filtered_search/filtered_search_with_query_sync.vue new file mode 100644 index 00000000000000..d3b0f5f2a96aea --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/security_dashboard_filtered_search/filtered_search_with_query_sync.vue @@ -0,0 +1,133 @@ + + -- GitLab From 7dd20f92913680b67b2bbfa849e29740a31a8cc2 Mon Sep 17 00:00:00 2001 From: Dave Pisek Date: Thu, 9 Oct 2025 19:50:21 +0200 Subject: [PATCH 2/2] WIP: Add panel-sync --- .../shared/group_risk_score_panel.vue | 22 +++++- .../group_vulnerabilities_over_time_panel.vue | 48 +++++++++++-- ...roject_vulnerabilities_over_time_panel.vue | 48 +++++++++++-- .../utils/panel_state_url_sync.js | 71 +++++++++++++++++++ .../shared/group_risk_score_panel_spec.js | 5 ++ ...up_vulnerabilities_over_time_panel_spec.js | 5 ++ ...ct_vulnerabilities_over_time_panel_spec.js | 5 ++ 7 files changed, 195 insertions(+), 9 deletions(-) create mode 100644 ee/app/assets/javascripts/security_dashboard/utils/panel_state_url_sync.js diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/group_risk_score_panel.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/group_risk_score_panel.vue index fa3f06cd0792d9..7deb808b115c94 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/group_risk_score_panel.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/group_risk_score_panel.vue @@ -2,11 +2,19 @@ import { GlDashboardPanel, GlBadge, GlTooltipDirective } from '@gitlab/ui'; import { sprintf, s__ } from '~/locale'; import groupTotalRiskScore from 'ee/security_dashboard/graphql/queries/group_total_risk_score.query.graphql'; +import { + readFromUrl, + writeToUrl, + getPanelParamName, +} from 'ee/security_dashboard/utils/panel_state_url_sync'; import TotalRiskScore from './charts/total_risk_score.vue'; import RiskScoreByProject from './charts/risk_score_by_project.vue'; import RiskScoreGroupBy from './risk_score_group_by.vue'; import RiskScoreTooltip from './risk_score_tooltip.vue'; +const PANEL_ID = 'risk-score'; +const DEFAULT_GROUP_BY = 'default'; + export default { name: 'GroupRiskScorePanel', components: { @@ -32,7 +40,10 @@ export default { riskScore: 0, projects: [], hasFetchError: false, - groupedBy: 'default', + groupedBy: readFromUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'groupBy' }), + defaultValue: DEFAULT_GROUP_BY, + }), isOverProjectCountThreshold: false, }; }, @@ -78,6 +89,15 @@ export default { ); }, }, + watch: { + groupedBy(value) { + writeToUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'groupBy' }), + value, + defaultValue: DEFAULT_GROUP_BY, + }); + }, + }, projectCountThreshold: 96, }; diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/group_vulnerabilities_over_time_panel.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/group_vulnerabilities_over_time_panel.vue index 2f7085ad482d71..b78bf480fa9d5f 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/group_vulnerabilities_over_time_panel.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/group_vulnerabilities_over_time_panel.vue @@ -5,6 +5,11 @@ import { formatDate, getDateInPast } from '~/lib/utils/datetime_utility'; import VulnerabilitiesOverTimeChart from 'ee/security_dashboard/components/shared/charts/open_vulnerabilities_over_time.vue'; import groupVulnerabilitiesOverTime from 'ee/security_dashboard/graphql/queries/group_vulnerabilities_over_time.query.graphql'; import { formatVulnerabilitiesOverTimeData } from 'ee/security_dashboard/utils/chart_utils'; +import { + readFromUrl, + writeToUrl, + getPanelParamName, +} from 'ee/security_dashboard/utils/panel_state_url_sync'; import OverTimeSeverityFilter from './over_time_severity_filter.vue'; import OverTimeGroupBy from './over_time_group_by.vue'; import OverTimePeriodSelector from './over_time_period_selector.vue'; @@ -15,6 +20,10 @@ const TIME_PERIODS = { NINETY_DAYS: { key: 'ninetyDays', startDays: 90, endDays: 61 }, }; +const PANEL_ID = 'vulnerabilities-over-time'; +const DEFAULT_GROUP_BY = 'severity'; +const DEFAULT_TIME_PERIOD = 30; + export default { name: 'GroupVulnerabilitiesOverTimePanel', components: { @@ -34,8 +43,14 @@ export default { data() { return { fetchError: false, - groupedBy: 'severity', - selectedTimePeriod: 30, + groupedBy: readFromUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'groupBy' }), + defaultValue: DEFAULT_GROUP_BY, + }), + selectedTimePeriod: readFromUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'timePeriod' }), + defaultValue: DEFAULT_TIME_PERIOD, + }), isLoading: false, chartData: { thirtyDays: [], @@ -43,7 +58,10 @@ export default { ninetyDays: [], }, panelLevelFilters: { - severity: [], + severity: readFromUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'severity' }), + defaultValue: [], + }), }, }; }, @@ -90,9 +108,31 @@ export default { deep: true, immediate: true, }, - selectedTimePeriod() { + selectedTimePeriod(value) { + writeToUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'timePeriod' }), + value, + defaultValue: DEFAULT_TIME_PERIOD, + }); this.fetchChartData(); }, + groupedBy(value) { + writeToUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'groupBy' }), + value, + defaultValue: DEFAULT_GROUP_BY, + }); + }, + 'panelLevelFilters.severity': { + handler(value) { + writeToUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'severity' }), + value, + defaultValue: [], + }); + }, + deep: true, + }, }, methods: { async fetchChartData() { diff --git a/ee/app/assets/javascripts/security_dashboard/components/shared/project_vulnerabilities_over_time_panel.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/project_vulnerabilities_over_time_panel.vue index 9f183916051a66..e78a599b63259f 100644 --- a/ee/app/assets/javascripts/security_dashboard/components/shared/project_vulnerabilities_over_time_panel.vue +++ b/ee/app/assets/javascripts/security_dashboard/components/shared/project_vulnerabilities_over_time_panel.vue @@ -5,6 +5,11 @@ import { formatDate, getDateInPast } from '~/lib/utils/datetime_utility'; import VulnerabilitiesOverTimeChart from 'ee/security_dashboard/components/shared/charts/open_vulnerabilities_over_time.vue'; import projectVulnerabilitiesOverTime from 'ee/security_dashboard/graphql/queries/project_vulnerabilities_over_time.query.graphql'; import { formatVulnerabilitiesOverTimeData } from 'ee/security_dashboard/utils/chart_utils'; +import { + readFromUrl, + writeToUrl, + getPanelParamName, +} from 'ee/security_dashboard/utils/panel_state_url_sync'; import OverTimeSeverityFilter from './over_time_severity_filter.vue'; import OverTimeGroupBy from './over_time_group_by.vue'; import OverTimePeriodSelector from './over_time_period_selector.vue'; @@ -15,6 +20,10 @@ const TIME_PERIODS = { NINETY_DAYS: { key: 'ninetyDays', startDays: 90, endDays: 61 }, }; +const PANEL_ID = 'vulnerabilities-over-time'; +const DEFAULT_GROUP_BY = 'severity'; +const DEFAULT_TIME_PERIOD = 30; + export default { name: 'ProjectVulnerabilitiesOverTimePanel', components: { @@ -34,8 +43,14 @@ export default { data() { return { fetchError: false, - groupedBy: 'severity', - selectedTimePeriod: 30, + groupedBy: readFromUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'groupBy' }), + defaultValue: DEFAULT_GROUP_BY, + }), + selectedTimePeriod: readFromUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'timePeriod' }), + defaultValue: DEFAULT_TIME_PERIOD, + }), isLoading: false, chartData: { thirtyDays: [], @@ -43,7 +58,10 @@ export default { ninetyDays: [], }, panelLevelFilters: { - severity: [], + severity: readFromUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'severity' }), + defaultValue: [], + }), }, }; }, @@ -89,9 +107,31 @@ export default { deep: true, immediate: true, }, - selectedTimePeriod() { + selectedTimePeriod(value) { + writeToUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'timePeriod' }), + value, + defaultValue: DEFAULT_TIME_PERIOD, + }); this.fetchChartData(); }, + groupedBy(value) { + writeToUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'groupBy' }), + value, + defaultValue: DEFAULT_GROUP_BY, + }); + }, + 'panelLevelFilters.severity': { + handler(value) { + writeToUrl({ + paramName: getPanelParamName({ panelId: PANEL_ID, paramName: 'severity' }), + value, + defaultValue: [], + }); + }, + deep: true, + }, }, methods: { async fetchChartData() { diff --git a/ee/app/assets/javascripts/security_dashboard/utils/panel_state_url_sync.js b/ee/app/assets/javascripts/security_dashboard/utils/panel_state_url_sync.js new file mode 100644 index 00000000000000..6614125729fee9 --- /dev/null +++ b/ee/app/assets/javascripts/security_dashboard/utils/panel_state_url_sync.js @@ -0,0 +1,71 @@ +/** + * Reads a value from URL query parameters + * + * @param {Object} options - The options object + * @param {String} options.paramName - The parameter name to read + * @param {*} options.defaultValue - Default value if parameter doesn't exist + * @returns {*} The parameter value or default + */ +export function readFromUrl({ paramName, defaultValue }) { + const params = new URLSearchParams(window.location.search); + const value = params.get(paramName); + + if (value === null) { + return defaultValue; + } + + // Handle arrays (comma-separated values) + if (Array.isArray(defaultValue)) { + return value.split(',').filter(Boolean); + } + + // Handle numbers + if (typeof defaultValue === 'number') { + const parsed = parseInt(value, 10); + return Number.isNaN(parsed) ? defaultValue : parsed; + } + + // Handle strings + return value; +} + +/** + * Writes a value to URL query parameters + * + * @param {Object} options - The options object + * @param {String} options.paramName - The parameter name to write + * @param {*} options.value - The value to write + * @param {*} options.defaultValue - The default value (used to determine if we should remove the param) + */ +export function writeToUrl({ paramName, value, defaultValue }) { + const params = new URLSearchParams(window.location.search); + + // Determine if value is at default state + const isDefault = Array.isArray(value) ? value.length === 0 : value === defaultValue; + + if (isDefault) { + params.delete(paramName); + } else { + const stringValue = Array.isArray(value) ? value.join(',') : String(value); + params.set(paramName, stringValue); + } + + // Update URL without triggering a page reload + const newUrl = params.toString() + ? `${window.location.pathname}?${params.toString()}` + : window.location.pathname; + + window.history.pushState({}, '', newUrl); +} + +/** + * Creates a panel-specific parameter name to avoid conflicts + * + * @param {Object} options - The options object + * @param {String} options.panelId - The unique panel identifier + * @param {String} options.paramName - The parameter name + * @returns {String} The prefixed parameter name + */ +export function getPanelParamName({ panelId, paramName }) { + return `${panelId}_${paramName}`; +} diff --git a/ee/spec/frontend/security_dashboard/components/shared/group_risk_score_panel_spec.js b/ee/spec/frontend/security_dashboard/components/shared/group_risk_score_panel_spec.js index 618d23fb89a492..7e047ed6541600 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/group_risk_score_panel_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/group_risk_score_panel_spec.js @@ -13,6 +13,11 @@ import RiskScoreTooltip from 'ee/security_dashboard/components/shared/risk_score import groupTotalRiskScore from 'ee/security_dashboard/graphql/queries/group_total_risk_score.query.graphql'; Vue.use(VueApollo); +jest.mock('ee/security_dashboard/utils/panel_state_url_sync', () => ({ + readFromUrl: jest.fn((paramName, defaultValue) => defaultValue), + writeToUrl: jest.fn(), + getPanelParamName: jest.fn((panelId, paramName) => `${panelId}_${paramName}`), +})); describe('GroupRiskScorePanel', () => { let wrapper; diff --git a/ee/spec/frontend/security_dashboard/components/shared/group_vulnerabilities_over_time_panel_spec.js b/ee/spec/frontend/security_dashboard/components/shared/group_vulnerabilities_over_time_panel_spec.js index 66727ad25ab60c..2f6c0d596616aa 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/group_vulnerabilities_over_time_panel_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/group_vulnerabilities_over_time_panel_spec.js @@ -14,6 +14,11 @@ import { useFakeDate } from 'helpers/fake_date'; Vue.use(VueApollo); jest.mock('~/alert'); +jest.mock('ee/security_dashboard/utils/panel_state_url_sync', () => ({ + readFromUrl: jest.fn((paramName, defaultValue) => defaultValue), + writeToUrl: jest.fn(), + getPanelParamName: jest.fn((panelId, paramName) => `${panelId}_${paramName}`), +})); describe('GroupVulnerabilitiesOverTimePanel', () => { const todayInIsoFormat = '2022-07-06'; diff --git a/ee/spec/frontend/security_dashboard/components/shared/project_vulnerabilities_over_time_panel_spec.js b/ee/spec/frontend/security_dashboard/components/shared/project_vulnerabilities_over_time_panel_spec.js index e9e42fa8bc0576..b295ae0546251c 100644 --- a/ee/spec/frontend/security_dashboard/components/shared/project_vulnerabilities_over_time_panel_spec.js +++ b/ee/spec/frontend/security_dashboard/components/shared/project_vulnerabilities_over_time_panel_spec.js @@ -14,6 +14,11 @@ import { useFakeDate } from 'helpers/fake_date'; Vue.use(VueApollo); jest.mock('~/alert'); +jest.mock('ee/security_dashboard/utils/panel_state_url_sync', () => ({ + readFromUrl: jest.fn((paramName, defaultValue) => defaultValue), + writeToUrl: jest.fn(), + getPanelParamName: jest.fn((panelId, paramName) => `${panelId}_${paramName}`), +})); describe('ProjectVulnerabilitiesOverTimePanel', () => { const todayInIsoFormat = '2022-07-06'; -- GitLab