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
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@
{
"id": "surveillanceCategories",
"name": "Surveillance Categories",
"url": "admin/categories.htm",
"url": "ui/index.html#/admin/surveillance-categories",
"locationMatch": "categories",
"roles": null
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ public void testMenuEntries() throws Exception {

// Administration Menu
clickMenuItem("Administration", "Surveillance Categories");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[@class='card-header']/span[text()='Surveillance Categories']")));
// now a /ui (Vue) page rather than the legacy JSP card header
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[@id='app']//h1[@class='page-title' and normalize-space(text())='Surveillance Categories']")));

clickMenuItem("Administration", "Configure Thresholds");
wait.until(ExpectedConditions.visibilityOfElementLocated(By.xpath("//div[@class='card-header']/span[text()='Threshold Configuration']")));
Expand Down
84 changes: 84 additions & 0 deletions ui/src/components/ManageCategories/CategoriesHelpPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<template>
<TogglePanel
:collapsed="collapsed"
class="categories-help-panel"
data-test="categories-help-panel"
@update:collapsed="(value: boolean) => (collapsed = value)"
>
<template #header>
<span class="panel-header">
<OnmsIcon :icon="Help" class="help-icon" />
About Surveillance Categories
</span>
</template>
<div class="help-columns">
<div class="help-section">
<div class="section-title">What surveillance categories are for</div>
<p>
A surveillance category is a named grouping of nodes used across
OpenNMS for filtering, dashboards, notifications, and the surveillance
view. A node can belong to any number of categories, and group
authorizations can restrict which categories a user group sees.
</p>
</div>
<div class="help-section">
<div class="section-title">How to use this page</div>
<p>
<strong>Add New Category</strong> creates a category with an optional
description; <strong>Edit</strong> changes the description (the name is
the identifier and cannot be changed). <strong>Manage Nodes</strong>
assigns and unassigns nodes. <strong>Delete</strong> removes the
category — filters or authorizations that reference it by name stop
matching, so review them afterward. The same operations are available
to tooling through the <code>/rest/categories</code> REST API.
</p>
</div>
</div>
</TogglePanel>
</template>

<script setup lang="ts">
import { OnmsIcon } from '@opennms/onms-ui'
import { ref } from 'vue'

import Help from '@/components/icons/action/Help.vue'
import TogglePanel from '@/components/Common/TogglePanel.vue'

const collapsed = ref(true)
</script>

<style lang="scss" scoped>
.panel-header {
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-weight: 600;

.help-icon {
color: var(--p-primary-color);
}
}

.help-columns {
display: flex;
gap: 2.5rem;
flex-wrap: wrap;

.help-section {
flex: 1;
min-width: 320px;
}
}

.section-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.5rem;
}

p {
margin: 0 0 0.75rem 0;
font-size: 0.9rem;
line-height: 1.5;
}
</style>
164 changes: 164 additions & 0 deletions ui/src/components/ManageCategories/CategoriesTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
<template>
<TableCard class="categories-table">
<div class="header">
<div class="card-title">Surveillance Categories</div>
<OnmsButton
variant="outlined"
data-test="add-category-button"
@click="openEditor(null)"
>
<OnmsIcon :icon="Add" /> Add New Category
</OnmsButton>
</div>

<OnmsTable
:value="store.categories"
:paginator="store.categories.length > 0"
dataKey="name"
sortField="name"
:sortOrder="1"
:rows="10"
:rowsPerPageOptions="[10, 20, 50, 100]"
class="data-table"
data-test="categories-table"
>
<template #empty>
<EmptyList
:content="store.loadError ? errorListContent : emptyListContent"
data-test="empty-list"
/>
</template>
<OnmsColumn field="name" header="Name" sortable />
<OnmsColumn header="Description">
<template #body="{ data }">{{ data.description || '-' }}</template>
</OnmsColumn>
<OnmsColumn header="Actions">
<template #body="{ data }">
<div class="action-container">
<OnmsButton
variant="text"
label="Manage Nodes"
:aria-label="`Manage nodes in ${data.name}`"
data-test="manage-nodes-button"
@click="openNodes(data)"
/>
<OnmsButton
variant="text"
label="Edit"
:aria-label="`Edit ${data.name}`"
data-test="edit-category-button"
@click="openEditor(data)"
/>
<OnmsButton
variant="text"
label="Delete"
severity="danger"
:aria-label="`Delete ${data.name}`"
data-test="delete-category-button"
@click="askDelete(data)"
/>
</div>
</template>
</OnmsColumn>
</OnmsTable>
</TableCard>

<CategoryEditorDialog v-model:visible="showEditor" :category="categoryToEdit" />
<CategoryNodesDialog v-model:visible="showNodes" :categoryId="actionCategoryId" :categoryName="actionCategoryName" />
<OnmsConfirmationDialog
:visible="showDeleteConfirmation"
title="Delete Surveillance Category"
actionButtonText="Delete"
@ok="confirmDelete"
@cancel="cancelDelete"
>
<template #content>
<p>
Are you sure you want to delete the surveillance category
<strong>{{ categoryToDelete?.name }}</strong>? Nodes will lose this
category, and any group authorizations or filters that reference it by
name will no longer match. This action cannot be undone.
</p>
</template>
</OnmsConfirmationDialog>
</template>

<script setup lang="ts">
import { ref } from 'vue'

import { OnmsButton, OnmsColumn, OnmsConfirmationDialog, OnmsIcon, OnmsTable } from '@opennms/onms-ui'

import Add from '@/components/icons/action/Add.vue'
import EmptyList from '@/components/Common/EmptyList.vue'
import TableCard from '@/components/Common/TableCard.vue'
import CategoryEditorDialog from '@/components/ManageCategories/CategoryEditorDialog.vue'
import CategoryNodesDialog from '@/components/ManageCategories/CategoryNodesDialog.vue'
import { useCategoryAdminStore } from '@/stores/categoryAdminStore'
import { AdminCategory } from '@/services/categoryAdminService'

const store = useCategoryAdminStore()

const showEditor = ref(false)
const categoryToEdit = ref<AdminCategory | null>(null)
const showNodes = ref(false)
const actionCategoryName = ref('')
const actionCategoryId = ref<number | null>(null)
const showDeleteConfirmation = ref(false)
const categoryToDelete = ref<AdminCategory | null>(null)

const emptyListContent = { msg: 'No surveillance categories found.' }
const errorListContent = { msg: 'Could not load surveillance categories. Please retry.' }

const openEditor = (category: AdminCategory | null) => {
categoryToEdit.value = category
showEditor.value = true
}

const openNodes = (category: AdminCategory) => {
actionCategoryName.value = category.name
actionCategoryId.value = category.id ?? null
showNodes.value = true
}

const askDelete = (category: AdminCategory) => {
categoryToDelete.value = category
showDeleteConfirmation.value = true
}

const confirmDelete = async () => {
if (categoryToDelete.value) {
await store.deleteCategory(categoryToDelete.value.name)
}
showDeleteConfirmation.value = false
categoryToDelete.value = null
}

const cancelDelete = () => {
showDeleteConfirmation.value = false
categoryToDelete.value = null
}
</script>

<style lang="scss" scoped>
.categories-table {
padding: 25px;
}

.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}

.card-title {
font-size: 1.1rem;
font-weight: 600;
}

.action-container {
display: flex;
gap: 0.25rem;
flex-wrap: wrap;
}
</style>
Loading
Loading