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 @@
-
+