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

NMS-16116: Node structure add additional queries for snmp, flows, etc. #6790

Merged
merged 2 commits into from
Oct 19, 2023
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
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
"date-fns": "^2.30.0",
"date-fns-tz": "^2.0.0",
"ip-regex": "^5.0.0",
"is-ip": "^5.0.1",
"is-valid-domain": "^0.1.6",
"leaflet": "^1.8.0",
"leaflet.markercluster": "^1.5.3",
Expand Down
137 changes: 137 additions & 0 deletions ui/src/components/Nodes/ExtendedSearchPanel.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
<template>
<div class="extended-search-container">
<FeatherSelect
label="Search Type"
:options="searchOptions"
:textProp="'title'"
v-model="currentSelection"
@update:modelValue="onSelectionUpdated"
/>

<FeatherInput
v-model="searchTerm"
@update:modelValue="onCurrentSearchUpdated"
label="Search Term"
/>
</div>
</template>

<script setup lang="ts">
import { isIP } from 'is-ip'
import { FeatherInput } from '@featherds/input'
import { FeatherSelect, ISelectItemType } from '@featherds/select'
import { useNodeQuery } from './hooks/useNodeQuery'
import { useNodeStore } from '@/stores/nodeStore'
import { useNodeStructureStore } from '@/stores/nodeStructureStore'
import { NodeQueryFilter, UpdateModelFunction } from '@/types'

const searchOptions: ISelectItemType[] = [
{ title: 'IP Address', value: 'ipAddress' },
{ title: 'SNMP Alias', value: 'snmpIfAlias' },
{ title: 'SNMP Description', value: 'snmpIfDesc' },
{ title: 'SNMP Index', value: 'snmpIfIndex' },
{ title: 'SNMP Name', value: 'snmpIfName' },
{ title: 'SNMP Type', value: 'snmpIfType' }
]
const snmpKeys = ['snmpIfAlias', 'snmpIfDesc', 'snmpIfIndex', 'snmpIfName', 'snmpIfType']

const { buildUpdatedNodeStructureQueryParameters } = useNodeQuery()
const nodeStore = useNodeStore()
const nodeStructureStore = useNodeStructureStore()
const searchTerm = ref('')
const currentSelection = ref<ISelectItemType | undefined>(undefined)

const onCurrentSearchUpdated = (updatedValue: any) => {
const item = (updatedValue as string) ?? ''

if (item) {
if (currentSelection.value?.value === 'ipAddress') {
if (!isIP(item)) {
// prevent search with invalid IP addresses, they'll just cause 500 errors
return
}
nodeStructureStore.setFilterWithIpAddress(item)
} else if ((currentSelection.value?.value as string || '').startsWith('snmp')) {
nodeStructureStore.setFilterWithSnmpParams((currentSelection.value?.value as string), item)
}

updateQuery()
}
}

const onSelectionUpdated: UpdateModelFunction = (selected: any) => {
if (selected.value === 'ipAddress') {
nodeStructureStore.setFilterWithIpAddress(searchTerm.value)
} else if ((selected.value as string || '').startsWith('snmp')) {
nodeStructureStore.setFilterWithSnmpParams(selected.value, searchTerm.value)
}

updateQuery()
}

// helper used in getOptionFromFilter
const getOptionFromObj = (obj: any, key: string) => {
if (obj[key]) {
return {
value: obj[key],
searchOption: searchOptions.find(x => x.value === key)
}
}

return undefined
}

/**
* Get an option object and search term based on the given filter.
* This prioritizes which search item is used.
*/
const getOptionFromFilter = (queryFilter: NodeQueryFilter) => {
if (queryFilter.ipAddress) {
return getOptionFromObj(queryFilter, 'ipAddress')
} else if (queryFilter.snmpParams) {
const p = queryFilter.snmpParams

for (let key of snmpKeys) {
const o = getOptionFromObj(p, key)

if (o) {
return o
}
}
}

return undefined
}

const updateQuery = () => {
// make sure anything setting nodeStore.nodeQueryParameters has been processed
nextTick()
const updatedParams = buildUpdatedNodeStructureQueryParameters(nodeStore.nodeQueryParameters, nodeStructureStore.queryFilter)

nodeStore.getNodes(updatedParams, true)
}

