From c248c250a2eb3016bf5495ae9b94169169cee8ee Mon Sep 17 00:00:00 2001 From: Marc-Antoine Hinse Date: Mon, 25 Nov 2024 11:07:13 -0500 Subject: [PATCH] Severity and categories filter - Moved back the configuration steps business logic into their own component - Added support for severity and source types in the fetch event requests - Created components for severity options and a source type list - Created a constants file - Block the user from going to the next step if the form isn't valid --- packages/flare/bin/constants.py | 2 + packages/flare/bin/cron_job_ingest_events.py | 36 ++- packages/flare/bin/flare.py | 29 +- packages/flare/bin/flare_external_requests.py | 64 +++- .../resources/splunk/default/restmap.conf | 15 + .../main/resources/splunk/default/web.conf | 12 + .../flare/tests/bin/test_flare_wrapper.py | 25 +- .../flare/tests/bin/test_ingest_events.py | 6 + packages/react-components/package.json | 6 +- .../src/ConfigurationScreen.css | 1 + .../src/ConfigurationScreen.tsx | 202 +++---------- .../react-components/src/StatusScreen.tsx | 11 +- .../src/components/Button.css | 5 + .../src/components/Button.tsx | 18 +- .../components/ConfigurationCompletedStep.tsx | 27 +- .../components/ConfigurationGlobalStep.css | 2 +- .../components/ConfigurationInitialStep.tsx | 62 +++- .../ConfigurationUserPreferencesStep.css | 9 +- .../ConfigurationUserPreferencesStep.tsx | 280 +++++++++++++++--- .../react-components/src/components/Label.css | 2 +- .../src/components/SeverityOption.css | 28 ++ .../src/components/SeverityOption.tsx | 47 +++ .../src/components/SeverityOptions.css | 6 + .../src/components/SeverityOptions.tsx | 51 ++++ .../components/SourceTypeCategoryOption.css | 56 ++++ .../components/SourceTypeCategoryOption.tsx | 76 +++++ .../components/SourceTypeCategoryOptions.css | 8 + .../components/SourceTypeCategoryOptions.tsx | 79 +++++ .../src/components/SourceTypeOption.css | 82 +++++ .../src/components/SourceTypeOption.tsx | 31 ++ .../src/components/ToastManager.tsx | 5 + .../src/components/Tooltip.css | 4 +- .../src/components/Tooltip.tsx | 8 +- .../react-components/src/models/constants.ts | 30 ++ packages/react-components/src/models/flare.ts | 17 +- .../react-components/src/models/splunk.ts | 12 +- .../src/utils/setupConfiguration.ts | 154 ++++++---- .../react-components/src/vendor/splunk.min.js | 1 - 38 files changed, 1178 insertions(+), 331 deletions(-) create mode 100644 packages/react-components/src/components/SeverityOption.css create mode 100644 packages/react-components/src/components/SeverityOption.tsx create mode 100644 packages/react-components/src/components/SeverityOptions.css create mode 100644 packages/react-components/src/components/SeverityOptions.tsx create mode 100644 packages/react-components/src/components/SourceTypeCategoryOption.css create mode 100644 packages/react-components/src/components/SourceTypeCategoryOption.tsx create mode 100644 packages/react-components/src/components/SourceTypeCategoryOptions.css create mode 100644 packages/react-components/src/components/SourceTypeCategoryOptions.tsx create mode 100644 packages/react-components/src/components/SourceTypeOption.css create mode 100644 packages/react-components/src/components/SourceTypeOption.tsx create mode 100644 packages/react-components/src/models/constants.ts delete mode 100644 packages/react-components/src/vendor/splunk.min.js diff --git a/packages/flare/bin/constants.py b/packages/flare/bin/constants.py index 6374a9b..53d8e3b 100644 --- a/packages/flare/bin/constants.py +++ b/packages/flare/bin/constants.py @@ -14,6 +14,8 @@ class PasswordKeys(Enum): API_KEY = "api_key" TENANT_ID = "tenant_id" INGEST_METADATA_ONLY = "ingest_metadata_only" + SEVERITIES_FILTER = "severities_filter" + SOURCE_TYPES_FILTER = "source_types_filter" class CollectionKeys(Enum): diff --git a/packages/flare/bin/cron_job_ingest_events.py b/packages/flare/bin/cron_job_ingest_events.py index 036f7a7..e470a97 100644 --- a/packages/flare/bin/cron_job_ingest_events.py +++ b/packages/flare/bin/cron_job_ingest_events.py @@ -93,6 +93,8 @@ def main( api_key = get_api_key(storage_passwords=storage_passwords) tenant_id = get_tenant_id(storage_passwords=storage_passwords) ingest_metadata_only = get_ingest_metadata_only(storage_passwords=storage_passwords) + severities_filter = get_severities_filter(storage_passwords=storage_passwords) + source_types_filter = get_source_types_filter(storage_passwords=storage_passwords) save_last_fetched(kvstore=kvstore) save_last_ingested_tenant_id(kvstore=kvstore, tenant_id=tenant_id) @@ -103,6 +105,8 @@ def main( api_key=api_key, tenant_id=tenant_id, ingest_metadata_only=ingest_metadata_only, + severities=severities_filter, + source_types=source_types_filter, ): save_last_fetched(kvstore=kvstore) @@ -158,6 +162,30 @@ def get_ingest_metadata_only(storage_passwords: StoragePasswords) -> bool: ) +def get_severities_filter(storage_passwords: StoragePasswords) -> list[str]: + severities_filter = get_storage_password_value( + storage_passwords=storage_passwords, + password_key=PasswordKeys.SEVERITIES_FILTER.value, + ) + + if severities_filter: + return severities_filter.split(",") + + return [] + + +def get_source_types_filter(storage_passwords: StoragePasswords) -> list[str]: + source_types_filter = get_storage_password_value( + storage_passwords=storage_passwords, + password_key=PasswordKeys.SOURCE_TYPES_FILTER.value, + ) + + if source_types_filter: + return source_types_filter.split(",") + + return [] + + def get_next(kvstore: KVStoreCollections, tenant_id: int) -> Optional[str]: return get_collection_value( kvstore=kvstore, key=f"{CollectionKeys.get_next_token(tenantId=tenant_id)}" @@ -281,6 +309,8 @@ def fetch_feed( api_key: str, tenant_id: int, ingest_metadata_only: bool, + severities: list[str], + source_types: list[str], ) -> Iterator[tuple[dict, str]]: try: flare_api = FlareAPI(api_key=api_key, tenant_id=tenant_id) @@ -289,7 +319,11 @@ def fetch_feed( start_date = get_start_date(kvstore=kvstore) logger.info(f"Fetching {tenant_id=}, {next=}, {start_date=}") for event_next in flare_api.fetch_feed_events( - next=next, start_date=start_date, ingest_metadata_only=ingest_metadata_only + next=next, + start_date=start_date, + ingest_metadata_only=ingest_metadata_only, + severities=severities, + source_types=source_types, ): yield event_next except Exception as e: diff --git a/packages/flare/bin/flare.py b/packages/flare/bin/flare.py index e1177e9..6c2159b 100644 --- a/packages/flare/bin/flare.py +++ b/packages/flare/bin/flare.py @@ -50,10 +50,14 @@ def fetch_feed_events( next: Optional[str] = None, start_date: Optional[date] = None, ingest_metadata_only: bool, + severities: list[str], + source_types: list[str], ) -> Iterator[tuple[dict, str]]: for response in self._fetch_event_feed_metadata( next=next, start_date=start_date, + severities=severities, + source_types=source_types, ): event_feed = response.json() self.logger.debug(event_feed) @@ -71,6 +75,8 @@ def _fetch_event_feed_metadata( *, next: Optional[str] = None, start_date: Optional[date] = None, + severities: list[str], + source_types: list[str], ) -> Iterator[requests.Response]: data: Dict[str, Any] = { "from": next if next else None, @@ -79,10 +85,16 @@ def _fetch_event_feed_metadata( "gte": start_date.isoformat() if start_date else date.today().isoformat() - } + }, }, } + if len(severities): + data["severity"] = severities + + if len(source_types): + data["type"] = source_types + for response in self.flare_client.scroll( method="POST", url="/firework/v4/events/tenant/_search", @@ -98,7 +110,22 @@ def _fetch_full_event_from_uid(self, *, uid: str) -> dict: self.logger.debug(event) return event + def fetch_api_key_validation(self) -> requests.Response: + return self.flare_client.get( + url="/tokens/test", + ) + def fetch_tenants(self) -> requests.Response: return self.flare_client.get( url="/firework/v2/me/tenants", ) + + def fetch_filters_severity(self) -> requests.Response: + return self.flare_client.get( + url="/firework/v4/events/filters/severities", + ) + + def fetch_filters_source_types(self) -> requests.Response: + return self.flare_client.get( + url="/firework/v4/events/filters/types", + ) diff --git a/packages/flare/bin/flare_external_requests.py b/packages/flare/bin/flare_external_requests.py index 4eb4eed..8a1e5ed 100644 --- a/packages/flare/bin/flare_external_requests.py +++ b/packages/flare/bin/flare_external_requests.py @@ -12,18 +12,66 @@ from logger import Logger +class FlareValidateApiKey(splunk.rest.BaseRestHandler): + def handle_POST(self) -> None: + payload = self.request["payload"] + params = parse.parse_qs(payload) + + if "apiKey" not in params: + raise Exception("API Key is required") + + flare_api = FlareAPI(api_key=params["apiKey"][0]) + flare_api.fetch_api_key_validation() + self.response.setHeader("Content-Type", "application/json") + self.response.write(json.dumps({})) + + class FlareUserTenants(splunk.rest.BaseRestHandler): def handle_POST(self) -> None: logger = Logger(class_name=__file__) payload = self.request["payload"] params = parse.parse_qs(payload) - if "apiKey" in params: - flare_api = FlareAPI(api_key=params["apiKey"][0]) - user_tenants_response = flare_api.fetch_tenants() - tenants_response = user_tenants_response.json() - logger.debug(tenants_response) - self.response.setHeader("Content-Type", "application/json") - self.response.write(json.dumps(tenants_response)) - else: + if "apiKey" not in params: raise Exception("API Key is required") + + flare_api = FlareAPI(api_key=params["apiKey"][0]) + response = flare_api.fetch_tenants() + response_json = response.json() + logger.debug(f"FlareUserTenants: {response_json}") + self.response.setHeader("Content-Type", "application/json") + self.response.write(json.dumps(response_json)) + + +class FlareFiltersSeverity(splunk.rest.BaseRestHandler): + def handle_POST(self) -> None: + logger = Logger(class_name=__file__) + payload = self.request["payload"] + params = parse.parse_qs(payload) + + if "apiKey" not in params: + raise Exception("API Key is required") + + flare_api = FlareAPI(api_key=params["apiKey"][0]) + response = flare_api.fetch_filters_severity() + response_json = response.json() + logger.debug(f"FlareFiltersSeverity: {response_json}") + self.response.setHeader("Content-Type", "application/json") + self.response.write(json.dumps(response_json)) + + +class FlareFiltersSourceTypes(splunk.rest.BaseRestHandler): + def handle_POST(self) -> None: + logger = Logger(class_name=__file__) + payload = self.request["payload"] + params = parse.parse_qs(payload) + + if "apiKey" not in params: + raise Exception("API Key is required") + + flare_api = FlareAPI(api_key=params["apiKey"][0]) + response = flare_api.fetch_filters_source_types() + response_json = response.json() + logger.debug(f"FlareFiltersSourceTypes: {response_json}") + self.response.setHeader("Content-Type", "application/json") + self.response.write(json.dumps(response_json)) diff --git a/packages/flare/src/main/resources/splunk/default/restmap.conf b/packages/flare/src/main/resources/splunk/default/restmap.conf index e5b7847..35f5fc5 100644 --- a/packages/flare/src/main/resources/splunk/default/restmap.conf +++ b/packages/flare/src/main/resources/splunk/default/restmap.conf @@ -1,4 +1,19 @@ +[script:flare_external_requests_api_key_validation] +match=/fetch_api_key_validation +handler=flare_external_requests.FlareValidateApiKey +python.version = python3 + [script:flare_external_requests_user_tenants] match=/fetch_user_tenants handler=flare_external_requests.FlareUserTenants python.version = python3 + +[script:flare_external_requests_filters_severities] +match=/fetch_filters_severities +handler=flare_external_requests.FlareFiltersSeverity +python.version = python3 + +[script:flare_external_requests_filters_source_types] +match=/fetch_filters_source_types +handler=flare_external_requests.FlareFiltersSourceTypes +python.version = python3 diff --git a/packages/flare/src/main/resources/splunk/default/web.conf b/packages/flare/src/main/resources/splunk/default/web.conf index e310a5d..546b97d 100644 --- a/packages/flare/src/main/resources/splunk/default/web.conf +++ b/packages/flare/src/main/resources/splunk/default/web.conf @@ -1,3 +1,15 @@ +[expose:flare_external_requests_api_key_validation] +pattern=fetch_api_key_validation +methods=POST + [expose:flare_external_requests_user_tenants] pattern=fetch_user_tenants methods=POST + +[expose:flare_external_requests_filters_severities] +pattern=fetch_filters_severities +methods=POST + +[expose:flare_external_requests_filters_source_types] +pattern=fetch_filters_source_types +methods=POST diff --git a/packages/flare/tests/bin/test_flare_wrapper.py b/packages/flare/tests/bin/test_flare_wrapper.py index 67894ed..a40540b 100644 --- a/packages/flare/tests/bin/test_flare_wrapper.py +++ b/packages/flare/tests/bin/test_flare_wrapper.py @@ -41,7 +41,11 @@ def test_flare_full_data_without_metadata( events: list[dict] = [] for event, next_token in flare_api.fetch_feed_events( - next=None, start_date=None, ingest_metadata_only=True + next=None, + start_date=None, + ingest_metadata_only=True, + severities=[], + source_types=[], ): assert next_token == expected_return_value["next"] events.append(event) @@ -100,7 +104,11 @@ def test_flare_full_data_with_metadata( events: list[dict] = [] for event, next_token in flare_api.fetch_feed_events( - next=None, start_date=None, ingest_metadata_only=False + next=None, + start_date=None, + ingest_metadata_only=False, + severities=[], + source_types=[], ): assert next_token == expected_return_value["next"] events.append(event) @@ -138,9 +146,14 @@ def test_flare_full_data_with_metadata_and_exception( flare_api = FlareAPI(api_key="some_key", tenant_id=111) with pytest.raises(KeyError, match="metadata"): - for _, _ in flare_api.fetch_feed_events( - next=None, start_date=None, ingest_metadata_only=False - ): - pass + next( + flare_api.fetch_feed_events( + next=None, + start_date=None, + ingest_metadata_only=False, + severities=[], + source_types=[], + ) + ) fetch_event_feed_metadata_mock.assert_called_once() diff --git a/packages/flare/tests/bin/test_ingest_events.py b/packages/flare/tests/bin/test_ingest_events.py index 1d503db..9433e2d 100644 --- a/packages/flare/tests/bin/test_ingest_events.py +++ b/packages/flare/tests/bin/test_ingest_events.py @@ -228,6 +228,8 @@ def test_fetch_feed_expect_exception() -> None: api_key="some_key", tenant_id=11111, ingest_metadata_only=False, + severities=[], + source_types=[], ): pass @@ -262,6 +264,8 @@ def test_fetch_feed_expect_feed_response( api_key="some_key", tenant_id=11111, ingest_metadata_only=False, + severities=[], + source_types=[], ): assert next_token == next events.append(event) @@ -320,4 +324,6 @@ def test_main_expect_normal_run( api_key="some_api_key", tenant_id=111, ingest_metadata_only=False, + severities=[], + source_types=[], ) diff --git a/packages/react-components/package.json b/packages/react-components/package.json index 4b7beb3..131cf97 100644 --- a/packages/react-components/package.json +++ b/packages/react-components/package.json @@ -4,12 +4,12 @@ "license": "UNLICENSED", "scripts": { "build": "node build.js build", - "eslint": "eslint src --ext \".js,.tsx,.ts\"", - "eslint:fix": "eslint src --ext \".js,.tsx,.ts\" --fix", + "eslint": "eslint src --ext \".tsx,.ts\"", + "eslint:fix": "eslint src --ext \".tsx,.ts\" --fix", "lint": "yarn run eslint && yarn run stylelint", "lint:ci": "yarn run eslint:ci && yarn run stylelint", "start": "webpack --watch", - "stylelint": "stylelint \"src/**/*.{js,jsx}\" --config stylelint.config.js" + "stylelint": "stylelint \"src/**/*.{ts,tsx}\" --config stylelint.config.js" }, "exports": { "./configuration-screen": "./ConfigurationScreen.js", diff --git a/packages/react-components/src/ConfigurationScreen.css b/packages/react-components/src/ConfigurationScreen.css index 40d7782..20b846b 100644 --- a/packages/react-components/src/ConfigurationScreen.css +++ b/packages/react-components/src/ConfigurationScreen.css @@ -17,6 +17,7 @@ width: 800px; gap: 1.5rem; align-self: center; + padding-bottom: 2rem; } .content-step { diff --git a/packages/react-components/src/ConfigurationScreen.tsx b/packages/react-components/src/ConfigurationScreen.tsx index 35f8996..84bf876 100644 --- a/packages/react-components/src/ConfigurationScreen.tsx +++ b/packages/react-components/src/ConfigurationScreen.tsx @@ -1,156 +1,40 @@ -import React, { useEffect, useState, FC } from 'react'; -import { - appName, - createFlareIndex, - redirectToHomepage, - fetchApiKey, - fetchAvailableIndexNames, - fetchCurrentIndexName, - fetchIngestMetadataOnly, - fetchTenantId, - fetchUserTenants, - saveConfiguration, -} from './utils/setupConfiguration'; -import { ConfigurationSteps, Tenant } from './models/flare'; -import './global.css'; +import React, { FC, useEffect, useState } from 'react'; import './ConfigurationScreen.css'; +import ConfigurationCompletedStep from './components/ConfigurationCompletedStep'; +import ConfigurationInitialStep from './components/ConfigurationInitialStep'; +import ConfigurationUserPreferencesStep from './components/ConfigurationUserPreferencesStep'; import LoadingBar from './components/LoadingBar'; +import { toastManager } from './components/ToastManager'; import DoneIcon from './components/icons/DoneIcon'; import ExternalLinkIcon from './components/icons/ExternalLinkIcon'; -import { toastManager } from './components/ToastManager'; import ToolIcon from './components/icons/ToolIcon'; -import ConfigurationInitialStep from './components/ConfigurationInitialStep'; -import ConfigurationUserPreferencesStep from './components/ConfigurationUserPreferencesStep'; -import ConfigurationCompletedStep from './components/ConfigurationCompletedStep'; - -const TOAST_API_KEY_ERROR = 'api_key_error'; -const TOAST_TENANT_SUCCESS = 'tenant_success'; +import './global.css'; +import { ConfigurationStep } from './models/flare'; +import { createFlareIndex, fetchApiKey, redirectToHomepage } from './utils/setupConfiguration'; const ConfigurationScreen: FC<{ theme: string }> = ({ theme }) => { + const [configurationStep, setConfigurationStep] = useState(ConfigurationStep.Initial); const [apiKey, setApiKey] = useState(''); - const [tenantId, setTenantId] = useState(-1); - const [indexName, setIndexName] = useState(appName); - const [errorMessage, setErrorMessage] = useState(''); - const [tenants, setUserTenants] = useState([]); - const [isIngestingMetadataOnly, setIsIngestingMetadataOnly] = useState(false); - const [isLoading, setIsLoading] = useState(false); - const [isCompleted, setIsCompleted] = useState(false); - const [indexNames, setIndexNames] = useState([]); toastManager.setTheme(theme); - function reset(): void { - setApiKey(''); - setTenantId(-1); - setUserTenants([]); - setIsLoading(false); - setIsCompleted(false); - } - - function getCurrentConfigurationStep(): ConfigurationSteps { - if (tenants.length === 0) { - return ConfigurationSteps.Initial; - } - if (!isCompleted) { - return ConfigurationSteps.UserPreferences; - } - - return ConfigurationSteps.Completed; - } - - const handleApiKeyChange = (e): void => setApiKey(e.target.value); - const handleTenantIdChange = (e): void => setTenantId(parseInt(e.target.value, 10)); - const handleIndexNameChange = (e): void => setIndexName(e.target.value); - const handleIsIngestingMetadataChange = (e): void => { - setIsIngestingMetadataOnly(e.target.checked); - }; - const handleBackButton = (): void => { - const currentConfigurationStep = getCurrentConfigurationStep(); - if (currentConfigurationStep === ConfigurationSteps.Initial) { + if (configurationStep === ConfigurationStep.Initial) { redirectToHomepage(); - } else if (currentConfigurationStep === ConfigurationSteps.UserPreferences) { - setUserTenants([]); - } else if (currentConfigurationStep === ConfigurationSteps.Completed) { - reset(); + } else if (configurationStep === ConfigurationStep.UserPreferences) { + setConfigurationStep(ConfigurationStep.Initial); + } else if (configurationStep === ConfigurationStep.Completed) { + setConfigurationStep(ConfigurationStep.Initial); } }; - const handleSubmitApiKey = (): void => { - setIsLoading(true); - fetchUserTenants( - apiKey, - (userTenants: Tenant[]) => { - if (tenantId === -1 && userTenants.length > 0) { - setTenantId(userTenants[0].id); - } - setErrorMessage(''); - setUserTenants(userTenants); - setIsLoading(false); - }, - (error: string) => { - setErrorMessage(error); - setIsLoading(false); - toastManager.show({ - id: TOAST_API_KEY_ERROR, - isError: true, - content: 'Something went wrong. Please review your form.', - }); - } - ); - }; - - const handleSubmitTenant = (): void => { - setIsLoading(true); - saveConfiguration(apiKey, tenantId, indexName, isIngestingMetadataOnly) - .then(() => { - setIsLoading(false); - setIsCompleted(true); - toastManager.destroy(TOAST_API_KEY_ERROR); - toastManager.show({ - id: TOAST_TENANT_SUCCESS, - content: 'Configured Flare Account', - }); - }) - .catch((e: any) => { - setIsLoading(false); - toastManager.show({ - id: TOAST_API_KEY_ERROR, - isError: true, - content: `Something went wrong. ${e.responseText}`, - }); - }); - }; - - function getSelectedTenantName(): string { - const filteredTenants = tenants.filter((tenant: Tenant) => tenant.id === tenantId); - if (filteredTenants.length > 0) { - return filteredTenants[0].name; - } - - return 'unknown'; - } - useEffect(() => { - if (isCompleted) { - return; - } - createFlareIndex().then(() => { - Promise.all([ - fetchApiKey(), - fetchTenantId(), - fetchIngestMetadataOnly(), - fetchCurrentIndexName(), - fetchAvailableIndexNames(), - ]).then(([key, id, ingestMetadataOnly, index, availableIndexNames]) => { + if (configurationStep === ConfigurationStep.Initial) { + Promise.all([fetchApiKey(), createFlareIndex()]).then(([key]) => { setApiKey(key); - setTenantId(id); - setIsIngestingMetadataOnly(ingestMetadataOnly); - setIndexName(index); - setIndexNames(availableIndexNames); }); - }); - }, [isCompleted]); + } + }, [configurationStep]); useEffect(() => { const container = document.getElementById('container') as HTMLDivElement; @@ -160,51 +44,37 @@ const ConfigurationScreen: FC<{ theme: string }> = ({ theme }) => { } }, [theme]); - const currentConfigurationStep = getCurrentConfigurationStep(); - return (
- +
-
diff --git a/packages/react-components/src/components/ConfigurationUserPreferencesStep.css b/packages/react-components/src/components/ConfigurationUserPreferencesStep.css index c367223..67d1cf9 100644 --- a/packages/react-components/src/components/ConfigurationUserPreferencesStep.css +++ b/packages/react-components/src/components/ConfigurationUserPreferencesStep.css @@ -2,13 +2,8 @@ font-size: 0.75rem; color: var(--secondary-text-color); margin-top: 5px; - margin-bottom: 1rem; } -.switch-layout { - display: flex; - flex-direction: row; - gap: 0.5rem; - align-items: center; - margin-top: 1rem; +.switch-container { + margin-top: 0.5rem; } diff --git a/packages/react-components/src/components/ConfigurationUserPreferencesStep.tsx b/packages/react-components/src/components/ConfigurationUserPreferencesStep.tsx index 92eaf9c..8fbe76f 100644 --- a/packages/react-components/src/components/ConfigurationUserPreferencesStep.tsx +++ b/packages/react-components/src/components/ConfigurationUserPreferencesStep.tsx @@ -1,47 +1,198 @@ -import React, { ChangeEvent, FC } from 'react'; -import Label from './Label'; +import React, { FC, useEffect, useState } from 'react'; +import { + ConfigurationStep, + Severity, + SourceType, + SourceTypeCategory, + Tenant, +} from '../models/flare'; import Button from './Button'; -import { Tenant } from '../models/flare'; +import Label from './Label'; import Select from './Select'; +import { appName, DEFAULT_FILTER_VALUE } from '../models/constants'; +import { + fetchAvailableIndexNames, + fetchCurrentIndexName, + fetchFiltersSeverities, + fetchFiltersSourceTypes, + fetchIngestMetadataOnly, + fetchSeveritiesFilter, + fetchSourceTypesFilter, + fetchTenantId, + fetchUserTenants, + saveConfiguration, +} from '../utils/setupConfiguration'; import './ConfigurationGlobalStep.css'; import './ConfigurationUserPreferencesStep.css'; +import SeverityOptions from './SeverityOptions'; +import SourceTypeCategoryOptions from './SourceTypeCategoryOptions'; import Switch from './Switch'; +import { ToastKeys, toastManager } from './ToastManager'; +import Tooltip from './Tooltip'; const ConfigurationUserPreferencesStep: FC<{ show: boolean; - tenants: Tenant[]; - selectedTenantId: number; - indexNames: string[]; - selectedIndexName: string; - isLoading: boolean; - isIngestingMetadataOnly: boolean; + configurationStep: ConfigurationStep; + apiKey: string; onNavigateBackClick: () => void; - onSubmitUserPreferencesClick: () => void; - onTenantIdChange: (e: ChangeEvent) => void; - onIndexNameChange: (e: ChangeEvent) => void; - onIngestingMetadataChange: (e: ChangeEvent) => void; -}> = ({ - show, - tenants, - selectedTenantId, - indexNames, - selectedIndexName, - isLoading, - isIngestingMetadataOnly, - onNavigateBackClick, - onSubmitUserPreferencesClick, - onTenantIdChange, - onIndexNameChange, - onIngestingMetadataChange, -}) => { + onUserPreferencesSaved: () => void; +}> = ({ show, configurationStep, apiKey, onNavigateBackClick, onUserPreferencesSaved }) => { + const [tenantId, setTenantId] = useState(undefined); + const [tenants, setUserTenants] = useState([]); + const [selectedSeverities, setSelectedSeverities] = useState([]); + const [severities, setSeverities] = useState([]); + const [sourceTypeCategories, setSourceTypeCategories] = useState([]); + const [selectedSourceTypes, setSelectedSourceTypes] = useState([]); + const [indexName, setIndexName] = useState(''); + const [indexNames, setIndexNames] = useState([]); + const [isIngestingMetadataOnly, setIsIngestingMetadataOnly] = useState(false); + const [isLoading, setIsLoading] = useState(false); + + const handleTenantIdChange = (e): void => setTenantId(parseInt(e.target.value, 10)); + const handleIndexNameChange = (e): void => setIndexName(e.target.value); + const handleIsIngestingMetadataChange = (e): void => + setIsIngestingMetadataOnly(e.target.checked); + + const handleSubmitUserPreferences = (): void => { + setIsLoading(true); + let severitiesFilter = ''; + if (selectedSeverities.length !== severities.length) { + severitiesFilter = selectedSeverities.map((severity) => severity.value).join(','); + } + let sourceTypesFilter = ''; + if ( + selectedSourceTypes.length !== + sourceTypeCategories.flatMap((category) => category.types).length + ) { + sourceTypesFilter = selectedSourceTypes.map((sourceType) => sourceType.value).join(','); + } + saveConfiguration( + apiKey, + Number(tenantId), + indexName, + isIngestingMetadataOnly, + severitiesFilter, + sourceTypesFilter + ) + .then(() => { + setIsLoading(false); + toastManager.destroy(ToastKeys.ERROR); + toastManager.show({ + id: ToastKeys.SUCCESS, + content: 'Configured Flare Account', + }); + onUserPreferencesSaved(); + }) + .catch((e: any) => { + setIsLoading(false); + toastManager.show({ + id: ToastKeys.ERROR, + isError: true, + content: `Something went wrong. ${e.responseText}`, + }); + }); + }; + + useEffect(() => { + if (configurationStep === ConfigurationStep.UserPreferences) { + Promise.all([ + fetchTenantId(), + fetchIngestMetadataOnly(), + fetchCurrentIndexName(), + fetchUserTenants(apiKey), + fetchFiltersSeverities(apiKey), + fetchFiltersSourceTypes(apiKey), + fetchAvailableIndexNames(), + fetchSeveritiesFilter(), + fetchSourceTypesFilter(), + ]) + .then( + ([ + id, + ingestMetadataOnly, + index, + userTenants, + availableSeverities, + availableSourceTypeCategories, + availableIndexNames, + severitiesFilter, + sourceTypesFilter, + ]) => { + setTenantId(id); + setIsIngestingMetadataOnly(ingestMetadataOnly); + setIndexName(index); + if (id === -1 && userTenants.length > 0) { + setTenantId(userTenants[0].id); + } + setUserTenants(userTenants); + setSeverities(availableSeverities); + setSourceTypeCategories(availableSourceTypeCategories); + setIndexNames(availableIndexNames); + + const currentSeverities: Severity[] = []; + severitiesFilter.forEach((severityValue) => { + if (severityValue === DEFAULT_FILTER_VALUE) { + currentSeverities.push(...availableSeverities); + } else { + const foundSeverityMatch = availableSeverities.find( + (severity) => severity.value === severityValue + ); + if (foundSeverityMatch) { + currentSeverities.push(foundSeverityMatch); + } + } + }); + setSelectedSeverities(currentSeverities); + + const currentSourceTypes: SourceType[] = []; + sourceTypesFilter.forEach((sourceTypeValue) => { + if (sourceTypeValue === DEFAULT_FILTER_VALUE) { + currentSourceTypes.push( + ...availableSourceTypeCategories.flatMap( + (category) => category.types + ) + ); + } else { + const foundSourceTypeMatch = availableSourceTypeCategories + .flatMap((category) => category.types) + .find((sourceType) => sourceType.value === sourceTypeValue); + if (foundSourceTypeMatch) { + currentSourceTypes.push(foundSourceTypeMatch); + } + } + }); + setSelectedSourceTypes(currentSourceTypes); + } + ) + .catch(() => { + toastManager.show({ + id: ToastKeys.ERROR, + isError: true, + content: 'Something went wrong.', + }); + }); + } else { + setTenantId(undefined); + setIndexName(appName); + setUserTenants([]); + setSeverities([]); + setSourceTypeCategories([]); + setIsLoading(false); + } + }, [configurationStep, apiKey]); + + const isFormValid = (): boolean => { + return selectedSeverities.length > 0 && selectedSourceTypes.length > 0; + }; + return (