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 fa3f06cd0792d906df3795349c2cefa01156d0dd..7deb808b115c947b18a7579a1343c82c720fbff6 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_security_dashboard_new.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/group_security_dashboard_new.vue
index 0362b7a4cba068fea593ffeb39bc5a71a1e6d017..64293bff01dffd28ac20755faedf7997923f9b1a 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/group_vulnerabilities_over_time_panel.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/group_vulnerabilities_over_time_panel.vue
index 2f7085ad482d71bb91e07c507615460dd8e49919..b78bf480fa9d5f975c85c8fdbc92d8bf48582568 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_security_dashboard_new.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/project_security_dashboard_new.vue
index 7dc609ff38128ccabe64ee98e7ebd56712cfaf8a..98fbea461b20746663e45461bc720baa6b2a9a39 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/project_vulnerabilities_over_time_panel.vue b/ee/app/assets/javascripts/security_dashboard/components/shared/project_vulnerabilities_over_time_panel.vue
index 9f183916051a66c1a69ad076293d15f8b5cb215f..e78a599b63259f90540d532cc3419fd3ccff18c8 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/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 0000000000000000000000000000000000000000..d3b0f5f2a96aea8153c2ca80075800b95bde104b
--- /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 @@
+
+
+
+
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 0000000000000000000000000000000000000000..6614125729fee9b0abde910d452e8b71655b2875
--- /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 618d23fb89a4929970beacb5b1f5b987e3485add..7e047ed654160045169d3764e848e6219564911b 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 66727ad25ab60cb5f96c53514f8f3be19f42b7e6..2f6c0d596616aad8eb52d1ff21170552a1393b26 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 e9e42fa8bc0576ed11937e7b839cf66efa677573..b295ae0546251c0e7ad4b6d27d7d486402b61713 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';