const updateFromStore = () => {
const option = getOptionFromFilter(nodeStructureStore.queryFilter)

if (option) {
searchTerm.value = option.value
currentSelection.value = option.searchOption
} else {
searchTerm.value = ''
}
}

watch([() => nodeStructureStore.queryFilter], () => {
updateFromStore()
})

onMounted(() => {
updateFromStore()
})

</script>

<style lang="scss" scoped>

</style>
37 changes: 4 additions & 33 deletions ui/src/components/Nodes/NodeStructurePanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -80,32 +80,20 @@
</FeatherList>
</FeatherExpansionPanel>
<div class="search-autocomplete-panel">
<h1 class="title">Metadata Search</h1>
<FeatherAutocomplete
v-model="metaSearchString"
type="multi"
:results="metadataSearchResults"
label="Search"
class="map-search"
@search="resetMetaSearch"
:loading="loading"
:hideLabel="true"
text-prop="label"
@update:modelValue="selectMetaItem"
:labels="metaLabels"
></FeatherAutocomplete>
<h1 class="title">Extended Search</h1>
<ExtendedSearchPanel />
</div>
</template>

<script setup lang="ts">
import ClearIcon from '@featherds/icon/action/Cancel'
import { FeatherAutocomplete } from '@featherds/autocomplete'
import { FeatherButton } from '@featherds/button'
import { FeatherExpansionPanel } from '@featherds/expansion'
import { FeatherIcon } from '@featherds/icon'
import { FeatherList, FeatherListItem } from '@featherds/list'
import { useNodeStructureStore } from '@/stores/nodeStructureStore'
import { Category, MonitoringLocation, SetOperator } from '@/types'
import ExtendedSearchPanel from './ExtendedSearchPanel.vue'

