diff --git a/README.md b/README.md
index 27f102d8..800a2ff6 100644
--- a/README.md
+++ b/README.md
@@ -22,17 +22,25 @@ This file will also be used when running in Docker.
{
"API_URL": "http://localhost:9000",
"LOGIN_BANNER": "",
- "LOGIN_BANNER_SEVERITY": "info"
+ "LOGIN_BANNER_SEVERITY": "info",
+ "FILTER_COMPONENT_LINKING_ENABLED": false
}
```
+`API_URL` is the url to connect to for the CCF API
`LOGIN_BANNER` displays a dismissible notice on the login page when set to a non-empty string.
`LOGIN_BANNER_SEVERITY` controls its styling and can be `info`, `warn`, `error`, or `success`.
+`FILTER_COMPONENT_LINKING_ENABLED` shows the UI for linking filters to system components
Developers can override these values at build time with `VITE_LOGIN_BANNER` and
`VITE_LOGIN_BANNER_SEVERITY`. `VITE_LOGIN_BANNER` takes precedence over `config.json`; setting it to
an explicit empty string disables the banner for that build without editing `config.json`.
+`FILTER_COMPONENT_LINKING_ENABLED` is a feature flag (default `false`) that shows the UI for linking
+filters to system components: the component "Dashboards" drawer on SSP pages and the Components
+selector on the filter form. It can be overridden at build time with
+`VITE_FILTER_COMPONENT_LINKING_ENABLED=true|false`.
+
#### In Production
Mount a file called `/app/config.json` with your configuration.
diff --git a/env.d.ts b/env.d.ts
index 103cdde7..bae94e7f 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -3,11 +3,8 @@
interface ImportMetaEnv {
readonly VITE_API_URL?: string;
readonly VITE_LOGIN_BANNER?: string;
- readonly VITE_LOGIN_BANNER_SEVERITY?:
- | 'info'
- | 'warn'
- | 'error'
- | 'success';
+ readonly VITE_LOGIN_BANNER_SEVERITY?: 'info' | 'warn' | 'error' | 'success';
+ readonly VITE_FILTER_COMPONENT_LINKING_ENABLED?: string;
}
interface ImportMeta {
diff --git a/src/components/system-security-plans/ComponentsPanel.vue b/src/components/system-security-plans/ComponentsPanel.vue
index afb8ea99..92ce42df 100644
--- a/src/components/system-security-plans/ComponentsPanel.vue
+++ b/src/components/system-security-plans/ComponentsPanel.vue
@@ -63,7 +63,10 @@
-
+
Dashboards
import TooltipTitle from '@/components/TooltipTitle.vue';
-import { computed, watch, ref } from 'vue';
+import { computed, onMounted, watch, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useToast } from 'primevue/usetoast';
import { downloadJson } from '@/utils/download-json';
@@ -225,6 +229,7 @@ import { useDataApi } from '@/composables/axios';
import { useDeleteConfirmationDialog } from '@/utils/delete-dialog';
import { usePermissions } from '@/composables/usePermissions';
import { RESOURCES, ACTIONS } from '@/constants/permissions';
+import { useConfigStore } from '@/stores/config';
import type { Risk, SystemComponent, SystemUser } from '@/oscal';
import {
getRiskComponentIds,
@@ -245,6 +250,11 @@ const router = useRouter();
const toast = useToast();
const { can, permissionTooltip } = usePermissions();
const { confirmDeleteDialog } = useDeleteConfirmationDialog();
+const configStore = useConfigStore();
+const filterComponentLinkingEnabled = computed(
+ () => configStore.filterComponentLinkingEnabled,
+);
+onMounted(() => configStore.getConfig());
const componentsEndpoint = computed(() => {
if (!props.sspId) return null;
diff --git a/src/defaultconfig.json b/src/defaultconfig.json
index 498ce891..4c416f04 100644
--- a/src/defaultconfig.json
+++ b/src/defaultconfig.json
@@ -1,5 +1,6 @@
{
"API_URL": "",
"LOGIN_BANNER": "",
- "LOGIN_BANNER_SEVERITY": "info"
+ "LOGIN_BANNER_SEVERITY": "info",
+ "FILTER_COMPONENT_LINKING_ENABLED": false
}
diff --git a/src/stores/config.spec.ts b/src/stores/config.spec.ts
index 01340816..357ba091 100644
--- a/src/stores/config.spec.ts
+++ b/src/stores/config.spec.ts
@@ -17,6 +17,7 @@ describe('config store', () => {
vi.stubEnv('VITE_API_URL', '');
vi.stubEnv('VITE_LOGIN_BANNER', undefined);
vi.stubEnv('VITE_LOGIN_BANNER_SEVERITY', undefined);
+ vi.stubEnv('VITE_FILTER_COMPONENT_LINKING_ENABLED', undefined);
});
afterEach(() => {
@@ -104,4 +105,34 @@ describe('config store', () => {
expect(config.LOGIN_BANNER).toBe('Local build notice');
expect(config.LOGIN_BANNER_SEVERITY).toBe('success');
});
+
+ it('disables filter component linking by default', async () => {
+ mockConfigResponse({ API_URL: 'https://api.example.test' });
+
+ const store = useConfigStore();
+ expect(store.filterComponentLinkingEnabled).toBe(false);
+ const config = await store.getConfig();
+
+ expect(config.FILTER_COMPONENT_LINKING_ENABLED).toBe(false);
+ expect(store.filterComponentLinkingEnabled).toBe(false);
+ });
+
+ it('enables filter component linking from config.json', async () => {
+ mockConfigResponse({ FILTER_COMPONENT_LINKING_ENABLED: true });
+
+ const store = useConfigStore();
+ await store.getConfig();
+
+ expect(store.filterComponentLinkingEnabled).toBe(true);
+ });
+
+ it('lets VITE_FILTER_COMPONENT_LINKING_ENABLED override config.json', async () => {
+ vi.stubEnv('VITE_FILTER_COMPONENT_LINKING_ENABLED', 'false');
+ mockConfigResponse({ FILTER_COMPONENT_LINKING_ENABLED: true });
+
+ const store = useConfigStore();
+ await store.getConfig();
+
+ expect(store.filterComponentLinkingEnabled).toBe(false);
+ });
});
diff --git a/src/stores/config.ts b/src/stores/config.ts
index 28342041..2aed9c19 100644
--- a/src/stores/config.ts
+++ b/src/stores/config.ts
@@ -1,11 +1,13 @@
import { defineStore } from 'pinia';
-import { ref } from 'vue';
+import { computed, ref } from 'vue';
import builtInConfig from '../defaultconfig.json';
export interface Config {
API_URL: string;
LOGIN_BANNER?: string;
LOGIN_BANNER_SEVERITY?: LoginBannerSeverity;
+ // Feature flag: allow filters (dashboards) to be linked to system components.
+ FILTER_COMPONENT_LINKING_ENABLED?: boolean;
}
const loginBannerSeverities = ['info', 'warn', 'error', 'success'] as const;
@@ -20,9 +22,18 @@ function isLoginBannerSeverity(value: unknown): value is LoginBannerSeverity {
);
}
-function applyBannerOverrides(cfg: Config): Config {
+function parseBooleanEnv(value: string | undefined): boolean | undefined {
+ if (value === 'true') return true;
+ if (value === 'false') return false;
+ return undefined;
+}
+
+function applyOverrides(cfg: Config): Config {
return {
...cfg,
+ FILTER_COMPONENT_LINKING_ENABLED:
+ parseBooleanEnv(import.meta.env.VITE_FILTER_COMPONENT_LINKING_ENABLED) ??
+ cfg.FILTER_COMPONENT_LINKING_ENABLED === true,
LOGIN_BANNER:
import.meta.env.VITE_LOGIN_BANNER !== undefined
? import.meta.env.VITE_LOGIN_BANNER
@@ -48,7 +59,7 @@ export const useConfigStore = defineStore('config', () => {
}
if (import.meta.env.VITE_API_URL) {
- config.value = applyBannerOverrides({
+ config.value = applyOverrides({
...defaultConfig,
API_URL: import.meta.env.VITE_API_URL,
});
@@ -72,13 +83,17 @@ export const useConfigStore = defineStore('config', () => {
}
}
}
- config.value = applyBannerOverrides({
+ config.value = applyOverrides({
...defaultConfig,
...returnedConfig,
});
return config.value;
}
+ const filterComponentLinkingEnabled = computed(
+ () => config.value?.FILTER_COMPONENT_LINKING_ENABLED === true,
+ );
+
function toggleLabels() {
showLabels.value = !showLabels.value;
}
@@ -93,5 +108,6 @@ export const useConfigStore = defineStore('config', () => {
toggleLabels,
toggleHiddenLabels,
getConfig,
+ filterComponentLinkingEnabled,
};
});
diff --git a/src/views/dashboard/partials/FilterForm.vue b/src/views/dashboard/partials/FilterForm.vue
index d801fe35..174bbb32 100644
--- a/src/views/dashboard/partials/FilterForm.vue
+++ b/src/views/dashboard/partials/FilterForm.vue
@@ -53,7 +53,7 @@
-