mirror of
https://github.com/jokob-sk/NetAlertX.git
synced 2026-04-02 16:22:20 -07:00
feat(api): Enhance session events API with pagination, sorting, and filtering
- Added support for pagination (page and limit) in the session events endpoint. - Implemented sorting functionality based on specified columns and directions. - Introduced free-text search capability for session events. - Updated SQL queries to retrieve all events and added a new SQL constant for events. - Refactored GraphQL types and helpers to support new plugin and event queries. - Created new GraphQL resolvers for plugins and events with pagination and filtering. - Added comprehensive tests for new GraphQL endpoints and session events functionality.
This commit is contained in:
@@ -1750,8 +1750,13 @@ def api_device_sessions(mac, payload=None):
|
||||
summary="Get Session Events",
|
||||
description="Retrieve events associated with sessions.",
|
||||
query_params=[
|
||||
{"name": "type", "description": "Event type", "required": False, "schema": {"type": "string", "default": "all"}},
|
||||
{"name": "period", "description": "Time period", "required": False, "schema": {"type": "string", "default": "7 days"}}
|
||||
{"name": "type", "description": "Event type", "required": False, "schema": {"type": "string", "default": "all"}},
|
||||
{"name": "period", "description": "Time period", "required": False, "schema": {"type": "string", "default": "7 days"}},
|
||||
{"name": "page", "description": "Page number (1-based)", "required": False, "schema": {"type": "integer", "default": 1}},
|
||||
{"name": "limit", "description": "Rows per page (max 1000)", "required": False, "schema": {"type": "integer", "default": 100}},
|
||||
{"name": "search", "description": "Free-text search filter", "required": False, "schema": {"type": "string"}},
|
||||
{"name": "sortCol", "description": "Column index to sort by (0-based)", "required": False, "schema": {"type": "integer", "default": 0}},
|
||||
{"name": "sortDir", "description": "Sort direction: asc or desc", "required": False, "schema": {"type": "string", "default": "desc"}}
|
||||
],
|
||||
tags=["sessions"],
|
||||
auth_callable=is_authorized
|
||||
@@ -1759,7 +1764,12 @@ def api_device_sessions(mac, payload=None):
|
||||
def api_get_session_events(payload=None):
|
||||
session_event_type = request.args.get("type", "all")
|
||||
period = get_date_from_period(request.args.get("period", "7 days"))
|
||||
return get_session_events(session_event_type, period)
|
||||
page = request.args.get("page", 1, type=int)
|
||||
limit = request.args.get("limit", 100, type=int)
|
||||
search = request.args.get("search", None)
|
||||
sort_col = request.args.get("sortCol", 0, type=int)
|
||||
sort_dir = request.args.get("sortDir", "desc")
|
||||
return get_session_events(session_event_type, period, page=page, limit=limit, search=search, sort_col=sort_col, sort_dir=sort_dir)
|
||||
|
||||
|
||||
# --------------------------
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import graphene
|
||||
from graphene import (
|
||||
ObjectType, String, Int, Boolean, List, Field, InputObjectType, Argument
|
||||
)
|
||||
from graphene import ObjectType, List, Field, Argument, String
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
@@ -19,175 +17,30 @@ from helper import ( # noqa: E402 [flake8 lint suppression]
|
||||
get_setting_value,
|
||||
)
|
||||
|
||||
# Define a base URL with the user's home directory
|
||||
from .graphql_types import ( # noqa: E402 [flake8 lint suppression]
|
||||
FilterOptionsInput, PageQueryOptionsInput,
|
||||
Device, DeviceResult,
|
||||
Setting, SettingResult,
|
||||
LangString, LangStringResult,
|
||||
AppEvent, AppEventResult,
|
||||
PluginQueryOptionsInput, PluginEntry,
|
||||
PluginsObjectsResult, PluginsEventsResult, PluginsHistoryResult,
|
||||
EventQueryOptionsInput, EventEntry, EventsResult,
|
||||
)
|
||||
from .graphql_helpers import ( # noqa: E402 [flake8 lint suppression]
|
||||
mixed_type_sort_key,
|
||||
apply_common_pagination,
|
||||
apply_plugin_filters,
|
||||
apply_events_filters,
|
||||
)
|
||||
|
||||
folder = apiPath
|
||||
|
||||
|
||||
# --- DEVICES ---
|
||||
# Pagination and Sorting Input Types
|
||||
class SortOptionsInput(InputObjectType):
|
||||
field = String()
|
||||
order = String()
|
||||
|
||||
|
||||
class FilterOptionsInput(InputObjectType):
|
||||
filterColumn = String()
|
||||
filterValue = String()
|
||||
|
||||
|
||||
class PageQueryOptionsInput(InputObjectType):
|
||||
page = Int()
|
||||
limit = Int()
|
||||
sort = List(SortOptionsInput)
|
||||
search = String()
|
||||
status = String()
|
||||
filters = List(FilterOptionsInput)
|
||||
|
||||
|
||||
# Device ObjectType
|
||||
class Device(ObjectType):
|
||||
rowid = Int(description="Database row ID")
|
||||
devMac = String(description="Device MAC address (e.g., 00:11:22:33:44:55)")
|
||||
devName = String(description="Device display name/alias")
|
||||
devOwner = String(description="Device owner")
|
||||
devType = String(description="Device type classification")
|
||||
devVendor = String(description="Hardware vendor from OUI lookup")
|
||||
devFavorite = Int(description="Favorite flag (0 or 1)")
|
||||
devGroup = String(description="Device group")
|
||||
devComments = String(description="User comments")
|
||||
devFirstConnection = String(description="Timestamp of first discovery")
|
||||
devLastConnection = String(description="Timestamp of last connection")
|
||||
devLastIP = String(description="Last known IP address")
|
||||
devPrimaryIPv4 = String(description="Primary IPv4 address")
|
||||
devPrimaryIPv6 = String(description="Primary IPv6 address")
|
||||
devVlan = String(description="VLAN identifier")
|
||||
devForceStatus = String(description="Force device status (online/offline/dont_force)")
|
||||
devStaticIP = Int(description="Static IP flag (0 or 1)")
|
||||
devScan = Int(description="Scan flag (0 or 1)")
|
||||
devLogEvents = Int(description="Log events flag (0 or 1)")
|
||||
devAlertEvents = Int(description="Alert events flag (0 or 1)")
|
||||
devAlertDown = Int(description="Alert on down flag (0 or 1)")
|
||||
devSkipRepeated = Int(description="Skip repeated alerts flag (0 or 1)")
|
||||
devLastNotification = String(description="Timestamp of last notification")
|
||||
devPresentLastScan = Int(description="Present in last scan flag (0 or 1)")
|
||||
devIsNew = Int(description="Is new device flag (0 or 1)")
|
||||
devLocation = String(description="Device location")
|
||||
devIsArchived = Int(description="Is archived flag (0 or 1)")
|
||||
devParentMAC = String(description="Parent device MAC address")
|
||||
devParentPort = String(description="Parent device port")
|
||||
devIcon = String(description="Base64-encoded HTML/SVG markup used to render the device icon")
|
||||
devGUID = String(description="Unique device GUID")
|
||||
devSite = String(description="Site name")
|
||||
devSSID = String(description="SSID connected to")
|
||||
devSyncHubNode = String(description="Sync hub node name")
|
||||
devSourcePlugin = String(description="Plugin that discovered the device")
|
||||
devCustomProps = String(description="Base64-encoded custom properties in JSON format")
|
||||
devStatus = String(description="Online/Offline status")
|
||||
devIsRandomMac = Int(description="Calculated: Is MAC address randomized?")
|
||||
devParentChildrenCount = Int(description="Calculated: Number of children attached to this parent")
|
||||
devIpLong = String(description="Calculated: IP address in long format (returned as string to support the full unsigned 32-bit range)")
|
||||
devFilterStatus = String(description="Calculated: Device status for UI filtering")
|
||||
devFQDN = String(description="Fully Qualified Domain Name")
|
||||
devParentRelType = String(description="Relationship type to parent")
|
||||
devReqNicsOnline = Int(description="Required NICs online flag")
|
||||
devMacSource = String(description="Source tracking for devMac (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devNameSource = String(description="Source tracking for devName (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devFQDNSource = String(description="Source tracking for devFQDN (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devLastIPSource = String(description="Source tracking for devLastIP (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devVendorSource = String(description="Source tracking for devVendor (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devSSIDSource = String(description="Source tracking for devSSID (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devParentMACSource = String(description="Source tracking for devParentMAC (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devParentPortSource = String(description="Source tracking for devParentPort (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devParentRelTypeSource = String(description="Source tracking for devParentRelType (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devVlanSource = String(description="Source tracking for devVlan")
|
||||
devFlapping = Int(description="Indicates flapping device (device changing between online/offline states frequently)")
|
||||
devCanSleep = Int(description="Can this device sleep? (0 or 1). When enabled, offline periods within NTFPRCS_sleep_time are reported as Sleeping instead of Down.")
|
||||
devIsSleeping = Int(description="Computed: Is device currently in a sleep window? (0 or 1)")
|
||||
|
||||
|
||||
class DeviceResult(ObjectType):
|
||||
devices = List(Device)
|
||||
count = Int()
|
||||
db_count = Int(description="Total device count in the database, before any status/filter/search is applied")
|
||||
|
||||
|
||||
# --- SETTINGS ---
|
||||
|
||||
|
||||
# Setting ObjectType
|
||||
class Setting(ObjectType):
|
||||
setKey = String(description="Unique configuration key")
|
||||
setName = String(description="Human-readable setting name")
|
||||
setDescription = String(description="Detailed description of the setting")
|
||||
setType = String(description="Config-driven type definition used to determine value type and UI rendering")
|
||||
setOptions = String(description="JSON string of available options")
|
||||
setGroup = String(description="UI group for categorization")
|
||||
setValue = String(description="Current value")
|
||||
setEvents = String(description="JSON string of events")
|
||||
setOverriddenByEnv = Boolean(description="Whether the value is currently overridden by an environment variable")
|
||||
|
||||
|
||||
class SettingResult(ObjectType):
|
||||
settings = List(Setting, description="List of setting objects")
|
||||
count = Int(description="Total count of settings")
|
||||
|
||||
# --- LANGSTRINGS ---
|
||||
|
||||
|
||||
# In-memory cache for lang strings
|
||||
_langstrings_cache = {} # caches lists per file (core JSON or plugin)
|
||||
_langstrings_cache_mtime = {} # tracks last modified times
|
||||
|
||||
|
||||
# LangString ObjectType
|
||||
class LangString(ObjectType):
|
||||
langCode = String(description="Language code (e.g., en_us, de_de)")
|
||||
langStringKey = String(description="Unique translation key")
|
||||
langStringText = String(description="Translated text content")
|
||||
|
||||
|
||||
class LangStringResult(ObjectType):
|
||||
langStrings = List(LangString, description="List of language string objects")
|
||||
count = Int(description="Total count of strings")
|
||||
|
||||
|
||||
# --- APP EVENTS ---
|
||||
|
||||
class AppEvent(ObjectType):
|
||||
index = Int(description="Internal index")
|
||||
guid = String(description="Unique event GUID")
|
||||
appEventProcessed = Int(description="Processing status (0 or 1)")
|
||||
dateTimeCreated = String(description="Event creation timestamp")
|
||||
|
||||
objectType = String(description="Type of the related object (Device, Setting, etc.)")
|
||||
objectGuid = String(description="GUID of the related object")
|
||||
objectPlugin = String(description="Plugin associated with the object")
|
||||
objectPrimaryId = String(description="Primary identifier of the object")
|
||||
objectSecondaryId = String(description="Secondary identifier of the object")
|
||||
objectForeignKey = String(description="Foreign key reference")
|
||||
objectIndex = Int(description="Object index")
|
||||
|
||||
objectIsNew = Int(description="Is the object new? (0 or 1)")
|
||||
objectIsArchived = Int(description="Is the object archived? (0 or 1)")
|
||||
objectStatusColumn = String(description="Column used for status")
|
||||
objectStatus = String(description="Object status value")
|
||||
|
||||
appEventType = String(description="Type of application event")
|
||||
|
||||
helper1 = String(description="Generic helper field 1")
|
||||
helper2 = String(description="Generic helper field 2")
|
||||
helper3 = String(description="Generic helper field 3")
|
||||
extra = String(description="Additional JSON data")
|
||||
|
||||
|
||||
class AppEventResult(ObjectType):
|
||||
appEvents = List(AppEvent, description="List of application events")
|
||||
count = Int(description="Total count of events")
|
||||
|
||||
|
||||
# ----------------------------------------------------------------------------------------------
|
||||
|
||||
# Define Query Type with Pagination Support
|
||||
class Query(ObjectType):
|
||||
# --- DEVICES ---
|
||||
devices = Field(DeviceResult, options=PageQueryOptionsInput())
|
||||
@@ -652,15 +505,75 @@ class Query(ObjectType):
|
||||
|
||||
return LangStringResult(langStrings=langStrings, count=len(langStrings))
|
||||
|
||||
# --- PLUGINS_OBJECTS ---
|
||||
pluginsObjects = Field(PluginsObjectsResult, options=PluginQueryOptionsInput())
|
||||
|
||||
# helps sorting inconsistent dataset mixed integers and strings
|
||||
def mixed_type_sort_key(value):
|
||||
if value is None or value == "":
|
||||
return (2, "") # Place None or empty strings last
|
||||
def resolve_pluginsObjects(self, info, options=None):
|
||||
return _resolve_plugin_table("table_plugins_objects.json", options, PluginsObjectsResult)
|
||||
|
||||
# --- PLUGINS_EVENTS ---
|
||||
pluginsEvents = Field(PluginsEventsResult, options=PluginQueryOptionsInput())
|
||||
|
||||
def resolve_pluginsEvents(self, info, options=None):
|
||||
return _resolve_plugin_table("table_plugins_events.json", options, PluginsEventsResult)
|
||||
|
||||
# --- PLUGINS_HISTORY ---
|
||||
pluginsHistory = Field(PluginsHistoryResult, options=PluginQueryOptionsInput())
|
||||
|
||||
def resolve_pluginsHistory(self, info, options=None):
|
||||
return _resolve_plugin_table("table_plugins_history.json", options, PluginsHistoryResult)
|
||||
|
||||
# --- EVENTS ---
|
||||
events = Field(EventsResult, options=EventQueryOptionsInput())
|
||||
|
||||
def resolve_events(self, info, options=None):
|
||||
try:
|
||||
with open(folder + "table_events.json", "r") as f:
|
||||
data = json.load(f).get("data", [])
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog("none", f"[graphql_schema] Error loading events data: {e}")
|
||||
return EventsResult(entries=[], count=0, db_count=0)
|
||||
|
||||
db_count = len(data)
|
||||
data = apply_events_filters(data, options)
|
||||
data, total_count = apply_common_pagination(data, options)
|
||||
return EventsResult(
|
||||
entries=[EventEntry(**r) for r in data],
|
||||
count=total_count,
|
||||
db_count=db_count,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Private resolver helper — shared by all three plugin table resolvers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _resolve_plugin_table(json_file, options, ResultType):
|
||||
try:
|
||||
return (0, int(value)) # Integers get priority
|
||||
except (ValueError, TypeError):
|
||||
return (1, str(value)) # Strings come next
|
||||
with open(folder + json_file, "r") as f:
|
||||
data = json.load(f).get("data", [])
|
||||
except (FileNotFoundError, json.JSONDecodeError) as e:
|
||||
mylog("none", f"[graphql_schema] Error loading {json_file}: {e}")
|
||||
return ResultType(entries=[], count=0, db_count=0)
|
||||
|
||||
# Scope to the requested plugin + foreignKey FIRST so db_count
|
||||
# reflects the total for THIS plugin, not the entire table.
|
||||
if options:
|
||||
if options.plugin:
|
||||
pl = options.plugin.lower()
|
||||
data = [r for r in data if str(r.get("plugin", "")).lower() == pl]
|
||||
if options.foreignKey:
|
||||
fk = options.foreignKey.lower()
|
||||
data = [r for r in data if str(r.get("foreignKey", "")).lower() == fk]
|
||||
|
||||
db_count = len(data)
|
||||
data = apply_plugin_filters(data, options)
|
||||
data, total_count = apply_common_pagination(data, options)
|
||||
return ResultType(
|
||||
entries=[PluginEntry(**r) for r in data],
|
||||
count=total_count,
|
||||
db_count=db_count,
|
||||
)
|
||||
|
||||
|
||||
# Schema Definition
|
||||
|
||||
140
server/api_server/graphql_helpers.py
Normal file
140
server/api_server/graphql_helpers.py
Normal file
@@ -0,0 +1,140 @@
|
||||
"""
|
||||
graphql_helpers.py — Shared utility functions for GraphQL resolvers.
|
||||
"""
|
||||
|
||||
_MAX_LIMIT = 1000
|
||||
_DEFAULT_LIMIT = 100
|
||||
|
||||
|
||||
def mixed_type_sort_key(value):
|
||||
"""Sort key that handles mixed int/string datasets without crashing.
|
||||
|
||||
Ordering priority:
|
||||
0 — integers (sorted numerically)
|
||||
1 — strings (sorted lexicographically)
|
||||
2 — None / empty string (always last)
|
||||
"""
|
||||
if value is None or value == "":
|
||||
return (2, "")
|
||||
try:
|
||||
return (0, int(value))
|
||||
except (ValueError, TypeError):
|
||||
return (1, str(value))
|
||||
|
||||
|
||||
def apply_common_pagination(data, options):
|
||||
"""Apply sort + capture total_count + paginate.
|
||||
|
||||
Returns (paged_data, total_count).
|
||||
Enforces a hard limit cap of _MAX_LIMIT — never returns unbounded results.
|
||||
"""
|
||||
if not options:
|
||||
return data, len(data)
|
||||
|
||||
# --- SORT ---
|
||||
if options.sort:
|
||||
for sort_option in reversed(options.sort):
|
||||
field = sort_option.field
|
||||
reverse = (sort_option.order or "asc").lower() == "desc"
|
||||
data = sorted(
|
||||
data,
|
||||
key=lambda x: mixed_type_sort_key(x.get(field)),
|
||||
reverse=reverse,
|
||||
)
|
||||
|
||||
total_count = len(data)
|
||||
|
||||
# --- PAGINATE ---
|
||||
if options.page is not None and options.limit is not None:
|
||||
effective_limit = min(options.limit, _MAX_LIMIT)
|
||||
start = (options.page - 1) * effective_limit
|
||||
end = start + effective_limit
|
||||
data = data[start:end]
|
||||
|
||||
return data, total_count
|
||||
|
||||
|
||||
def apply_plugin_filters(data, options):
|
||||
"""Filter a list of plugin table rows (Plugins_Objects/Events/History).
|
||||
|
||||
Handles: date range, column filters, free-text search.
|
||||
NOTE: plugin prefix and foreignKey scoping is done in the resolver
|
||||
BEFORE db_count is captured — do NOT duplicate here.
|
||||
"""
|
||||
if not options:
|
||||
return data
|
||||
|
||||
# Date-range filter on dateTimeCreated
|
||||
if options.dateFrom:
|
||||
data = [r for r in data if str(r.get("dateTimeCreated", "")) >= options.dateFrom]
|
||||
if options.dateTo:
|
||||
data = [r for r in data if str(r.get("dateTimeCreated", "")) <= options.dateTo]
|
||||
|
||||
# Column-value exact-match filters
|
||||
if options.filters:
|
||||
for f in options.filters:
|
||||
if f.filterColumn and f.filterValue is not None:
|
||||
data = [
|
||||
r for r in data
|
||||
if str(r.get(f.filterColumn, "")).lower() == str(f.filterValue).lower()
|
||||
]
|
||||
|
||||
# Free-text search
|
||||
if options.search:
|
||||
term = options.search.lower()
|
||||
searchable = [
|
||||
"plugin", "objectPrimaryId", "objectSecondaryId",
|
||||
"watchedValue1", "watchedValue2", "watchedValue3", "watchedValue4",
|
||||
"status", "extra", "foreignKey", "objectGuid", "userData",
|
||||
]
|
||||
data = [
|
||||
r for r in data
|
||||
if any(term in str(r.get(field, "")).lower() for field in searchable)
|
||||
]
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def apply_events_filters(data, options):
|
||||
"""Filter a list of Events table rows.
|
||||
|
||||
Handles: eveMac, eventType, date range, column filters, free-text search.
|
||||
"""
|
||||
if not options:
|
||||
return data
|
||||
|
||||
# MAC filter
|
||||
if options.eveMac:
|
||||
mac = options.eveMac.lower()
|
||||
data = [r for r in data if str(r.get("eveMac", "")).lower() == mac]
|
||||
|
||||
# Event-type filter
|
||||
if options.eventType:
|
||||
et = options.eventType.lower()
|
||||
data = [r for r in data if str(r.get("eveEventType", "")).lower() == et]
|
||||
|
||||
# Date-range filter on eveDateTime
|
||||
if options.dateFrom:
|
||||
data = [r for r in data if str(r.get("eveDateTime", "")) >= options.dateFrom]
|
||||
if options.dateTo:
|
||||
data = [r for r in data if str(r.get("eveDateTime", "")) <= options.dateTo]
|
||||
|
||||
# Column-value exact-match filters
|
||||
if options.filters:
|
||||
for f in options.filters:
|
||||
if f.filterColumn and f.filterValue is not None:
|
||||
data = [
|
||||
r for r in data
|
||||
if str(r.get(f.filterColumn, "")).lower() == str(f.filterValue).lower()
|
||||
]
|
||||
|
||||
# Free-text search
|
||||
if options.search:
|
||||
term = options.search.lower()
|
||||
searchable = ["eveMac", "eveIp", "eveEventType", "eveAdditionalInfo"]
|
||||
data = [
|
||||
r for r in data
|
||||
if any(term in str(r.get(field, "")).lower() for field in searchable)
|
||||
]
|
||||
|
||||
return data
|
||||
261
server/api_server/graphql_types.py
Normal file
261
server/api_server/graphql_types.py
Normal file
@@ -0,0 +1,261 @@
|
||||
import graphene # noqa: F401 (re-exported for schema creation in graphql_endpoint.py)
|
||||
from graphene import (
|
||||
ObjectType, String, Int, Boolean, List, InputObjectType,
|
||||
)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared Input Types
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class SortOptionsInput(InputObjectType):
|
||||
field = String()
|
||||
order = String()
|
||||
|
||||
|
||||
class FilterOptionsInput(InputObjectType):
|
||||
filterColumn = String()
|
||||
filterValue = String()
|
||||
|
||||
|
||||
class PageQueryOptionsInput(InputObjectType):
|
||||
page = Int()
|
||||
limit = Int()
|
||||
sort = List(SortOptionsInput)
|
||||
search = String()
|
||||
status = String()
|
||||
filters = List(FilterOptionsInput)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Devices
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Device(ObjectType):
|
||||
rowid = Int(description="Database row ID")
|
||||
devMac = String(description="Device MAC address (e.g., 00:11:22:33:44:55)")
|
||||
devName = String(description="Device display name/alias")
|
||||
devOwner = String(description="Device owner")
|
||||
devType = String(description="Device type classification")
|
||||
devVendor = String(description="Hardware vendor from OUI lookup")
|
||||
devFavorite = Int(description="Favorite flag (0 or 1)")
|
||||
devGroup = String(description="Device group")
|
||||
devComments = String(description="User comments")
|
||||
devFirstConnection = String(description="Timestamp of first discovery")
|
||||
devLastConnection = String(description="Timestamp of last connection")
|
||||
devLastIP = String(description="Last known IP address")
|
||||
devPrimaryIPv4 = String(description="Primary IPv4 address")
|
||||
devPrimaryIPv6 = String(description="Primary IPv6 address")
|
||||
devVlan = String(description="VLAN identifier")
|
||||
devForceStatus = String(description="Force device status (online/offline/dont_force)")
|
||||
devStaticIP = Int(description="Static IP flag (0 or 1)")
|
||||
devScan = Int(description="Scan flag (0 or 1)")
|
||||
devLogEvents = Int(description="Log events flag (0 or 1)")
|
||||
devAlertEvents = Int(description="Alert events flag (0 or 1)")
|
||||
devAlertDown = Int(description="Alert on down flag (0 or 1)")
|
||||
devSkipRepeated = Int(description="Skip repeated alerts flag (0 or 1)")
|
||||
devLastNotification = String(description="Timestamp of last notification")
|
||||
devPresentLastScan = Int(description="Present in last scan flag (0 or 1)")
|
||||
devIsNew = Int(description="Is new device flag (0 or 1)")
|
||||
devLocation = String(description="Device location")
|
||||
devIsArchived = Int(description="Is archived flag (0 or 1)")
|
||||
devParentMAC = String(description="Parent device MAC address")
|
||||
devParentPort = String(description="Parent device port")
|
||||
devIcon = String(description="Base64-encoded HTML/SVG markup used to render the device icon")
|
||||
devGUID = String(description="Unique device GUID")
|
||||
devSite = String(description="Site name")
|
||||
devSSID = String(description="SSID connected to")
|
||||
devSyncHubNode = String(description="Sync hub node name")
|
||||
devSourcePlugin = String(description="Plugin that discovered the device")
|
||||
devCustomProps = String(description="Base64-encoded custom properties in JSON format")
|
||||
devStatus = String(description="Online/Offline status")
|
||||
devIsRandomMac = Int(description="Calculated: Is MAC address randomized?")
|
||||
devParentChildrenCount = Int(description="Calculated: Number of children attached to this parent")
|
||||
devIpLong = String(description="Calculated: IP address in long format (returned as string to support the full unsigned 32-bit range)")
|
||||
devFilterStatus = String(description="Calculated: Device status for UI filtering")
|
||||
devFQDN = String(description="Fully Qualified Domain Name")
|
||||
devParentRelType = String(description="Relationship type to parent")
|
||||
devReqNicsOnline = Int(description="Required NICs online flag")
|
||||
devMacSource = String(description="Source tracking for devMac (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devNameSource = String(description="Source tracking for devName (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devFQDNSource = String(description="Source tracking for devFQDN (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devLastIPSource = String(description="Source tracking for devLastIP (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devVendorSource = String(description="Source tracking for devVendor (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devSSIDSource = String(description="Source tracking for devSSID (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devParentMACSource = String(description="Source tracking for devParentMAC (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devParentPortSource = String(description="Source tracking for devParentPort (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devParentRelTypeSource = String(description="Source tracking for devParentRelType (USER, LOCKED, NEWDEV, or plugin prefix)")
|
||||
devVlanSource = String(description="Source tracking for devVlan")
|
||||
devFlapping = Int(description="Indicates flapping device (device changing between online/offline states frequently)")
|
||||
devCanSleep = Int(description="Can this device sleep? (0 or 1). When enabled, offline periods within NTFPRCS_sleep_time are reported as Sleeping instead of Down.")
|
||||
devIsSleeping = Int(description="Computed: Is device currently in a sleep window? (0 or 1)")
|
||||
|
||||
|
||||
class DeviceResult(ObjectType):
|
||||
devices = List(Device)
|
||||
count = Int()
|
||||
db_count = Int(description="Total device count in the database, before any status/filter/search is applied")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Settings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class Setting(ObjectType):
|
||||
setKey = String(description="Unique configuration key")
|
||||
setName = String(description="Human-readable setting name")
|
||||
setDescription = String(description="Detailed description of the setting")
|
||||
setType = String(description="Config-driven type definition used to determine value type and UI rendering")
|
||||
setOptions = String(description="JSON string of available options")
|
||||
setGroup = String(description="UI group for categorization")
|
||||
setValue = String(description="Current value")
|
||||
setEvents = String(description="JSON string of events")
|
||||
setOverriddenByEnv = Boolean(description="Whether the value is currently overridden by an environment variable")
|
||||
|
||||
|
||||
class SettingResult(ObjectType):
|
||||
settings = List(Setting, description="List of setting objects")
|
||||
count = Int(description="Total count of settings")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Language Strings
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class LangString(ObjectType):
|
||||
langCode = String(description="Language code (e.g., en_us, de_de)")
|
||||
langStringKey = String(description="Unique translation key")
|
||||
langStringText = String(description="Translated text content")
|
||||
|
||||
|
||||
class LangStringResult(ObjectType):
|
||||
langStrings = List(LangString, description="List of language string objects")
|
||||
count = Int(description="Total count of strings")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# App Events
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class AppEvent(ObjectType):
|
||||
index = Int(description="Internal index")
|
||||
guid = String(description="Unique event GUID")
|
||||
appEventProcessed = Int(description="Processing status (0 or 1)")
|
||||
dateTimeCreated = String(description="Event creation timestamp")
|
||||
|
||||
objectType = String(description="Type of the related object (Device, Setting, etc.)")
|
||||
objectGuid = String(description="GUID of the related object")
|
||||
objectPlugin = String(description="Plugin associated with the object")
|
||||
objectPrimaryId = String(description="Primary identifier of the object")
|
||||
objectSecondaryId = String(description="Secondary identifier of the object")
|
||||
objectForeignKey = String(description="Foreign key reference")
|
||||
objectIndex = Int(description="Object index")
|
||||
|
||||
objectIsNew = Int(description="Is the object new? (0 or 1)")
|
||||
objectIsArchived = Int(description="Is the object archived? (0 or 1)")
|
||||
objectStatusColumn = String(description="Column used for status")
|
||||
objectStatus = String(description="Object status value")
|
||||
|
||||
appEventType = String(description="Type of application event")
|
||||
|
||||
helper1 = String(description="Generic helper field 1")
|
||||
helper2 = String(description="Generic helper field 2")
|
||||
helper3 = String(description="Generic helper field 3")
|
||||
extra = String(description="Additional JSON data")
|
||||
|
||||
|
||||
class AppEventResult(ObjectType):
|
||||
appEvents = List(AppEvent, description="List of application events")
|
||||
count = Int(description="Total count of events")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin tables (Plugins_Objects, Plugins_Events, Plugins_History)
|
||||
# All three tables share the same schema — one ObjectType, three result wrappers.
|
||||
# GraphQL requires distinct named types even when fields are identical.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class PluginQueryOptionsInput(InputObjectType):
|
||||
page = Int()
|
||||
limit = Int()
|
||||
sort = List(SortOptionsInput)
|
||||
search = String()
|
||||
filters = List(FilterOptionsInput)
|
||||
plugin = String(description="Filter by plugin prefix (e.g. 'ARPSCAN')")
|
||||
foreignKey = String(description="Filter by foreignKey (e.g. device MAC)")
|
||||
dateFrom = String(description="dateTimeCreated >= dateFrom (ISO datetime string)")
|
||||
dateTo = String(description="dateTimeCreated <= dateTo (ISO datetime string)")
|
||||
|
||||
|
||||
class PluginEntry(ObjectType):
|
||||
index = Int(description="Auto-increment primary key")
|
||||
plugin = String(description="Plugin prefix identifier")
|
||||
objectPrimaryId = String(description="Primary identifier (e.g. MAC, IP)")
|
||||
objectSecondaryId = String(description="Secondary identifier")
|
||||
dateTimeCreated = String(description="Record creation timestamp")
|
||||
dateTimeChanged = String(description="Record last-changed timestamp")
|
||||
watchedValue1 = String(description="Monitored value 1")
|
||||
watchedValue2 = String(description="Monitored value 2")
|
||||
watchedValue3 = String(description="Monitored value 3")
|
||||
watchedValue4 = String(description="Monitored value 4")
|
||||
status = String(description="Record status")
|
||||
extra = String(description="Extra JSON payload")
|
||||
userData = String(description="User-supplied data")
|
||||
foreignKey = String(description="Foreign key (e.g. device MAC)")
|
||||
syncHubNodeName = String(description="Sync hub node name")
|
||||
helpVal1 = String(description="Helper value 1")
|
||||
helpVal2 = String(description="Helper value 2")
|
||||
helpVal3 = String(description="Helper value 3")
|
||||
helpVal4 = String(description="Helper value 4")
|
||||
objectGuid = String(description="Object GUID")
|
||||
|
||||
|
||||
class PluginsObjectsResult(ObjectType):
|
||||
entries = List(PluginEntry, description="Plugins_Objects rows")
|
||||
count = Int(description="Filtered count (before pagination)")
|
||||
db_count = Int(description="Total rows in table before any filter")
|
||||
|
||||
|
||||
class PluginsEventsResult(ObjectType):
|
||||
entries = List(PluginEntry, description="Plugins_Events rows")
|
||||
count = Int(description="Filtered count (before pagination)")
|
||||
db_count = Int(description="Total rows in table before any filter")
|
||||
|
||||
|
||||
class PluginsHistoryResult(ObjectType):
|
||||
entries = List(PluginEntry, description="Plugins_History rows")
|
||||
count = Int(description="Filtered count (before pagination)")
|
||||
db_count = Int(description="Total rows in table before any filter")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Events table (device presence events)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class EventQueryOptionsInput(InputObjectType):
|
||||
page = Int()
|
||||
limit = Int()
|
||||
sort = List(SortOptionsInput)
|
||||
search = String()
|
||||
filters = List(FilterOptionsInput)
|
||||
eveMac = String(description="Filter by device MAC address")
|
||||
eventType = String(description="Filter by eveEventType (exact match)")
|
||||
dateFrom = String(description="eveDateTime >= dateFrom (ISO datetime string)")
|
||||
dateTo = String(description="eveDateTime <= dateTo (ISO datetime string)")
|
||||
|
||||
|
||||
class EventEntry(ObjectType):
|
||||
rowid = Int(description="SQLite rowid")
|
||||
eveMac = String(description="Device MAC address")
|
||||
eveIp = String(description="Device IP at event time")
|
||||
eveDateTime = String(description="Event timestamp")
|
||||
eveEventType = String(description="Event type (Connected, New Device, etc.)")
|
||||
eveAdditionalInfo = String(description="Additional event info")
|
||||
evePendingAlertEmail = Int(description="Pending alert flag (0 or 1)")
|
||||
evePairEventRowid = Int(description="Paired event rowid (for session pairing)")
|
||||
|
||||
|
||||
class EventsResult(ObjectType):
|
||||
entries = List(EventEntry, description="Events table rows")
|
||||
count = Int(description="Filtered count (before pagination)")
|
||||
db_count = Int(description="Total rows in table before any filter")
|
||||
@@ -295,10 +295,16 @@ def get_device_sessions(mac, period):
|
||||
return jsonify({"success": True, "sessions": sessions})
|
||||
|
||||
|
||||
def get_session_events(event_type, period_date):
|
||||
def get_session_events(event_type, period_date, page=1, limit=100, search=None, sort_col=0, sort_dir="desc"):
|
||||
"""
|
||||
Fetch events or sessions based on type and period.
|
||||
Supports server-side pagination (page/limit), free-text search, and sorting.
|
||||
Returns { data, total, recordsFiltered } so callers can drive DataTables serverSide mode.
|
||||
"""
|
||||
_MAX_LIMIT = 1000
|
||||
limit = min(max(1, int(limit)), _MAX_LIMIT)
|
||||
page = max(1, int(page))
|
||||
|
||||
conn = get_temp_db_connection()
|
||||
conn.row_factory = sqlite3.Row
|
||||
cur = conn.cursor()
|
||||
@@ -420,4 +426,30 @@ def get_session_events(event_type, period_date):
|
||||
|
||||
table_data["data"].append(row)
|
||||
|
||||
return jsonify(table_data)
|
||||
all_rows = table_data["data"]
|
||||
|
||||
# --- Sorting ---
|
||||
num_cols = len(all_rows[0]) if all_rows else 0
|
||||
if 0 <= sort_col < num_cols:
|
||||
reverse = sort_dir.lower() == "desc"
|
||||
all_rows.sort(
|
||||
key=lambda r: (r[sort_col] is None, r[sort_col] if r[sort_col] is not None else ""),
|
||||
reverse=reverse,
|
||||
)
|
||||
|
||||
total = len(all_rows)
|
||||
|
||||
# --- Free-text search (applied after formatting so display values are searchable) ---
|
||||
if search:
|
||||
search_lower = search.strip().lower()
|
||||
|
||||
def _row_matches(r):
|
||||
return any(search_lower in str(v).lower() for v in r if v is not None)
|
||||
all_rows = [r for r in all_rows if _row_matches(r)]
|
||||
records_filtered = len(all_rows)
|
||||
|
||||
# --- Pagination ---
|
||||
offset = (page - 1) * limit
|
||||
paged_rows = all_rows[offset: offset + limit]
|
||||
|
||||
return jsonify({"data": paged_rows, "total": total, "recordsFiltered": records_filtered})
|
||||
|
||||
Reference in New Issue
Block a user