const nodeStructureStore = useNodeStructureStore()
const clearIcon = ref(ClearIcon)
Expand All @@ -116,24 +104,7 @@ const locations = computed<MonitoringLocation[]>(() => nodeStructureStore.monito
const selectedCategoryCount = computed<number>(() => nodeStructureStore.queryFilter.selectedCategories?.length || 0)
const selectedFlowCount = computed<number>(() => nodeStructureStore.queryFilter.selectedFlows?.length || 0)
const selectedLocationCount = computed<number>(() => nodeStructureStore.queryFilter.selectedMonitoringLocations?.length || 0)
const isAnyFilterSelected = computed<boolean>(() => nodeStructureStore.queryFilter.searchTerm?.length > 0 || selectedCategoryCount.value > 0 || selectedFlowCount.value > 0 || selectedLocationCount.value > 0)

const metaSearchString = ref()
const loading = ref(false)
const defaultMetaLabels = { noResults: 'Searching...' }
const metaLabels = ref(defaultMetaLabels)

const metadataSearchResults = computed(() => {
return ['cat', 'dog', 'parakeet'].map(x => ({ _text: x }))
})

const selectMetaItem = (obj: any) => {
console.log('selectMetaItem')
}

const resetMetaSearch = () => {
console.log('resetMetaSearch')
}
const isAnyFilterSelected = computed<boolean>(() => nodeStructureStore.isAnyFilterSelected())

const categoryModeUpdated = (val: any) => {
nodeStructureStore.setCategoryMode(val)
Expand Down
11 changes: 9 additions & 2 deletions ui/src/components/Nodes/NodesTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ const dialogVisible = ref(false)
const dialogNode = ref<Node>()
const preferencesVisible = ref(false)
const tableCssClasses = computed<string[]>(() => getTableCssClasses(nodeStructureStore.columns))
const queryParameters = ref({ limit: 20, offset: 0, orderBy: 'label' } as QueryParameters)
const queryParameters = ref<QueryParameters>(nodeStore.nodeQueryParameters)
const pageNumber = ref(1)

const isSelectedColumn = (column: NodeColumnSelectionItem, id: string) => {
Expand All @@ -223,11 +223,15 @@ const updatePageNumber = (page: number) => {
pageNumber.value = page
const pageSize = queryParameters.value.limit || 0
queryParameters.value = { ...queryParameters.value, offset: (page - 1) * pageSize }
nodeStore.setNodeQueryParameters(queryParameters.value)

updateQuery()
}

const updatePageSize = (size: number) => {
queryParameters.value = { ...queryParameters.value, limit: size }
nodeStore.setNodeQueryParameters(queryParameters.value)

updateQuery()
}

Expand Down Expand Up @@ -302,7 +306,10 @@ const onNodeLinkClick = (nodeId: number | string) => {
}

const updateQuery = () => {
const updatedParams = buildUpdatedNodeStructureQueryParameters(queryParameters.value, nodeStructureStore.queryFilter)
// make sure anything setting nodeStore.nodeQueryParameters has been processed
nextTick()

const updatedParams = buildUpdatedNodeStructureQueryParameters(nodeStore.nodeQueryParameters, nodeStructureStore.queryFilter)
queryParameters.value = updatedParams

nodeStore.getNodes(updatedParams, true)
Expand Down
118 changes: 118 additions & 0 deletions ui/src/components/Nodes/hooks/queryStringParser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import {
Category,
MonitoringLocation,
NodeQuerySnmpParams,
SetOperator
} from '@/types'
import { isIP } from 'is-ip'

/** Parse node label from a vue-router route.query object */
export const parseNodeLabel = (queryObject: any) => {
return queryObject.nodename as string || queryObject.nodeLabel as string || ''
}

/**
* Parse categories from a vue-router route.query object.
* The route.query 'categories' string can be a comma- or semicolon-separated list of either
* numeric Category ids or names.
* comma: Union; semicolon: Intersection
*
* @returns The category mode and categories parsed from the queryObject. If 'selectedCategories' is empty,
* it means no categories were present.
*/
export const parseCategories = (queryObject: any, categories: Category[]) => {
let categoryMode: SetOperator = SetOperator.Union
const selectedCategories: Category[] = []

const queryCategories = queryObject.categories as string ?? ''

if (selectedCategories.length > 0) {
categoryMode = queryCategories.includes(';') ? SetOperator.Intersection : SetOperator.Union

const cats: string[] = queryCategories.replace(';', ',').split(',')

// add any valid categories
cats.forEach(c => {
if (/\d+/.test(c)) {
// category id number
const id = parseInt(c)

const item = categories.find(x => x.id === id)

if (item) {
selectedCategories.push(item)
}
} else {
// category name, case insensitive
const item = categories.find(x => x.name.toLowerCase() === c.toLowerCase())

if (item) {
selectedCategories.push(item)
}
}
})
}

return {
categoryMode,
selectedCategories
}
}

export const parseMonitoringLocation = (queryObject: any, monitoringLocations: MonitoringLocation[]) => {
const locationName = queryObject.monitoringLocation as string || ''

if (locationName) {
return monitoringLocations.find(x => x.name.toLowerCase() === locationName.toLowerCase()) ?? null
}

return null
}

export const parseFlows = (queryObject: any) => {
const flows = (queryObject.flows as string || '').toLowerCase()

if (flows === 'true') {
return ['Ingress', 'Egress']
} else if (flows === 'ingress') {
return ['Ingress']
} else if (flows === 'egress') {
return ['Egress']
}

// TODO: we don't yet have support for excluding flows, i.e. if queryObject.flows === 'false'

return []
}

/**
* Currently this accepts anything in any valid IPv4 or IPv6 format (see `is-ip`), but
* some formats may not actually be supported by our FIQL search.
*/
export const parseIplike = (queryObject: any) => {
const ip = queryObject.iplike as string || queryObject.ipAddress as string || ''

if (ip && isIP(ip)) {
return ip
}

return null
}

export const parseSnmpParams = (queryObject: any) => {
const snmpIfAlias = queryObject.snmpifalias as string || ''
const snmpIfDesc = queryObject.snmpifdesc as string || ''
const snmpIfIndex = queryObject.snmpifindex as string || ''
const snmpIfName = queryObject.snmpifname as string || ''

if (snmpIfAlias || snmpIfDesc || snmpIfIndex || snmpIfName) {
return {
snmpIfAlias,
snmpIfDesc,
snmpIfIndex,
snmpIfName
} as NodeQuerySnmpParams
}

return null
}
Loading
Loading