Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for custom dashboards #979

Merged
merged 7 commits into from
Jan 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading