Skip to content

Commit

Permalink
feat: add support for custom dashboards (#979)
Browse files Browse the repository at this point in the history
This feature allows user to add his own, custom components to the
dashboard page ("row" tags). Custom components can be added along with
the 3 currently functioning panels. Content and order of panels on
dashboard page is set using globalconfig.

---------

Co-authored-by: Artem Rys <rysartem@gmail.com>
  • Loading branch information
sgoral-splunk and artemrys authored Jan 3, 2024
1 parent 0e167d0 commit 7fe3d58
Show file tree
Hide file tree
Showing 5 changed files with 432 additions and 23 deletions.
98 changes: 98 additions & 0 deletions docs/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,101 @@ index=_internal source=*<addon_name>* ERROR
```

> Note: <addon_name> is being replaced by the actual value during the build time.
<br>
# Custom components

UCC also supports adding your own components to the dashboard.
To do this, create a **dashboard_components.xml** file in the add-on's root directory (at the same level as globalConfig.json).
The correct structure of this file is a root tag called **custom-dashboard** and child elements called **row**, any other child tags will break the build process.
Inside the **row** tags you can specify your **panels**.

For more information about xml structure and element hierarchy [see](https://docs.splunk.com/Documentation/Splunk/latest/Viz/PanelreferenceforSimplifiedXML#panel).


Please note that this autogenerated dashboard also supports dynamic setting of data range `<input type="time" token="log_time">`. If you want to use a dynamic data range in your panels, you must reference the `log_time` token.<br>

```xml
<earliest>$log_time.earliest$</earliest>
<latest>$log_time.latest$</latest>
```
<br>

**dashboard_components.xml** location:
```
<TA>
├── package
...
├── dashboard_components.xml
├── globalConfig.json
...
```

sample **dashboard_components.xml** structure:
```xml
<custom-dashboard>
<row>
<panel>
<title>MY PANEL IN ROW 1</title>
<chart>
<search>
<query>index=_internal
</query>
<earliest>0</earliest>
<latest>now</latest>
</search>
</chart>
</panel>
<panel>
<title>MY SECOND PANEL IN ROW 1</title>
<chart>
<search>
<query>index=_internal</query>
<earliest>-14d@d</earliest>
<latest></latest>
<sampleRatio>1</sampleRatio>
</search>
</chart>
</panel>
</row>
<row>
<panel>
<title>MY PANEL IN ROW 2</title>
<chart>
<search>
<query>index=_internal</query>
<earliest>$log_time.earliest$</earliest>
<latest>$log_time.latest$</latest>
</search>
</chart>
</panel>
</row>
</custom-dashboard>
```

Next you have to add **custom** panel to your dashboard page in globalConfig.json.
The order of panels in the globalConfig corresponds to the order of rows on the dashboard.

```json
{
...
"dashboard": {
"panels": [
{
"name": "addon_version"
},
{
"name": "events_ingested_by_sourcetype"
},
{
"name": "errors_in_the_addon"
},
{
"name": "custom"
}
]
}
...
}
```
7 changes: 2 additions & 5 deletions splunk_add_on_ucc_framework/commands/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,11 +599,8 @@ def generate(
"views",
"dashboard.xml",
)
dashboard.generate_dashboard(
global_config,
ta_name,
dashboard_xml_path,
)
dashboard.generate_dashboard(global_config, ta_name, dashboard_xml_path)

else:
global_config = None
conf_file_names = []
Expand Down
70 changes: 67 additions & 3 deletions splunk_add_on_ucc_framework/dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#
import logging
import os
import sys
from defusedxml import ElementTree
from typing import Sequence

from splunk_add_on_ucc_framework import global_config as global_config_lib
Expand All @@ -24,12 +26,14 @@
PANEL_ADDON_VERSION = "addon_version"
PANEL_EVENTS_INGESTED_BY_SOURCETYPE = "events_ingested_by_sourcetype"
PANEL_ERRORS_IN_THE_ADDON = "errors_in_the_addon"
PANEL_CUSTOM = "custom"

SUPPORTED_PANEL_NAMES = frozenset(
[
PANEL_ADDON_VERSION,
PANEL_EVENTS_INGESTED_BY_SOURCETYPE,
PANEL_ERRORS_IN_THE_ADDON,
PANEL_CUSTOM,
]
)
SUPPORTED_PANEL_NAMES_READABLE = ", ".join(SUPPORTED_PANEL_NAMES)
Expand All @@ -48,7 +52,6 @@
"""
DASHBOARD_END = """</form>"""


PANEL_ADDON_VERSION_TEMPLATE = """ <row>
<panel>
<title>Add-on version</title>
Expand Down Expand Up @@ -103,7 +106,9 @@
"""


def generate_dashboard_content(addon_name: str, panel_names: Sequence[str]) -> str:
def generate_dashboard_content(
addon_name: str, panel_names: Sequence[str], custom_components: str
) -> str:
content = DASHBOARD_START
for panel_name in panel_names:
logger.info(f"Including {panel_name} into the dashboard page")
Expand All @@ -117,6 +122,8 @@ def generate_dashboard_content(addon_name: str, panel_names: Sequence[str]) -> s
content += PANEL_ERRORS_IN_THE_ADDON_TEMPLATE.format(
addon_name=addon_name.lower()
)
elif panel_name == PANEL_CUSTOM:
content += custom_components
else:
raise AssertionError("Should not be the case!")
content += DASHBOARD_END
Expand All @@ -136,6 +143,63 @@ def generate_dashboard(
else:
panels = global_config.dashboard.get("panels", [])
panel_names = [panel["name"] for panel in panels]
content = generate_dashboard_content(addon_name, panel_names)
custom_components = ""
if PANEL_CUSTOM in panel_names:
dashboard_components_path = os.path.abspath(
os.path.join(
global_config.original_path,
os.pardir,
"dashboard_components.xml",
)
)
custom_components = get_custom_xml_content(dashboard_components_path)

content = generate_dashboard_content(addon_name, panel_names, custom_components)
with open(dashboard_xml_file_path, "w") as dashboard_xml_file:
dashboard_xml_file.write(content)


def get_custom_xml_content(xml_path: str) -> str:
custom_xml = load_custom_xml(xml_path)
root = custom_xml.getroot()
if root.tag != "custom-dashboard":
logger.error(
f"File {xml_path} has invalid root tag '{root.tag}'. "
f"Valid root tag is 'custom-dashboard'"
)
sys.exit(1)

custom_components = ""
for it, child in enumerate(root, 1):
if child.tag == "row":
custom_components += ElementTree.tostring(child).decode()
else:
logger.error(
f"In file {xml_path}, there should only be tags 'row' under the root tag. "
f"Child tag no.{it} has invalid name '{child.tag}'."
)
sys.exit(1)

if not custom_components:
logger.error(
f"Custom dashboard page set in globalConfig.json but custom content not found. "
f"Please verify if file {xml_path} has a proper structure "
f"(see https://splunk.github.io/addonfactory-ucc-generator/dashboard/)"
)
sys.exit(1)
return custom_components


def load_custom_xml(xml_path: str) -> ElementTree:
try:
custom_xml = ElementTree.parse(xml_path)
except FileNotFoundError:
logger.error(
f"Custom dashboard page set in globalConfig.json but "
f"file {xml_path} not found"
)
sys.exit(1)
except ElementTree.ParseError:
logger.error(f"{xml_path} it's not a valid xml file")
sys.exit(1)
return custom_xml
Loading

0 comments on commit 7fe3d58

Please sign in to comment.