Skip to content
Open
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
47 changes: 47 additions & 0 deletions splunklib/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
import socket
from datetime import datetime, timedelta
from time import sleep
from typing import Any
from urllib import parse

try:
Expand Down Expand Up @@ -102,6 +103,7 @@ def deprecated(message): # pyright: ignore[reportUnknownParameterType]
PATH_APPS = "apps/local/"
PATH_CAPABILITIES = "authorization/capabilities/"
PATH_CONF = "configs/conf-%s/"
PATH_DASHBOARDS = "data/ui/views/"
PATH_PROPERTIES = "properties/"
PATH_DEPLOYMENT_CLIENTS = "deployment/client/"
PATH_DEPLOYMENT_TENANTS = "deployment/tenants/"
Expand Down Expand Up @@ -481,6 +483,15 @@ def capabilities(self):
response = self.get(PATH_CAPABILITIES)
return _load_atom(response, MATCH_ENTRY_CONTENT).capabilities

@property
def dashboards(self):
"""Returns the collection of dashboards for this Splunk instance.

:return: A :class:`Dashboards` collection of :class:`Dashboard`
entities.
"""
return Dashboards(self)

@property
def event_types(self):
"""Returns the collection of event types defined in this Splunk instance.
Expand Down Expand Up @@ -3653,6 +3664,42 @@ def create(self, name, definition, **kwargs):
return Collection.create(self, name, definition=definition, **kwargs)


class Dashboard(Entity):
"""This class represents a dashboard (view) in Splunk."""

def __init__(self, service: "Service", path: str, **kwargs: Any) -> None:
Entity.__init__(self, service, path, **kwargs)

def export(self) -> str:
"""Returns the dashboard XML content.

:return: The dashboard XML definition.
:rtype: ``string``
"""
return self.content.get("eai:data", "") # pyright: ignore[reportUnknownVariableType]


class Dashboards(Collection):
"""This class represents a collection of dashboards. Retrieve this
collection using :meth:`Service.dashboards`."""

def __init__(self, service: "Service") -> None:
Collection.__init__(self, service, PATH_DASHBOARDS, item=Dashboard)

def create(self, name: str, xml: str, **kwargs: Any) -> Entity: # pyright: ignore[reportIncompatibleMethodOverride,reportImplicitOverride]
"""Creates a dashboard.

:param name: The name for the dashboard.
:type name: ``string``
:param xml: The dashboard XML definition.
:type xml: ``string``
:param kwargs: Additional arguments (optional).
:type kwargs: ``dict``
:return: The :class:`Dashboard` entity.
"""
return Collection.create(self, name, **{"eai:data": xml, **kwargs}) # pyright: ignore[reportUnknownVariableType]


class Settings(Entity):
"""This class represents configuration settings for a Splunk service.
Retrieve this collection using :meth:`Service.settings`."""
Expand Down
60 changes: 60 additions & 0 deletions tests/unit/test_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Copyright © 2011-2026 Splunk, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License"): you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

from unittest.mock import MagicMock, patch

import pytest

from splunklib.client import (
PATH_DASHBOARDS,
Collection,
Dashboard,
Dashboards,
Entity,
)


class TestDashboard:
def test_is_entity_subclass(self) -> None:
assert issubclass(Dashboard, Entity)

def test_path_constant(self) -> None:
assert PATH_DASHBOARDS == "data/ui/views/"

def test_export_returns_eai_data(self) -> None:
dashboard = MagicMock(spec=Dashboard)
dashboard.content = {"eai:data": "<dashboard><label>Test</label></dashboard>"}
assert Dashboard.export(dashboard) == "<dashboard><label>Test</label></dashboard>"

def test_export_returns_empty_when_missing(self) -> None:
dashboard = MagicMock(spec=Dashboard)
dashboard.content = {}
assert Dashboard.export(dashboard) == ""


class TestDashboards:
def test_is_collection_subclass(self) -> None:
assert issubclass(Dashboards, Collection)

@patch.object(Collection, "create")
def test_create_passes_xml_as_eai_data(self, mock_create: MagicMock) -> None:
dashboards = Dashboards.__new__(Dashboards)
xml = "<dashboard><label>Test</label></dashboard>"
Dashboards.create(dashboards, "test_dash", xml)
mock_create.assert_called_once_with(dashboards, "test_dash", **{"eai:data": xml})

def test_create_raises_on_missing_xml(self) -> None:
dashboards = Dashboards.__new__(Dashboards)
with pytest.raises(TypeError):
Dashboards.create(dashboards, "test_dash") # pyright: ignore[reportCallIssue]