diff --git a/webapp/src/Alerts.tsx b/webapp/src/Alerts.tsx index 4b551cab..5a558a98 100644 --- a/webapp/src/Alerts.tsx +++ b/webapp/src/Alerts.tsx @@ -42,13 +42,12 @@ import { eventSetEscalated, Tag, } from "./event"; -import { AddressCell, TimestampCell } from "./TimestampCell"; import { IdleTimer } from "./idletimer"; import { eventStore } from "./eventstore"; import { Logger } from "./util"; import { SensorSelect } from "./common/SensorSelect"; import * as bootstrap from "bootstrap"; -import { FilterStrip } from "./components"; +import { AddressCell, FilterStrip, TimestampCell } from "./components"; const DEFAULT_SORTBY = "timestamp"; const DEFAULT_SORTORDER = "desc"; @@ -374,7 +373,6 @@ export function Alerts() { untrack(() => { const logger = new Logger("Alerts.refreshEvents", true); let qFilters = getFilters(); - console.log(qFilters); let q: undefined | string = qFilters.join(" "); if (searchParams.q) { @@ -670,34 +668,8 @@ export function Alerts() { }); } - function removeFilter(filter: string) { - // TODO: There is a cleaner way with closure to do this. - let newFilters = filters(); - newFilters = newFilters.filter((f: any) => f !== filter); - - // But we have an effect to do this.. But it might not happen - // until after the refresh. Need to revisit effect/reactive - // dependency chains here. - setFilters(newFilters); - - if (newFilters.length == 0) { - console.log("Setting filters to null"); - setSearchParams({ - filters: undefined, - }); - } else { - setSearchParams({ - filters: newFilters, - }); - } - } - - const clearFilters = () => { - setFilters([]); - setSearchParams({ filters: null }); - }; - - // Getter for searchParams.filters to convert to an array if there is only one "filters" parameter. + // Getter for searchParams.filters to convert to an array if there + // is only one "filters" parameter. const getFilters = createMemo(() => { let filters = searchParams.filters || []; @@ -713,6 +685,13 @@ export function Alerts() { setFilters(getFilters()); }); + // Effect to update the query parameters when the filters signal updates. + createEffect(() => { + setSearchParams({ + filters: filters().length == 0 ? undefined : filters(), + }); + }); + function updateSort(key: string) { console.log("Sorting by " + key); let order = getSortOrder(); @@ -872,11 +851,7 @@ export function Alerts() { {/* Filter strip. */} 0}> - +
// SPDX-License-Identifier: MIT -import { TIME_RANGE, Top } from "./Top"; +import { Top } from "./Top"; import * as API from "./api"; import { createEffect, + createMemo, createSignal, For, Match, @@ -15,16 +16,16 @@ import { } from "solid-js"; import { EventWrapper } from "./types"; import { Button, Col, Container, Form, Row } from "solid-bootstrap"; -import { AddressCell, TimestampCell } from "./TimestampCell"; import { useNavigate, useSearchParams } from "@solidjs/router"; import { formatEventDescription } from "./formatters"; -import { BiCaretRightFill } from "./icons"; +import { BiCaretRightFill, BiDashCircle, BiPlusCircle } from "./icons"; import tinykeys from "tinykeys"; import { scrollToClass } from "./scroll"; import { Transition } from "solid-transition-group"; import { eventIsArchived, eventSetArchived } from "./event"; import { AlertDescription } from "./Alerts"; import { EventsQueryParams } from "./api"; +import { AddressCell, FilterStrip, TimestampCell } from "./components"; // The list of event types that will be shown in dropdowns. export const EVENT_TYPES: { name: string; eventType: string }[] = [ @@ -87,8 +88,10 @@ export function Events() { event_type?: string; from?: string; to?: string; + filters?: string[]; }>(); const [cursor, setCursor] = createSignal(0); + const [filters, setFilters] = createSignal([]); let keybindings: any = null; onMount(() => { @@ -131,6 +134,23 @@ export function Events() { } }); + // Getter for searchParams.filters to convert to an array if there + // is only one "filters" parameter. + const getFilters = createMemo(() => { + let filters = searchParams.filters || []; + + if (!Array.isArray(filters)) { + return [filters]; + } else { + return filters; + } + }); + + // Effect to update the filter strip based on the filters in the query string. + createEffect(() => { + setFilters(getFilters()); + }); + createEffect(() => { loadEvents(); }); @@ -144,15 +164,23 @@ export function Events() { } function loadEvents() { - let params: EventsQueryParams = {}; + let params: EventsQueryParams = { + query_string: searchParams.q || "", + }; if (searchParams.event_type) { params.event_type = searchParams.event_type; setEventType(params.event_type); } - if (searchParams.q) { - params.query_string = searchParams.q; + /* if (searchParams.q) { + * params.query_string = searchParams.q; + * } */ + + //const filterQuery: string = getFilters()?.join(" "); + const filterQuery: string = filters()?.join(" "); + if (filterQuery && filterQuery.length > 0) { + params.query_string += " " + filterQuery; } if (searchParams.order) { @@ -252,6 +280,39 @@ export function Events() { }); } + function addFilter(what: string, op: string, value: any) { + if (op == "+") { + op = ""; + } + let entry: string = ""; + if (typeof value === "number") { + entry = `${op}${what}:${value}`; + } else if (value.includes(" ")) { + entry = `${op}${what}:"${value}"`; + } else { + entry = `${op}${what}:${value}`; + } + + let newFilters = filters(); + + // If if entry already exists. + if (newFilters.indexOf(entry) > -1) { + return; + } + + newFilters.push(entry); + setSearchParams({ + filters: newFilters, + }); + } + + // Effect to update the query parameters when the filters signal updates. + createEffect(() => { + setSearchParams({ + filters: filters().length == 0 ? undefined : filters(), + }); + }); + return ( <> @@ -357,6 +418,10 @@ export function Events() { + + + +
@@ -438,16 +503,48 @@ export function Events() { {event._source.event_type?.toUpperCase() || "???"} + { + e.stopPropagation(); + addFilter( + "event_type", + "+", + event._source.event_type + ); + }} + title={`Filter for event_type: ${event._source.event_type}`} + > + + + { + e.stopPropagation(); + addFilter( + "event_type", + "-", + event._source.event_type + ); + }} + title={`Filter out event_type: ${event._source.event_type}`} + > + + {event._source.host}}> - + -// SPDX-License-Identifier: MIT - -import { parse_timestamp } from "./datetime"; -import { EventSource } from "./types"; -import { formatAddress } from "./formatters"; -import { BiDashCircle, BiPlusCircle } from "./icons"; -import { Show } from "solid-js"; - -export function TimestampCell(props: { timestamp: string }) { - let timestamp = parse_timestamp(props.timestamp); - return ( -
- {timestamp.format("YYYY-MM-DD HH:mm:ss")} -
- {timestamp.fromNow()} -
- ); -} - -export function AddressCell(props: { - source: EventSource; - fn?: (what: string, op: string, value: string | number) => void; -}) { - try { - return ( - <> - 0}> - S: {formatAddress(props.source.src_ip)} - - { - e.stopPropagation(); - props.fn!("src_ip", "+", props.source.src_ip); - }} - title="Filter for this src_ip" - > - - - { - e.stopPropagation(); - props.fn!("src_ip", "-", props.source.src_ip); - }} - title="Filter out this src_ip" - > - - - -
-
- 0}> - D: {formatAddress(props.source.dest_ip)} - - { - e.stopPropagation(); - props.fn!("dest_ip", "+", props.source.dest_ip); - }} - title="Filter for this dest_ip" - > - - - { - e.stopPropagation(); - props.fn!("dest_ip", "-", props.source.dest_ip); - }} - title="Filter out this dest_ip" - > - - - - - - ); - } catch (e) { - console.log(e); - return <>`Failed to format address: ${e}`; - } -} diff --git a/webapp/src/components.tsx b/webapp/src/components.tsx index 9b0eca93..f55733c1 100644 --- a/webapp/src/components.tsx +++ b/webapp/src/components.tsx @@ -3,6 +3,10 @@ import { For, Show } from "solid-js"; import { SearchLink } from "./common/SearchLink"; +import { parse_timestamp } from "./datetime"; +import { EventSource } from "./types"; +import { formatAddress } from "./formatters"; +import { BiDashCircle, BiFilter, BiPlusCircle } from "./icons"; // Creates a table where the first column is a count, and the second // column is value. @@ -78,18 +82,20 @@ export function CountValueDataTable(props: { ); } -export function FilterStrip(props: { - filters: any; - remove: (filter: any) => void; - clear: () => void; -}) { +export function FilterStrip(props: { filters: any; setFilters: any }) { + const removeFilter = (filter: any) => { + props.setFilters((filters: any[]) => + filters.filter((f: any) => f !== filter) + ); + }; + return ( <>
@@ -109,7 +115,7 @@ export function FilterStrip(props: { type="button" class="btn btn-outline-secondary" onClick={() => { - props.remove(filter); + removeFilter(filter); }} > X @@ -124,3 +130,113 @@ export function FilterStrip(props: { ); } + +export function TimestampCell(props: { + timestamp: string; + addFilter?: (what: string, op: string, value: string) => void; +}) { + let timestamp = parse_timestamp(props.timestamp); + let formatted = timestamp.format("YYYY-MM-DD HH:mm:ss"); + return ( +
+ {timestamp.format("YYYY-MM-DD HH:mm:ss")} +
+ {timestamp.fromNow()}{" "} + + e.stopPropagation()}> + + + + + + +
+ ); +} + +export function AddressCell(props: { + source: EventSource; + fn?: (what: string, op: string, value: string | number) => void; +}) { + try { + return ( + <> + 0}> + S: {formatAddress(props.source.src_ip)} + + { + e.stopPropagation(); + props.fn!("src_ip", "+", props.source.src_ip); + }} + title="Filter for this src_ip" + > + + + { + e.stopPropagation(); + props.fn!("src_ip", "-", props.source.src_ip); + }} + title="Filter out this src_ip" + > + + + +
+
+ 0}> + D: {formatAddress(props.source.dest_ip)} + + { + e.stopPropagation(); + props.fn!("dest_ip", "+", props.source.dest_ip); + }} + title="Filter for this dest_ip" + > + + + { + e.stopPropagation(); + props.fn!("dest_ip", "-", props.source.dest_ip); + }} + title="Filter out this dest_ip" + > + + + + + + ); + } catch (e) { + console.log(e); + return <>`Failed to format address: ${e}`; + } +} diff --git a/webapp/src/icons.tsx b/webapp/src/icons.tsx index ed6bafb9..d8b7f934 100644 --- a/webapp/src/icons.tsx +++ b/webapp/src/icons.tsx @@ -211,3 +211,18 @@ export function BiDashCircle() { ); } + +export function BiFilter() { + return ( + + + + ); +}