diff --git a/docs/dashboard.md b/docs/dashboard.md index efacf4d1a..6b44fbb81 100644 --- a/docs/dashboard.md +++ b/docs/dashboard.md @@ -435,3 +435,7 @@ e.g. of globalConfig.json: the above configuration will create the following filter query: `...source=*license_usage.log type=Usage (st IN ("*addon123*","my_custom_condition*"))...` + +> Note: + +> * In the Data Ingestion table, the first column displays the `View by` options list. When you click on any row in this column, a modal opens, showing detailed information such as `Data volume` and the `Number of events` over time, visualized in charts. The modal allows you to adjust the options via a dropdown to view data for different View by options. This enables dynamic exploration of data trends for various selected inputs. diff --git a/splunk_add_on_ucc_framework/dashboard.py b/splunk_add_on_ucc_framework/dashboard.py index 30f7460ce..083b34063 100644 --- a/splunk_add_on_ucc_framework/dashboard.py +++ b/splunk_add_on_ucc_framework/dashboard.py @@ -47,6 +47,7 @@ "data_ingestion_tab": "data_ingestion_tab_definition.json", "errors_tab": "errors_tab_definition.json", "resources_tab": "resources_tab_definition.json", + "data_ingestion_modal_definition": "data_ingestion_modal_definition.json", } data_ingestion = ( @@ -242,6 +243,21 @@ def generate_dashboard_content( ) ) + if ( + definition_json_name + == default_definition_json_filename["data_ingestion_modal_definition"] + ): + content = ( + utils.get_j2_env() + .get_template(definition_json_name) + .render( + data_ingestion=data_ingestion.format( + lic_usg_condition=lic_usg_condition, determine_by=determine_by + ), + events_count=events_count.format(addon_name=addon_name.lower()), + ) + ) + return content diff --git a/splunk_add_on_ucc_framework/templates/data_ingestion_modal_definition.json b/splunk_add_on_ucc_framework/templates/data_ingestion_modal_definition.json new file mode 100644 index 000000000..460dd3268 --- /dev/null +++ b/splunk_add_on_ucc_framework/templates/data_ingestion_modal_definition.json @@ -0,0 +1,148 @@ +{ + "visualizations": { + "data_ingestion_modal_timerange_label_start_viz": { + "type": "splunk.singlevalue", + "options": { + "majorFontSize": 12, + "backgroundColor": "transparent", + "majorColor": "#9fa4af" + }, + "dataSources": { + "primary": "data_ingestion_modal_data_time_label_start_ds" + } + }, + "data_ingestion_modal_timerange_label_end_viz": { + "type": "splunk.singlevalue", + "options": { + "majorFontSize": 12, + "backgroundColor": "transparent", + "majorColor": "#9fa4af" + }, + "dataSources": { + "primary": "data_ingestion_modal_data_time_label_end_ds" + } + }, + "data_ingestion_modal_data_volume_viz": { + "type": "splunk.line", + "options": { + "xAxisVisibility": "hide", + "seriesColors": ["#A870EF"], + "yAxisTitleText": "Volume (bytes)", + "xAxisTitleText": "Time" + }, + "title": "Data volume", + "dataSources": { + "primary": "data_ingestion_modal_data_volume_ds" + } + }, + "data_ingestion_modal_events_count_viz": { + "type": "splunk.line", + "options": { + "xAxisVisibility": "hide", + "xAxisTitleText": "Time", + "seriesColors": ["#A870EF"], + "yAxisTitleText": "Number of events" + }, + "title": "Number of events", + "dataSources": { + "primary": "ds_search_1" + } + } + }, + "dataSources": { + "data_ingestion_modal_data_time_label_start_ds": { + "type": "ds.search", + "options": { + "query": "| makeresults | addinfo | eval StartDate = strftime(info_min_time, \"%e %b %Y %I:%M%p\") | table StartDate", + "queryParameters": { + "earliest": "$data_ingestion_modal_time.earliest$", + "latest": "$data_ingestion_modal_time.latest$" + } + } + }, + "data_ingestion_modal_data_time_label_end_ds": { + "type": "ds.search", + "options": { + "query": "| makeresults | addinfo | eval EndDate = strftime(info_max_time, \"%e %b %Y %I:%M%p\") | table EndDate", + "queryParameters": { + "earliest": "$data_ingestion_modal_time.earliest$", + "latest": "$data_ingestion_modal_time.latest$" + } + } + }, + "data_ingestion_modal_data_volume_ds": { + "type": "ds.search", + "options": { + "query": "{{data_ingestion}}", + "queryParameters": { + "earliest": "$data_ingestion_modal_time.earliest$", + "latest": "$data_ingestion_modal_time.latest$" + } + } + }, + "ds_search_1": { + "type": "ds.search", + "options": { + "query": "{{events_count}}", + "queryParameters": { + "earliest": "$data_ingestion_modal_time.earliest$", + "latest": "$data_ingestion_modal_time.latest$" + } + }, + "name": "Security Score vs Spend" + } + }, + "defaults": {}, + "inputs": { + "data_ingestion_modal_time_window": { + "options": { + "defaultValue": "-24h,now", + "token": "data_ingestion_modal_time" + }, + "title": "Time Window", + "type": "input.timerange" + } + }, + "layout": { + "type": "grid", + "globalInputs": ["data_ingestion_modal_time_window"], + "structure": [ + { + "item": "data_ingestion_modal_timerange_label_start_viz", + "position": { + "x": 0, + "y": 50, + "w": 100, + "h": 20 + } + }, + { + "item": "data_ingestion_modal_timerange_label_end_viz", + "position": { + "x": 100, + "y": 50, + "w": 100, + "h": 20 + } + }, + { + "item": "data_ingestion_modal_data_volume_viz", + "position": { + "x": 0, + "y": 80, + "w": 300, + "h": 400 + } + }, + { + "item": "data_ingestion_modal_events_count_viz", + "position": { + "x": 0, + "y": 500, + "w": 300, + "h": 400 + } + } + ] + } +} \ No newline at end of file diff --git a/splunk_add_on_ucc_framework/templates/data_ingestion_tab_definition.json b/splunk_add_on_ucc_framework/templates/data_ingestion_tab_definition.json index dd2a6760b..3528f4955 100644 --- a/splunk_add_on_ucc_framework/templates/data_ingestion_tab_definition.json +++ b/splunk_add_on_ucc_framework/templates/data_ingestion_tab_definition.json @@ -103,7 +103,12 @@ } }, "count": 10 - } + }, + "eventHandlers": [ + { + "type": "table.click.handler" + } + ] } }, "dataSources": { diff --git a/tests/testdata/expected_addons/expected_files_conflict_test/expected_log.json b/tests/testdata/expected_addons/expected_files_conflict_test/expected_log.json index 8b33c26da..0aa40f75e 100644 --- a/tests/testdata/expected_addons/expected_files_conflict_test/expected_log.json +++ b/tests/testdata/expected_addons/expected_files_conflict_test/expected_log.json @@ -27,6 +27,7 @@ "appserver/static/js/build/globalConfig.json created\u001b[0m": "INFO", "appserver/static/openapi.json created\u001b[0m": "INFO", "metadata/default.meta created\u001b[0m": "INFO", + "appserver/static/js/build/custom/data_ingestion_modal_definition.json created\u001b[0m": "INFO", "appserver/static/js/build/custom/data_ingestion_tab_definition.json created\u001b[0m": "INFO", "appserver/static/js/build/custom/errors_tab_definition.json created\u001b[0m": "INFO", "appserver/static/js/build/custom/overview_definition.json created\u001b[0m": "INFO", diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/example_input_four.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/example_input_four.py index 652842d8f..394f74826 100644 --- a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/example_input_four.py +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/example_input_four.py @@ -2,8 +2,12 @@ import json import sys +from time import time from splunklib import modularinput as smi +from solnlib import log + +logger = log.Logs().get_logger('splunk_ta_uccexample_four') class EXAMPLE_INPUT_FOUR(smi.Script): @@ -32,13 +36,23 @@ def validate_input(self, definition: smi.ValidationDefinition): def stream_events(self, inputs: smi.InputDefinition, ew: smi.EventWriter): input_items = [{'count': len(inputs.inputs)}] + input_name_1 = "" for input_name, input_item in inputs.inputs.items(): input_item['name'] = input_name + input_name_1 = input_name input_items.append(input_item) + + sourcetype = f'example_input_four-st--{input_name_1.split("://")[-1]}' + host = f'host--{input_name_1.split("://")[-1]}' + event = smi.Event( data=json.dumps(input_items), - sourcetype='example_input_four', + sourcetype=sourcetype, + host=host, + source=input_name_1, ) + log.events_ingested(logger, input_name_1, sourcetype, + str(time())[-3:], "main", "no_account_4", host) ew.write_event(event) diff --git a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/example_input_three.py b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/example_input_three.py index 95bf73b84..c5f4d1cf1 100644 --- a/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/example_input_three.py +++ b/tests/testdata/expected_addons/expected_output_global_config_everything/Splunk_TA_UCCExample/bin/example_input_three.py @@ -2,8 +2,12 @@ import json import sys +from time import time from splunklib import modularinput as smi +from solnlib import log + +logger = log.Logs().get_logger('splunk_ta_uccexample_three') class EXAMPLE_INPUT_THREE(smi.Script): @@ -32,13 +36,24 @@ def validate_input(self, definition: smi.ValidationDefinition): def stream_events(self, inputs: smi.InputDefinition, ew: smi.EventWriter): input_items = [{'count': len(inputs.inputs)}] + input_name_1 = "" for input_name, input_item in inputs.inputs.items(): input_item['name'] = input_name + input_name_1 = input_name input_items.append(input_item) + + sourcetype = f'example_input_three-st--{input_name_1.split("://")[-1]}' + host = f'host--{input_name_1.split("://")[-1]}' + source = f'example_input_three-s--{input_name_1.split("://")[-1]}' + event = smi.Event( data=json.dumps(input_items), - sourcetype='example_input_three', + sourcetype=sourcetype, + host=host, + source=source, ) + log.events_ingested(logger, input_name_1, sourcetype, + str(time())[-3:], "main", "no_account_4", host) ew.write_event(event) diff --git a/tests/testdata/test_addons/package_global_config_everything/package/bin/example_input_four.py b/tests/testdata/test_addons/package_global_config_everything/package/bin/example_input_four.py new file mode 100644 index 000000000..394f74826 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything/package/bin/example_input_four.py @@ -0,0 +1,61 @@ +import import_declare_test + +import json +import sys +from time import time + +from splunklib import modularinput as smi +from solnlib import log + +logger = log.Logs().get_logger('splunk_ta_uccexample_four') + + +class EXAMPLE_INPUT_FOUR(smi.Script): + def __init__(self): + super(EXAMPLE_INPUT_FOUR, self).__init__() + + def get_scheme(self): + scheme = smi.Scheme('example_input_four') + scheme.description = 'Example Input Four' + scheme.use_external_validation = True + scheme.streaming_mode_xml = True + scheme.use_single_instance = False + + scheme.add_argument( + smi.Argument( + 'name', + title='Name', + description='Name', + required_on_create=True + ) + ) + return scheme + + def validate_input(self, definition: smi.ValidationDefinition): + return + + def stream_events(self, inputs: smi.InputDefinition, ew: smi.EventWriter): + input_items = [{'count': len(inputs.inputs)}] + input_name_1 = "" + for input_name, input_item in inputs.inputs.items(): + input_item['name'] = input_name + input_name_1 = input_name + input_items.append(input_item) + + sourcetype = f'example_input_four-st--{input_name_1.split("://")[-1]}' + host = f'host--{input_name_1.split("://")[-1]}' + + event = smi.Event( + data=json.dumps(input_items), + sourcetype=sourcetype, + host=host, + source=input_name_1, + ) + log.events_ingested(logger, input_name_1, sourcetype, + str(time())[-3:], "main", "no_account_4", host) + ew.write_event(event) + + +if __name__ == '__main__': + exit_code = EXAMPLE_INPUT_FOUR().run(sys.argv) + sys.exit(exit_code) \ No newline at end of file diff --git a/tests/testdata/test_addons/package_global_config_everything/package/bin/example_input_three.py b/tests/testdata/test_addons/package_global_config_everything/package/bin/example_input_three.py new file mode 100644 index 000000000..c5f4d1cf1 --- /dev/null +++ b/tests/testdata/test_addons/package_global_config_everything/package/bin/example_input_three.py @@ -0,0 +1,62 @@ +import import_declare_test + +import json +import sys +from time import time + +from splunklib import modularinput as smi +from solnlib import log + +logger = log.Logs().get_logger('splunk_ta_uccexample_three') + + +class EXAMPLE_INPUT_THREE(smi.Script): + def __init__(self): + super(EXAMPLE_INPUT_THREE, self).__init__() + + def get_scheme(self): + scheme = smi.Scheme('example_input_three') + scheme.description = 'Example Input Three' + scheme.use_external_validation = True + scheme.streaming_mode_xml = True + scheme.use_single_instance = False + + scheme.add_argument( + smi.Argument( + 'name', + title='Name', + description='Name', + required_on_create=True + ) + ) + return scheme + + def validate_input(self, definition: smi.ValidationDefinition): + return + + def stream_events(self, inputs: smi.InputDefinition, ew: smi.EventWriter): + input_items = [{'count': len(inputs.inputs)}] + input_name_1 = "" + for input_name, input_item in inputs.inputs.items(): + input_item['name'] = input_name + input_name_1 = input_name + input_items.append(input_item) + + sourcetype = f'example_input_three-st--{input_name_1.split("://")[-1]}' + host = f'host--{input_name_1.split("://")[-1]}' + source = f'example_input_three-s--{input_name_1.split("://")[-1]}' + + event = smi.Event( + data=json.dumps(input_items), + sourcetype=sourcetype, + host=host, + source=source, + ) + log.events_ingested(logger, input_name_1, sourcetype, + str(time())[-3:], "main", "no_account_4", host) + ew.write_event(event) + + +if __name__ == '__main__': + exit_code = EXAMPLE_INPUT_THREE().run(sys.argv) + sys.exit(exit_code) \ No newline at end of file diff --git a/tests/unit/expected_results/data_ingestion_modal_definition.json b/tests/unit/expected_results/data_ingestion_modal_definition.json new file mode 100644 index 000000000..3adcbd7fe --- /dev/null +++ b/tests/unit/expected_results/data_ingestion_modal_definition.json @@ -0,0 +1,148 @@ +{ + "visualizations": { + "data_ingestion_modal_timerange_label_start_viz": { + "type": "splunk.singlevalue", + "options": { + "majorFontSize": 12, + "backgroundColor": "transparent", + "majorColor": "#9fa4af" + }, + "dataSources": { + "primary": "data_ingestion_modal_data_time_label_start_ds" + } + }, + "data_ingestion_modal_timerange_label_end_viz": { + "type": "splunk.singlevalue", + "options": { + "majorFontSize": 12, + "backgroundColor": "transparent", + "majorColor": "#9fa4af" + }, + "dataSources": { + "primary": "data_ingestion_modal_data_time_label_end_ds" + } + }, + "data_ingestion_modal_data_volume_viz": { + "type": "splunk.line", + "options": { + "xAxisVisibility": "hide", + "seriesColors": ["#A870EF"], + "yAxisTitleText": "Volume (bytes)", + "xAxisTitleText": "Time" + }, + "title": "Data volume", + "dataSources": { + "primary": "data_ingestion_modal_data_volume_ds" + } + }, + "data_ingestion_modal_events_count_viz": { + "type": "splunk.line", + "options": { + "xAxisVisibility": "hide", + "xAxisTitleText": "Time", + "seriesColors": ["#A870EF"], + "yAxisTitleText": "Number of events" + }, + "title": "Number of events", + "dataSources": { + "primary": "ds_search_1" + } + } + }, + "dataSources": { + "data_ingestion_modal_data_time_label_start_ds": { + "type": "ds.search", + "options": { + "query": "| makeresults | addinfo | eval StartDate = strftime(info_min_time, \"%e %b %Y %I:%M%p\") | table StartDate", + "queryParameters": { + "earliest": "$data_ingestion_modal_time.earliest$", + "latest": "$data_ingestion_modal_time.latest$" + } + } + }, + "data_ingestion_modal_data_time_label_end_ds": { + "type": "ds.search", + "options": { + "query": "| makeresults | addinfo | eval EndDate = strftime(info_max_time, \"%e %b %Y %I:%M%p\") | table EndDate", + "queryParameters": { + "earliest": "$data_ingestion_modal_time.earliest$", + "latest": "$data_ingestion_modal_time.latest$" + } + } + }, + "data_ingestion_modal_data_volume_ds": { + "type": "ds.search", + "options": { + "query": "index=_internal source=*license_usage.log type=Usage (s IN (example_input_one*,example_input_two*)) | timechart sum(b) as Usage | rename Usage as \"Data volume\"", + "queryParameters": { + "earliest": "$data_ingestion_modal_time.earliest$", + "latest": "$data_ingestion_modal_time.latest$" + } + } + }, + "ds_search_1": { + "type": "ds.search", + "options": { + "query": "index=_internal source=*splunk_ta_uccexample* action=events_ingested | timechart sum(n_events) as \"Number of events\"", + "queryParameters": { + "earliest": "$data_ingestion_modal_time.earliest$", + "latest": "$data_ingestion_modal_time.latest$" + } + }, + "name": "Security Score vs Spend" + } + }, + "defaults": {}, + "inputs": { + "data_ingestion_modal_time_window": { + "options": { + "defaultValue": "-24h,now", + "token": "data_ingestion_modal_time" + }, + "title": "Time Window", + "type": "input.timerange" + } + }, + "layout": { + "type": "grid", + "globalInputs": ["data_ingestion_modal_time_window"], + "structure": [ + { + "item": "data_ingestion_modal_timerange_label_start_viz", + "position": { + "x": 0, + "y": 50, + "w": 100, + "h": 20 + } + }, + { + "item": "data_ingestion_modal_timerange_label_end_viz", + "position": { + "x": 100, + "y": 50, + "w": 100, + "h": 20 + } + }, + { + "item": "data_ingestion_modal_data_volume_viz", + "position": { + "x": 0, + "y": 80, + "w": 300, + "h": 400 + } + }, + { + "item": "data_ingestion_modal_events_count_viz", + "position": { + "x": 0, + "y": 500, + "w": 300, + "h": 400 + } + } + ] + } +} \ No newline at end of file diff --git a/tests/unit/expected_results/data_ingestion_tab_definition.json b/tests/unit/expected_results/data_ingestion_tab_definition.json index e2ee8d737..ae470a03d 100644 --- a/tests/unit/expected_results/data_ingestion_tab_definition.json +++ b/tests/unit/expected_results/data_ingestion_tab_definition.json @@ -103,7 +103,12 @@ } }, "count": 10 - } + }, + "eventHandlers": [ + { + "type": "table.click.handler" + } + ] } }, "dataSources": { diff --git a/ui/src/pages/Dashboard/Custom.tsx b/ui/src/pages/Dashboard/Custom.tsx index 8695e9c40..2d364666a 100644 --- a/ui/src/pages/Dashboard/Custom.tsx +++ b/ui/src/pages/Dashboard/Custom.tsx @@ -1,5 +1,4 @@ import React from 'react'; - import { DashboardCore } from '@splunk/dashboard-core'; import { DashboardContextProvider } from '@splunk/dashboard-context'; import EnterpriseViewOnlyPreset from '@splunk/dashboard-presets/EnterpriseViewOnlyPreset'; diff --git a/ui/src/pages/Dashboard/DashboardModal.tsx b/ui/src/pages/Dashboard/DashboardModal.tsx new file mode 100644 index 000000000..96d72aaf2 --- /dev/null +++ b/ui/src/pages/Dashboard/DashboardModal.tsx @@ -0,0 +1,208 @@ +import React, { useEffect, useCallback, useRef, useState, useMemo } from 'react'; +import { DashboardCore } from '@splunk/dashboard-core'; +import { DashboardContextProvider } from '@splunk/dashboard-context'; +import EnterpriseViewOnlyPreset from '@splunk/dashboard-presets/EnterpriseViewOnlyPreset'; +import type { DashboardCoreApi } from '@splunk/dashboard-types'; +import { EventType } from '@splunk/react-events-viewer/common-types'; +import { getUnifiedConfigs } from '../../util/util'; + +import { + createNewQueryForDataVolumeInModal, + createNewQueryForNumberOfEventsInModal, + fetchDropdownValuesFromQuery, + getActionButtons, + loadDashboardJsonDefinition, + queryMap, +} from './utils'; +import { FieldValue, SearchResponse } from './DataIngestion.types'; + +/** + * @param {object} props + * @param {string} props.selectValueForDropdownInModal state for value in the modal + * @param {string} props.selectTitleForDropdownInModal state for title in the modal + * @param {string} props.setDataIngestionDropdownValues set state for dropdown values in modal + */ +export const DashboardModal = ({ + selectValueForDropdownInModal, + selectTitleForDropdownInModal, + setDataIngestionDropdownValues, +}: { + selectValueForDropdownInModal: string; + selectTitleForDropdownInModal: string; + setDataIngestionDropdownValues: React.Dispatch>; +}) => { + const dashboardCoreApi = useRef(null); + const setDashboardCoreApi = useCallback((api: DashboardCoreApi | null) => { + dashboardCoreApi.current = api; + }, []); + const [dataIngestionModalDef, setDataIngestionModalDef] = useState | null>(null); + const [dropdownFetchedValues, setDropdownFetchedValues] = useState[]>( + [] + ); + + const globalConfig = useMemo(() => getUnifiedConfigs(), []); + + const mergeInputValues = ( + activeValues?: string[], + inactiveValues?: string[] | string + ): Record[] => { + let safeInactiveValues: Record[] = []; + if (typeof inactiveValues === 'string') { + safeInactiveValues = [{ label: `${inactiveValues} (disabled)`, value: inactiveValues }]; + } else if (Array.isArray(inactiveValues)) { + safeInactiveValues = inactiveValues.map((item: string) => ({ + label: `${item} (disabled)`, + value: item, + })); + } + + let safeActiveValues: Record[] = []; + if (typeof activeValues === 'string') { + safeActiveValues = [{ label: activeValues, value: activeValues }]; + } else if (Array.isArray(activeValues)) { + safeActiveValues = activeValues.map((item: string) => ({ + label: item, + value: item, + })); + } + return [...safeActiveValues, ...safeInactiveValues]; + }; + + const processResults = (results: Record[], fieldKey: string) => + results.reduce((extractColumnsValues: Record[], value) => { + if (queryMap[fieldKey] === value.field) { + const dropDownValues = JSON.parse(value.values); + return dropDownValues.map((item: FieldValue) => ({ + label: item.value, + value: item.value, + })); + } + return extractColumnsValues; + }, []); + + const updateModalData = useCallback(() => { + if (!dataIngestionModalDef) { + return null; + } + + const copyDataIngestionModalJson = JSON.parse(JSON.stringify(dataIngestionModalDef)); + let extractColumnsValues: Record[] = []; + + if (selectTitleForDropdownInModal === 'Input') { + const activeState = dropdownFetchedValues[0]?.results[0]?.Active; + const activeInputs = dropdownFetchedValues[0]?.results[0]?.event_input; + const inactiveInputs = dropdownFetchedValues[0]?.results[1]?.event_input; + + // Handle cases where only active or inactive inputs exist + if (activeState === 'yes') { + extractColumnsValues = mergeInputValues(activeInputs, inactiveInputs); + } else if (activeState === 'no') { + extractColumnsValues = mergeInputValues(inactiveInputs, activeInputs); + } + } else if (selectTitleForDropdownInModal === 'Account') { + extractColumnsValues = processResults( + dropdownFetchedValues[1]?.results || [], + selectTitleForDropdownInModal + ); + } else { + extractColumnsValues = processResults( + dropdownFetchedValues[2]?.results || [], + selectTitleForDropdownInModal + ); + } + + setDataIngestionDropdownValues(extractColumnsValues); + + const eventsQuery = createNewQueryForNumberOfEventsInModal( + selectTitleForDropdownInModal, + selectValueForDropdownInModal, + copyDataIngestionModalJson.dataSources.ds_search_1.options.query + ); + const dataVolumeQuery = createNewQueryForDataVolumeInModal( + selectTitleForDropdownInModal, + selectValueForDropdownInModal, + copyDataIngestionModalJson.dataSources.data_ingestion_modal_data_volume_ds.options.query + ); + + copyDataIngestionModalJson.dataSources.data_ingestion_modal_data_volume_ds.options.query = + dataVolumeQuery; + copyDataIngestionModalJson.dataSources.ds_search_1.options.query = eventsQuery; + + // Modify visualizations only for specific cases + if ( + selectTitleForDropdownInModal === 'Input' || + selectTitleForDropdownInModal === 'Account' + ) { + // Remove data volume visualization for "Input" and "Account" + delete copyDataIngestionModalJson.visualizations.data_ingestion_modal_data_volume_viz; + delete copyDataIngestionModalJson.dataSources.data_ingestion_modal_data_volume_ds; + delete copyDataIngestionModalJson.layout.structure[2]; + copyDataIngestionModalJson.layout.structure = + copyDataIngestionModalJson.layout.structure.filter( + (item: Record) => item !== null + ); + copyDataIngestionModalJson.layout.structure[2].position.y = 80; + } else if (selectTitleForDropdownInModal === 'Host') { + // Remove event count visualization for "Host" + delete copyDataIngestionModalJson.visualizations.data_ingestion_modal_events_count_viz; + delete copyDataIngestionModalJson.dataSources.ds_search_1; + delete copyDataIngestionModalJson.layout.structure[3]; + copyDataIngestionModalJson.layout.structure = + copyDataIngestionModalJson.layout.structure.filter( + (item: Record) => item !== null + ); + } + + return copyDataIngestionModalJson; + }, [ + dataIngestionModalDef, + dropdownFetchedValues, + selectTitleForDropdownInModal, + selectValueForDropdownInModal, + setDataIngestionDropdownValues, + ]); + + useEffect(() => { + const fetchDropdownValues = async () => { + const values = await fetchDropdownValuesFromQuery(globalConfig); + setDropdownFetchedValues(values); + }; + + fetchDropdownValues(); + }, [globalConfig]); + + // Update the dashboard when the modal data changes + useEffect(() => { + const updateDefinitionForDashboardModal = async () => { + if (dashboardCoreApi.current && dataIngestionModalDef) { + const updatedModalData = await updateModalData(); + if (updatedModalData) { + dashboardCoreApi.current?.updateDefinition(updatedModalData); + } + } + }; + + updateDefinitionForDashboardModal(); + }, [dataIngestionModalDef, selectValueForDropdownInModal, updateModalData]); + + useEffect(() => { + loadDashboardJsonDefinition( + 'data_ingestion_modal_definition.json', + setDataIngestionModalDef + ); + }, []); + + return dataIngestionModalDef ? ( + + + + ) : null; +}; diff --git a/ui/src/pages/Dashboard/DashboardPage.tsx b/ui/src/pages/Dashboard/DashboardPage.tsx index 25cdfc996..4b47070d1 100644 --- a/ui/src/pages/Dashboard/DashboardPage.tsx +++ b/ui/src/pages/Dashboard/DashboardPage.tsx @@ -20,7 +20,7 @@ import { getUnifiedConfigs } from '../../util/util'; * @param {boolean} isComponentMounted used to remove component data leakage, determines if component is still mounted and dataHandler referes to setState * @param {string} dataHandler callback, called with data as params */ -function loadJson( +export function loadJson( fileName: string, isComponentMounted: boolean, dataHandler: (data: Record) => void diff --git a/ui/src/pages/Dashboard/DataIngestion.tsx b/ui/src/pages/Dashboard/DataIngestion.tsx index d27067f26..a0659ad05 100644 --- a/ui/src/pages/Dashboard/DataIngestion.tsx +++ b/ui/src/pages/Dashboard/DataIngestion.tsx @@ -1,18 +1,21 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { DashboardCore } from '@splunk/dashboard-core'; import { DashboardContextProvider } from '@splunk/dashboard-context'; import EnterpriseViewOnlyPreset from '@splunk/dashboard-presets/EnterpriseViewOnlyPreset'; import Search from '@splunk/react-ui/Search'; import Message from '@splunk/react-ui/Message'; import type { DashboardCoreApi } from '@splunk/dashboard-types'; - import { debounce } from 'lodash'; +import TabLayout from '@splunk/react-ui/TabLayout'; + import { createNewQueryBasedOnSearchAndHideTraffic, getActionButtons, makeVisualAdjustmentsOnDataIngestionPage, addDescriptionToExpandedViewByOptions, } from './utils'; +import { DataIngestionModal } from './DataIngestionModal'; +import { DashboardModal } from './DashboardModal'; const VIEW_BY_INFO_MAP: Record = { Input: 'Volume metrics are not available when the Input view is selected.', @@ -25,15 +28,21 @@ export const DataIngestionDashboard = ({ }: { dashboardDefinition: Record; }) => { - const dashboardCoreApi = React.useRef(); - + const dashboardCoreApi = useRef(null); const [searchInput, setSearchInput] = useState(''); const [viewByInput, setViewByInput] = useState(''); const [toggleNoTraffic, setToggleNoTraffic] = useState(false); - + const [selectValueForDropdownInModal, setSelectValueForDropdownInModal] = useState< + string | null + >(null); + const [selectTitleForDropdownInModal, setSelectTitleForDropdownInModal] = useState< + string | null + >(null); + const [dataIngestionDropdownValues, setDataIngestionDropdownValues] = useState([{}]); useEffect(() => { makeVisualAdjustmentsOnDataIngestionPage(); + // Select the target node for observing mutations const targetNode = document.querySelector( '[data-input-id="data_ingestion_table_input"] button' ); @@ -41,9 +50,12 @@ export const DataIngestionDashboard = ({ const callback = (mutationsList: MutationRecord[]) => { mutationsList.forEach((mutation: MutationRecord) => { if (mutation.attributeName === 'data-test-value') { + // Update the dashboard definition dashboardCoreApi.current?.updateDefinition(dashboardDefinition); setSearchInput(''); setToggleNoTraffic(false); + + // Get the view-by option from the mutated element const viewByOption = (mutation.target as HTMLElement)?.getAttribute('label'); setViewByInput(viewByOption || ''); } @@ -52,25 +64,29 @@ export const DataIngestionDashboard = ({ } }); }; - // mutation is used to detect if dropdown value is changed - // todo: do a better solution + + // Create a MutationObserver instance and start observing const observer = new MutationObserver(callback); if (targetNode) { observer.observe(targetNode, config); } + // Set the current "view by" option when the component mounts const currentViewBy = document .querySelector('[data-input-id="data_ingestion_table_input"] button') ?.getAttribute('label'); setViewByInput(currentViewBy || ''); + // Clean-up function to disconnect the observer when the component unmounts return () => { - observer.disconnect(); + if (observer && targetNode) { + observer.disconnect(); + } }; }, [dashboardDefinition]); - const setDashboardCoreApi = React.useCallback((api: DashboardCoreApi | null) => { + const setDashboardCoreApi = useCallback((api: DashboardCoreApi | null) => { dashboardCoreApi.current = api; }, []); @@ -109,13 +125,58 @@ export const DataIngestionDashboard = ({ const infoMessage = VIEW_BY_INFO_MAP[viewByInput]; + const handleDashboardEvent = useCallback(async (event) => { + if ( + event.type === 'datasource.done' && + event.targetId === 'data_ingestion_table_ds' && + event.payload.data + ) { + const modalInputSelectorName = event.payload.data?.fields[0]?.name; + setSelectTitleForDropdownInModal(modalInputSelectorName); + } + if ( + event.type === 'cell.click' && + event.targetId === 'data_ingestion_table_viz' && + event.payload.cellIndex === 0 && + event.payload.value + ) { + setSelectValueForDropdownInModal(event.payload.value); + } + }, []); + + const dashboardPlugin = useMemo( + () => ({ onEventTrigger: handleDashboardEvent }), + [handleDashboardEvent] + ); return ( <> <> + setSelectValueForDropdownInModal(null)} + title={selectTitleForDropdownInModal || ''} + acceptBtnLabel="Done" + dataIngestionDropdownValues={dataIngestionDropdownValues} + selectValueForDropdownInModal={selectValueForDropdownInModal || ''} + setSelectValueForDropdownInModal={setSelectValueForDropdownInModal} + > + + + + + - {/*
- - Hide items with no traffic - -
*/}
- {infoMessage ? ( + {infoMessage && ( {infoMessage} - ) : null} + )}
diff --git a/ui/src/pages/Dashboard/DataIngestion.types.ts b/ui/src/pages/Dashboard/DataIngestion.types.ts new file mode 100644 index 000000000..e49b3b209 --- /dev/null +++ b/ui/src/pages/Dashboard/DataIngestion.types.ts @@ -0,0 +1,22 @@ +import { EventType } from '@splunk/react-events-viewer/common-types'; + +export interface FieldValue { + value: string; + count: number; +} + +export interface SearchMessage { + type: string; + text: string; +} + +export interface SearchResponse { + sid?: string; + fields: { name: string }[]; + highlighted?: unknown; + init_offset: number; + messages: SearchMessage[]; + preview: boolean; + post_process_count: number; + results: TResult[]; +} diff --git a/ui/src/pages/Dashboard/DataIngestionModal.tsx b/ui/src/pages/Dashboard/DataIngestionModal.tsx new file mode 100644 index 000000000..a79fde076 --- /dev/null +++ b/ui/src/pages/Dashboard/DataIngestionModal.tsx @@ -0,0 +1,176 @@ +import Modal from '@splunk/react-ui/Modal'; +import React, { ReactElement, useCallback, useEffect } from 'react'; +import styled from 'styled-components'; + +import { variables } from '@splunk/themes'; +import Button from '@splunk/react-ui/Button'; +import Dropdown from '@splunk/react-ui/Dropdown'; +import Menu from '@splunk/react-ui/Menu'; +import Checkmark from '@splunk/react-icons/Checkmark'; +import P from '@splunk/react-ui/Paragraph'; +import { StyledButton } from '../EntryPageStyle'; +import { makeVisualAdjustmentsOnDataIngestionModal } from './utils'; + +const ModalWrapper = styled(Modal)` + width: 60vw; + height: 80vh; + margin-top: 3vh; +`; + +const ModalHeader = styled(Modal.Header)` + background-color: ${variables.neutral200}; +`; + +const ModalFooter = styled(Modal.Footer)` + background-color: ${variables.neutral200}; +`; + +const ModalBody = styled(Modal.Body)` + background-color: ${variables.neutral200}; + padding: 15px 30px; + height: 70vh; +`; + +const FooterButtonGroup = styled('div')` + display: grid; + grid-template-columns: 0.35fr 1fr; + margin: 0px ${variables.spacingSmall}; + + .footerBtn:first-child { + justify-self: start; + } + + .footerBtn:last-child { + justify-self: end; + } +`; + +export const DataIngestionModal = ({ + open = false, + handleRequestClose, + title, + acceptBtnLabel = 'Done', + dataIngestionDropdownValues, + selectValueForDropdownInModal, + setSelectValueForDropdownInModal, + children, +}: { + open?: boolean; + handleRequestClose: () => void; + title?: string; + acceptBtnLabel?: string; + dataIngestionDropdownValues: Record[]; + selectValueForDropdownInModal: string; + setSelectValueForDropdownInModal: React.Dispatch>; + children: ReactElement; +}) => { + const toggle = ( +