Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
32989cb
NMS-19851: Configurable system-wide dashboard (milestones 1-2 + persi…
joseanesONMS Jun 2, 2026
ae53494
NMS-19851: Add first real dashboard panels (notifications, status ove…
joseanesONMS Jun 2, 2026
1b295a3
NMS-19851: Status Overview outages donut + News Feed panel
joseanesONMS Jun 2, 2026
9248f35
NMS-19851: Add Pending Situations and Service Outages panels
joseanesONMS Jun 2, 2026
3110b4b
NMS-19851: Add Regional Status map panel
joseanesONMS Jun 3, 2026
5099b1d
NMS-19851: Wider middle column in default dashboard layout
joseanesONMS Jun 3, 2026
f1ec789
NMS-19851: Add Availability (24h) panel
joseanesONMS Jun 3, 2026
ce031da
NMS-19851: Add Business Services and Applications (with pending alarm…
joseanesONMS Jun 3, 2026
aaf9c93
NMS-19851: Search widgets, original-layout order, toolbar tooltips + …
joseanesONMS Jun 3, 2026
4263de1
NMS-19851: Dashboard panel polish (map, donuts, panel height, double …
joseanesONMS Jun 3, 2026
fd21bbe
NMS-19851: Fix fullscreen, footer overlap, overlay font, donut resize
joseanesONMS Jun 3, 2026
0a16e9e
NMS-19851: Per-panel options dialog, fixed/auto height, Notes + HTML …
joseanesONMS Jun 3, 2026
d618d94
NMS-19851: Fix panel overlap from auto-height (manual vertical compac…
joseanesONMS Jun 3, 2026
7402835
NMS-19851: Fix fixed-panel height chain, internal scroll vs footer, H…
joseanesONMS Jun 3, 2026
ec43475
NMS-19851: Auto-height panels shrink to content (min-h=1, no reserved…
joseanesONMS Jun 3, 2026
d9dc29d
NMS-19851: Add Top N panel (rank entities by a chosen KPI over the ti…
joseanesONMS Jun 3, 2026
78a0520
NMS-19851: TopN measurements use a normal-resolution step (fix NaN)
joseanesONMS Jun 3, 2026
baf08a7
NMS-19851: Fix auto-height measurement + dark-mode panel theming
joseanesONMS Jun 3, 2026
0d69eb4
NMS-19851: Tighten panel padding + compact columns on mount (less whi…
joseanesONMS Jun 3, 2026
3765408
NMS-19851: Real compaction (fresh refs), severity-shading option, leg…
joseanesONMS Jun 3, 2026
83140aa
NMS-19851: Auto-height actually applies (always reassign) + service-t…
joseanesONMS Jun 3, 2026
f16b44f
NMS-19851: Pixel-perfect panel heights, panel/category deep-links, de…
joseanesONMS Jun 3, 2026
e07f093
NMS-19851: BSM title deep-link + readable donut legend in dark mode
joseanesONMS Jun 3, 2026
d4ac2d5
NMS-19851: Metric Chart panel — line chart of one entity/metric over …
joseanesONMS Jun 11, 2026
fe94443
NMS-19851: Hide Sample Panel from the Add Panel picker
joseanesONMS Jun 11, 2026
7b1c65b
NMS-19851: 'Reset to default' in edit mode — restore the factory layout
joseanesONMS Jun 11, 2026
955b686
NMS-19851: Status Overview donut slices deep-link in context (legacy …
joseanesONMS Jun 11, 2026
8919f53
Merge remote-tracking branch 'origin/develop' into HEAD
joseanesONMS Jul 31, 2026
b748218
NMS-19851: adapt the dashboard to current develop
joseanesONMS Jul 31, 2026
5108243
NMS-19851: dashboard test suite and admin-only layout writes
joseanesONMS Jul 31, 2026
3139a2d
NMS-19851: fix adversarial review findings on the dashboard
joseanesONMS Jul 31, 2026
273054c
NMS-19851: move timeframeRange into the framework timeframe helper
joseanesONMS Jul 31, 2026
3be6fc5
NMS-19851: configurable dashboard framework (base)
joseanesONMS Jul 31, 2026
321cbbb
NMS-19851: resolve dashboard filter categories via the node search th…
joseanesONMS Jul 31, 2026
a5de37d
NMS-19851: drop accidentally committed ui/node_modules symlink
joseanesONMS Jul 31, 2026
4a74b63
NMS-19851: onms-ui seam migration, auto-fit layout option, auto-heigh…
joseanesONMS Aug 3, 2026
626d9ea
Merge remote-tracking branch 'origin/develop' into jira/NMS-19851-das…
joseanesONMS Aug 4, 2026
4802bff
NMS-19851: satisfy arrow-parens lint on listPanelDefinitions
joseanesONMS Aug 4, 2026
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
Binary file added opennms-webapp-rest/127.0.0.1.tm4.epoch
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Licensed to The OpenNMS Group, Inc (TOG) under one or more
* contributor license agreements. See the LICENSE.md file
* distributed with this work for additional information
* regarding copyright ownership.
*
* TOG licenses this file to You under the GNU Affero General
* Public License Version 3 (the "License") or (at your option)
* any later version. You may not use this file except in
* compliance with the License. You may obtain a copy of the
* License at:
*
* https://www.gnu.org/licenses/agpl-3.0.txt
*
* 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.
*/
package org.opennms.web.rest.v2;

import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

import org.codehaus.jackson.map.ObjectMapper;
import org.opennms.features.distributed.kvstore.api.JsonStore;
import org.opennms.netmgt.dao.api.ServiceTypeDao;
import org.opennms.netmgt.model.OnmsServiceType;
import org.opennms.web.rest.v2.api.DashboardRestApi;
import org.slf4j.Logger;
import org.opennms.web.api.Authentication;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
* Implementation of {@link DashboardRestApi}. Persists the single system-wide
* dashboard layout JSON document in the {@link JsonStore} (the same key-value
* store used by e.g. ResourceRestService), keyed by a fixed context/key.
*/
@Service
public class DashboardRestService implements DashboardRestApi {
private static final Logger LOG = LoggerFactory.getLogger(DashboardRestService.class);

// One document for the whole system. Future per-user / named dashboards would
// vary the key while keeping the same store.
private static final String CONTEXT = "dashboard";
private static final String KEY = "system";

@Autowired
private JsonStore jsonStore;

@Autowired
private ServiceTypeDao serviceTypeDao;

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public Response getSystemLayout() {
final Optional<String> stored = jsonStore.get(KEY, CONTEXT);
if (stored.isEmpty()) {
return Response.status(Status.NOT_FOUND).build();
}
try {
final Map<?, ?> layout = objectMapper.readValue(stored.get(), Map.class);
return Response.ok(layout).build();
} catch (final Exception e) {
LOG.error("Failed to parse stored system dashboard layout", e);
return Response.serverError().build();
}
}

@Override
public Response updateSystemLayout(final javax.ws.rs.core.SecurityContext securityContext, final Map<String, Object> layout) {
// the layout is system-wide: every user reads it, only admins write it
if (securityContext == null || !securityContext.isUserInRole(Authentication.ROLE_ADMIN)) {
return Response.status(Status.FORBIDDEN).entity("Saving the system dashboard requires the admin role.").build();
}
if (layout == null) {
return Response.status(Status.BAD_REQUEST).build();
}
try {
final String json = objectMapper.writeValueAsString(layout);
jsonStore.put(KEY, json, CONTEXT);
return Response.noContent().build();
} catch (final Exception e) {
LOG.error("Failed to store system dashboard layout", e);
return Response.serverError().build();
}
}

@Override
@Transactional(readOnly = true)
public Response getServiceTypes() {
try {
final List<Map<String, Object>> types = serviceTypeDao.findAll().stream()
.sorted(Comparator.comparing(OnmsServiceType::getName, String.CASE_INSENSITIVE_ORDER))
.map(t -> {
final Map<String, Object> m = new LinkedHashMap<>();
m.put("id", t.getId());
m.put("name", t.getName());
return m;
})
.collect(Collectors.toList());
return Response.ok(types).build();
} catch (final Exception e) {
LOG.error("Failed to list service types", e);
return Response.serverError().build();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Licensed to The OpenNMS Group, Inc (TOG) under one or more
* contributor license agreements. See the LICENSE.md file
* distributed with this work for additional information
* regarding copyright ownership.
*
* TOG licenses this file to You under the GNU Affero General
* Public License Version 3 (the "License") or (at your option)
* any later version. You may not use this file except in
* compliance with the License. You may obtain a copy of the
* License at:
*
* https://www.gnu.org/licenses/agpl-3.0.txt
*
* 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.
*/
package org.opennms.web.rest.v2.api;

import java.util.Map;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;

/**
* REST API for the configurable system-wide dashboard layout (NMS-19851).
*
* A single JSON layout document is stored for the whole system. The document is
* opaque to the backend (the UI owns its shape); it is persisted verbatim.
*/
@Path("dashboard")
@Tag(name = "Dashboard", description = "System-wide dashboard layout API V2")
public interface DashboardRestApi {

@GET
@Path("system")
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Get the system-wide dashboard layout.",
description = "Returns the stored system-wide dashboard layout document, or 404 if none has been saved yet.",
operationId = "getSystemDashboardLayout"
)
Response getSystemLayout();

@PUT
@Path("system")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "Save the system-wide dashboard layout.",
description = "Replaces the stored system-wide dashboard layout document.",
operationId = "updateSystemDashboardLayout"
)
Response updateSystemLayout(@Context SecurityContext securityContext, Map<String, Object> layout);

@GET
@Path("service-types")
@Produces(MediaType.APPLICATION_JSON)
@Operation(
summary = "List monitored service types (id + name).",
description = "Used by the dashboard Quick Search 'Providing service' control.",
operationId = "getDashboardServiceTypes"
)
Response getServiceTypes();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/*
* Licensed to The OpenNMS Group, Inc (TOG) under one or more
* contributor license agreements. See the LICENSE.md file
* distributed with this work for additional information
* regarding copyright ownership.
*
* TOG licenses this file to You under the GNU Affero General
* Public License Version 3 (the "License") or (at your option)
* any later version. You may not use this file except in
* compliance with the License. You may obtain a copy of the
* License at:
*
* https://www.gnu.org/licenses/agpl-3.0.txt
*
* 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.
*/
package org.opennms.web.rest.v2;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;

import javax.ws.rs.core.MediaType;

import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.opennms.core.test.MockLogAppender;
import org.opennms.core.test.OpenNMSJUnit4ClassRunner;
import org.opennms.core.test.db.annotations.JUnitTemporaryDatabase;
import org.opennms.core.test.rest.AbstractSpringJerseyRestTestCase;
import org.opennms.netmgt.dao.DatabasePopulator;
import org.opennms.test.JUnitConfigurationEnvironment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.web.WebAppConfiguration;

@RunWith(OpenNMSJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {
"classpath:/META-INF/opennms/applicationContext-soa.xml",
"classpath:/META-INF/opennms/applicationContext-commonConfigs.xml",
"classpath:/META-INF/opennms/applicationContext-minimal-conf.xml",
"classpath:/META-INF/opennms/applicationContext-dao.xml",
"classpath:/META-INF/opennms/applicationContext-mockConfigManager.xml",
"classpath*:/META-INF/opennms/component-service.xml",
"classpath*:/META-INF/opennms/component-dao.xml",
"classpath:/META-INF/opennms/applicationContext-databasePopulator.xml",
"classpath:/META-INF/opennms/mockEventIpcManager.xml",
"file:src/main/webapp/WEB-INF/applicationContext-svclayer.xml",
"file:src/main/webapp/WEB-INF/applicationContext-cxf-common.xml",
"classpath:/META-INF/opennms/applicationContext-postgresJsonStore.xml",
"classpath:/applicationContext-rest-test.xml"
})
@JUnitConfigurationEnvironment(systemProperties = "org.opennms.timeseries.strategy=integration")
@JUnitTemporaryDatabase
public class DashboardRestServiceIT extends AbstractSpringJerseyRestTestCase {

@Autowired
private DatabasePopulator m_databasePopulator;

public DashboardRestServiceIT() {
super(CXF_REST_V2_CONTEXT_PATH);
}

private static boolean s_populated = false;

@Override
protected void afterServletStart() {
MockLogAppender.setupLogging();
// the temp database is shared across the methods of this class;
// repopulating trips unique constraints
if (!s_populated) {
m_databasePopulator.populateDatabase();
s_populated = true;
}
}

@Test
public void testLayoutLifecycle() throws Exception {
// the document round-trips as nested JSON, not an escaped string
final String layout = "{\"scope\":\"SYSTEM\",\"version\":1,"
+ "\"panels\":[{\"id\":\"a\",\"type\":\"notes\",\"x\":0,\"y\":0,\"w\":6,\"h\":200,"
+ "\"options\":{\"text\":\"hello\"}}],"
+ "\"refresh\":{\"seconds\":120,\"paused\":false}}";
sendData(PUT, MediaType.APPLICATION_JSON, "/dashboard/system", layout, 204);

final JSONObject read = new JSONObject(getJson("/dashboard/system"));
assertEquals("SYSTEM", read.getString("scope"));
assertEquals(1, read.getJSONArray("panels").length());
assertEquals("hello", read.getJSONArray("panels").getJSONObject(0).getJSONObject("options").getString("text"));

// saving again replaces the document
sendData(PUT, MediaType.APPLICATION_JSON, "/dashboard/system",
"{\"scope\":\"SYSTEM\",\"version\":1,\"panels\":[],\"refresh\":{\"seconds\":60,\"paused\":true}}", 204);
final JSONObject replaced = new JSONObject(getJson("/dashboard/system"));
assertEquals(0, replaced.getJSONArray("panels").length());
assertEquals(60, replaced.getJSONObject("refresh").getInt("seconds"));
}

@Test
public void testNullBodyRejected() throws Exception {
sendData(PUT, MediaType.APPLICATION_JSON, "/dashboard/system", "null", 400);
}

@Test
public void testServiceTypesSortedCaseInsensitively() throws Exception {
final JSONArray types = new JSONArray(getJson("/dashboard/service-types"));
assertTrue(types.length() > 0);
String previous = null;
for (int i = 0; i < types.length(); i++) {
final JSONObject type = types.getJSONObject(i);
assertTrue(type.has("id"));
final String name = type.getString("name");
if (previous != null) {
assertTrue(previous + " <= " + name, previous.compareToIgnoreCase(name) <= 0);
}
previous = name;
}
}

@Test
public void testLayoutWriteRequiresAdmin() throws Exception {
// ensure a layout exists so the read below is a definite 200
sendData(PUT, MediaType.APPLICATION_JSON, "/dashboard/system",
"{\"scope\":\"SYSTEM\",\"panels\":[]}", 204);
setUser("nobody", new String[]{ "ROLE_USER" });
try {
// every user may read the layout, only admins may write it
sendRequest(GET, "/dashboard/system", 200);
sendData(PUT, MediaType.APPLICATION_JSON, "/dashboard/system",
"{\"scope\":\"SYSTEM\",\"panels\":[]}", 403);
} finally {
setUser("admin", new String[]{ "ROLE_ADMIN" });
}
}

private String getJson(final String url) throws Exception {
final MockHttpServletRequest request = createRequest(GET, url);
request.addHeader("Accept", MediaType.APPLICATION_JSON);
return sendRequest(request, 200);
}
}
Binary file added opennms-webapp-rest/tmlog4.log
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@

<!-- Discovery is a provisioning operation (its include/exclude URLs can read local files or fetch URLs);
restrict it to provisioning/admin roles rather than the generic ROLE_REST catch-all below -->
<!-- Dashboard layout is system-wide: reads fall through to the /api/v2/** GET
catch-all (every reader role); only admins may save it, so override just
the write. Order matters — this must precede the /api/v2/** PUT rule. -->
<intercept-url pattern="/api/v2/dashboard/**" method="PUT" access="ROLE_ADMIN" />

<intercept-url pattern="/api/v2/discovery" method="POST" access="ROLE_PROVISION,ROLE_ADMIN"/>
<intercept-url pattern="/api/v2/discovery/**" method="POST" access="ROLE_PROVISION,ROLE_ADMIN"/>

Expand Down
5 changes: 5 additions & 0 deletions opennms-webapp/src/main/webapp/includes/quicksearch-box.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -101,3 +101,8 @@ function submitNodeSearch(params) {
</form>
</div>
</div>

<%-- Standalone call-to-action below the Quick Search box, not part of it --%>
<div class="form-group" style="margin-top:0.75rem;">
<a class="btn btn-light btn-block" style="border:1px solid #ccc;" href="ui/#/dashboard">Try the new Dashboard (beta) &raquo;</a>
</div>
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"@vueuse/core": "^9.13.0",
"ace-builds": "^1.32.6",
"axios": "^1.15.0",
"grid-layout-plus": "^1.1.1",
"chart.js": "^3.9.1",
"chartjs-plugin-zoom": "^2.0.1",
"cronstrue": "^3.9.0",
Expand Down
Loading
Loading