diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/DashboardRestService.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/DashboardRestService.java new file mode 100644 index 000000000000..c7e4a252d922 --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/DashboardRestService.java @@ -0,0 +1,116 @@ +/* + * 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.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 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 Map layout) { + 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> types = serviceTypeDao.findAll().stream() + .sorted(Comparator.comparing(OnmsServiceType::getName, String.CASE_INSENSITIVE_ORDER)) + .map(t -> { + final Map 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(); + } + } +} diff --git a/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/DashboardRestApi.java b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/DashboardRestApi.java new file mode 100644 index 000000000000..83678afaa20a --- /dev/null +++ b/opennms-webapp-rest/src/main/java/org/opennms/web/rest/v2/api/DashboardRestApi.java @@ -0,0 +1,77 @@ +/* + * 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.MediaType; +import javax.ws.rs.core.Response; + +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(Map 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(); +} diff --git a/opennms-webapp/src/main/webapp/WEB-INF/web.xml b/opennms-webapp/src/main/webapp/WEB-INF/web.xml index feb0250c4d0a..b0207e9387fc 100644 --- a/opennms-webapp/src/main/webapp/WEB-INF/web.xml +++ b/opennms-webapp/src/main/webapp/WEB-INF/web.xml @@ -157,7 +157,7 @@ Sets the header value. value - accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=() + accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(self), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=() diff --git a/opennms-webapp/src/main/webapp/includes/quicksearch-box.jsp b/opennms-webapp/src/main/webapp/includes/quicksearch-box.jsp index a9f50cf330e2..9c8f964679c1 100644 --- a/opennms-webapp/src/main/webapp/includes/quicksearch-box.jsp +++ b/opennms-webapp/src/main/webapp/includes/quicksearch-box.jsp @@ -83,5 +83,8 @@ + diff --git a/ui/package.json b/ui/package.json index c7eb3ea5fa35..cd23198de4a7 100644 --- a/ui/package.json +++ b/ui/package.json @@ -65,11 +65,11 @@ "@featherds/toggle-button": "^0.12.43", "@featherds/tooltip": "0.12.43", "@featherds/utils": "0.12.43", - "@primevue/themes": "^4.5.4", "@fortawesome/fontawesome-svg-core": "^6.7.2", "@fortawesome/free-regular-svg-icons": "^6.7.2", "@fortawesome/free-solid-svg-icons": "^6.7.2", "@fortawesome/vue-fontawesome": "^3.0.6", + "@primevue/themes": "^4.5.4", "@vueuse/core": "^9.13.0", "ace-builds": "^1.32.6", "axios": "^1.15.0", @@ -81,6 +81,7 @@ "date-fns-tz": "^3.2.0", "dotenv": "^17.2.3", "fast-xml-parser": "^5.5.11", + "grid-layout-plus": "^1.1.1", "ip-regex": "^5.0.0", "is-ip": "^5.0.1", "is-valid-domain": "^0.1.6", diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index f9f3a75dc6ee..a8f9cbd3ab7b 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -197,6 +197,9 @@ importers: fast-xml-parser: specifier: ^5.5.11 version: 5.6.0 + grid-layout-plus: + specifier: ^1.1.1 + version: 1.1.1(vue@3.5.33(typescript@5.4.5)) ip-regex: specifier: ^5.0.0 version: 5.0.0 @@ -717,6 +720,15 @@ packages: '@featherds/utils@0.12.44': resolution: {integrity: sha512-ic6JDH7mL5fYkqDD7Vu/caE+CbshlBDyt5clGG/0GxtYFU462RRuiEAphuyDlfA8IggHMIyQK+XUAqSJX1kkYA==} + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + '@fortawesome/fontawesome-common-types@6.7.2': resolution: {integrity: sha512-Zs+YeHUC5fkt7Mg1l6XTniei3k4bwG/yo3iFUtZWd/pMx9g3fdvkSK9E0FOC+++phXOka78uJcYb8JaFkW52Xg==} engines: {node: '>=6'} @@ -755,6 +767,9 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@interactjs/types@1.10.27': + resolution: {integrity: sha512-BUdv0cvs4H5ODuwft2Xp4eL8Vmi3LcihK42z0Ft/FbVJZoRioBsxH+LlsBdK4tAie7PqlKGy+1oyOncu1nQ6eA==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -762,6 +777,9 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@juggle/resize-observer@3.4.0': + resolution: {integrity: sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==} + '@lit-labs/ssr-dom-shim@1.5.1': resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} @@ -815,42 +833,36 @@ packages: engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm-musl@2.5.6': resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} engines: {node: '>= 10.0.0'} cpu: [arm] os: [linux] - libc: [musl] '@parcel/watcher-linux-arm64-glibc@2.5.6': resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-arm64-musl@2.5.6': resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} engines: {node: '>= 10.0.0'} cpu: [arm64] os: [linux] - libc: [musl] '@parcel/watcher-linux-x64-glibc@2.5.6': resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [glibc] '@parcel/watcher-linux-x64-musl@2.5.6': resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} engines: {node: '>= 10.0.0'} cpu: [x64] os: [linux] - libc: [musl] '@parcel/watcher-win32-arm64@2.5.6': resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} @@ -955,79 +967,66 @@ packages: resolution: {integrity: sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==} cpu: [arm] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.60.1': resolution: {integrity: sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==} cpu: [arm] os: [linux] - libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.60.1': resolution: {integrity: sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==} cpu: [arm64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.60.1': resolution: {integrity: sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==} cpu: [arm64] os: [linux] - libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.60.1': resolution: {integrity: sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==} cpu: [loong64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.60.1': resolution: {integrity: sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==} cpu: [loong64] os: [linux] - libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.60.1': resolution: {integrity: sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==} cpu: [ppc64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.60.1': resolution: {integrity: sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==} cpu: [ppc64] os: [linux] - libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.60.1': resolution: {integrity: sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==} cpu: [riscv64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.60.1': resolution: {integrity: sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==} cpu: [riscv64] os: [linux] - libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.60.1': resolution: {integrity: sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==} cpu: [s390x] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.60.1': resolution: {integrity: sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==} cpu: [x64] os: [linux] - libc: [glibc] '@rollup/rollup-linux-x64-musl@4.60.1': resolution: {integrity: sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==} cpu: [x64] os: [linux] - libc: [musl] '@rollup/rollup-openbsd-x64@4.60.1': resolution: {integrity: sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==} @@ -1400,6 +1399,14 @@ packages: resolution: {integrity: sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==} engines: {node: ^16.0.0 || >=18.0.0} + '@vexip-ui/hooks@2.9.4': + resolution: {integrity: sha512-dGUiBAeHIsnSVigGSPHcuHBVqrSGW8LV+zGohvOpBfXs8Ynn5ZcSmybIWJ3G826NsicPu9rqwcJG8uvSgG4k4Q==} + peerDependencies: + vue: ^3.2.25 + + '@vexip-ui/utils@2.16.4': + resolution: {integrity: sha512-KX+Q4EsuwDp6ZlRJ7OAkiYxu52D5CVM8zpqQz/FXYV+JUtzl9T3dvxgtA8gQ0wm5Sh/xT6jp8Wo4X7tLAzRh/A==} + '@vitejs/plugin-vue@5.2.4': resolution: {integrity: sha512-7Yx/SXSOcQq5HiiV3orevHUFn+pmMB4cgbEkDYgnkUWb0WfeQ/wa2yFv6D5ICiCQOVpjA7vYDXrC7AGO8yjDHA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -2239,6 +2246,11 @@ packages: graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} + grid-layout-plus@1.1.1: + resolution: {integrity: sha512-7CWehJubrVC8Ps5QFUlnDsp0kiREvKfi3Pdjp21EyY8BNzSusqI3Utcxvu1Y9UUKe3YExvbhJzIxHK6rorbRaQ==} + peerDependencies: + vue: ^3.0.0 + hammerjs@2.0.8: resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} engines: {node: '>=0.8.0'} @@ -2295,6 +2307,9 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + interactjs@1.10.27: + resolution: {integrity: sha512-y/8RcCftGAF24gSp76X2JS3XpHiUvDQyhF8i7ujemBz77hwiHDuJzftHx7thY8cxGogwGiPJ+o97kWB6eAXnsA==} + internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} @@ -3765,6 +3780,17 @@ snapshots: '@featherds/utils@0.12.44': {} + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/utils@0.2.11': {} + '@fortawesome/fontawesome-common-types@6.7.2': {} '@fortawesome/fontawesome-svg-core@6.7.2': @@ -3795,6 +3821,8 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@interactjs/types@1.10.27': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -3806,6 +3834,8 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.5': {} + '@juggle/resize-observer@3.4.0': {} + '@lit-labs/ssr-dom-shim@1.5.1': {} '@lit/reactive-element@2.1.2': @@ -4715,6 +4745,15 @@ snapshots: '@typescript-eslint/types': 6.21.0 eslint-visitor-keys: 3.4.3 + '@vexip-ui/hooks@2.9.4(vue@3.5.33(typescript@5.4.5))': + dependencies: + '@floating-ui/dom': 1.7.6 + '@juggle/resize-observer': 3.4.0 + '@vexip-ui/utils': 2.16.4 + vue: 3.5.33(typescript@5.4.5) + + '@vexip-ui/utils@2.16.4': {} + '@vitejs/plugin-vue@5.2.4(vite@6.4.2(@types/node@25.6.0)(sass@1.99.0))(vue@3.5.33(typescript@5.4.5))': dependencies: vite: 6.4.2(@types/node@25.6.0)(sass@1.99.0) @@ -5669,6 +5708,13 @@ snapshots: graphemer@1.4.0: {} + grid-layout-plus@1.1.1(vue@3.5.33(typescript@5.4.5)): + dependencies: + '@vexip-ui/hooks': 2.9.4(vue@3.5.33(typescript@5.4.5)) + '@vexip-ui/utils': 2.16.4 + interactjs: 1.10.27 + vue: 3.5.33(typescript@5.4.5) + hammerjs@2.0.8: {} happy-dom@9.20.3: @@ -5715,6 +5761,10 @@ snapshots: ini@1.3.8: {} + interactjs@1.10.27: + dependencies: + '@interactjs/types': 1.10.27 + internmap@2.0.3: {} ip-regex@5.0.0: {} diff --git a/ui/src/components/Dashboard/DashboardFilterControl.vue b/ui/src/components/Dashboard/DashboardFilterControl.vue new file mode 100644 index 000000000000..2864382955d4 --- /dev/null +++ b/ui/src/components/Dashboard/DashboardFilterControl.vue @@ -0,0 +1,146 @@ + + + + + + + + diff --git a/ui/src/components/Dashboard/DashboardGrid.vue b/ui/src/components/Dashboard/DashboardGrid.vue new file mode 100644 index 000000000000..59b130468746 --- /dev/null +++ b/ui/src/components/Dashboard/DashboardGrid.vue @@ -0,0 +1,222 @@ + + + + + + + + diff --git a/ui/src/components/Dashboard/DashboardToolbar.vue b/ui/src/components/Dashboard/DashboardToolbar.vue new file mode 100644 index 000000000000..bf2ff265f6ed --- /dev/null +++ b/ui/src/components/Dashboard/DashboardToolbar.vue @@ -0,0 +1,230 @@ + + + + + + + + diff --git a/ui/src/components/Dashboard/PanelFrame.vue b/ui/src/components/Dashboard/PanelFrame.vue new file mode 100644 index 000000000000..6ae0836977bc --- /dev/null +++ b/ui/src/components/Dashboard/PanelFrame.vue @@ -0,0 +1,274 @@ + + + + + + + + diff --git a/ui/src/components/Dashboard/PanelOptionsDialog.vue b/ui/src/components/Dashboard/PanelOptionsDialog.vue new file mode 100644 index 000000000000..b8cc389ddf89 --- /dev/null +++ b/ui/src/components/Dashboard/PanelOptionsDialog.vue @@ -0,0 +1,367 @@ + + + + + + + + diff --git a/ui/src/components/Dashboard/defaultLayout.ts b/ui/src/components/Dashboard/defaultLayout.ts new file mode 100644 index 000000000000..a4b49543c3b9 --- /dev/null +++ b/ui/src/components/Dashboard/defaultLayout.ts @@ -0,0 +1,70 @@ +/// +/// 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. +/// + +import { type DashboardLayout, type DashboardPanel, TimeframePreset } from '@/types/dashboard' + +const panel = (id: string, type: string, x: number, y: number, w: number, h: number): DashboardPanel => ({ + id, + type, + x, + y, + w, + h, + collapsed: false, + titleOverride: null, + filterOverride: null, + timeframeOverride: null, + refreshSeconds: null, + options: {} +}) + +// Built-in default used until the backend (NMS-19851) persists a layout, and +// as the "reset" target. A factory (not a constant) so each load gets a fresh, +// independently-mutable object. +export const createDefaultLayout = (): DashboardLayout => ({ + scope: 'SYSTEM', + version: 1, + refresh: { seconds: 120, paused: false }, + globalFilter: { surveillanceCategories: [], ipMatch: null }, + globalTimeframe: { preset: TimeframePreset.Last24h, from: null, to: null }, + // Three columns with a wider middle (3 / 6 / 3 of 12), matching the legacy + // homepage column order/positions for easy side-by-side comparison: + // Left : Situations, Nodes w/ Alarms, Service Outages, Business Services, + // Applications, News Feed + // Center : Status Overview, Availability (24h), Regional Status map + // Right : Notifications, Resource Graphs, KSC Reports, Quick Search + panels: [ + panel('situations-1', 'pending-situations', 0, 0, 3, 3), + panel('nodes-1', 'nodes-with-alarms', 0, 3, 3, 5), + panel('outages-1', 'service-outages', 0, 8, 3, 3), + panel('bsm-1', 'business-services', 0, 11, 3, 3), + panel('apps-1', 'applications', 0, 14, 3, 3), + panel('news-1', 'newsfeed', 0, 17, 3, 5), + panel('status-1', 'status-overview', 3, 0, 6, 5), + panel('availability-1', 'availability', 3, 5, 6, 4), + panel('map-1', 'regional-map', 3, 9, 6, 7), + panel('notif-1', 'notifications', 9, 0, 3, 3), + panel('graphs-1', 'resource-graphs', 9, 3, 3, 2), + panel('ksc-1', 'ksc-reports', 9, 5, 3, 2), + panel('quicksearch-1', 'quick-search', 9, 7, 3, 5) + ] +}) diff --git a/ui/src/components/Dashboard/panels/ApplicationsPanel.vue b/ui/src/components/Dashboard/panels/ApplicationsPanel.vue new file mode 100644 index 000000000000..33fbea56bf77 --- /dev/null +++ b/ui/src/components/Dashboard/panels/ApplicationsPanel.vue @@ -0,0 +1,38 @@ + + + + + + diff --git a/ui/src/components/Dashboard/panels/AvailabilityPanel.vue b/ui/src/components/Dashboard/panels/AvailabilityPanel.vue new file mode 100644 index 000000000000..70ee2477ce10 --- /dev/null +++ b/ui/src/components/Dashboard/panels/AvailabilityPanel.vue @@ -0,0 +1,160 @@ + + + + + + + + diff --git a/ui/src/components/Dashboard/panels/BusinessServicesPanel.vue b/ui/src/components/Dashboard/panels/BusinessServicesPanel.vue new file mode 100644 index 000000000000..267a87335f95 --- /dev/null +++ b/ui/src/components/Dashboard/panels/BusinessServicesPanel.vue @@ -0,0 +1,38 @@ + + + + + + diff --git a/ui/src/components/Dashboard/panels/HtmlContentPanel.vue b/ui/src/components/Dashboard/panels/HtmlContentPanel.vue new file mode 100644 index 000000000000..2c0d5472a422 --- /dev/null +++ b/ui/src/components/Dashboard/panels/HtmlContentPanel.vue @@ -0,0 +1,66 @@ + + + +