From 0acc6f63d9e72876190faef263f848659baabe54 Mon Sep 17 00:00:00 2001 From: Fira Date: Thu, 2 Jul 2026 17:49:49 +0000 Subject: [PATCH 01/18] WIP --- code/__DEFINES/opensearch.dm | 35 ++ code/__DEFINES/subsystems.dm | 1 + code/controllers/subsystem/opensearch.dm | 101 +++++ code/datums/opensearch/opensearch_query.dm | 273 +++++++++++ code/modules/admin/admin_verbs.dm | 1 + colonialmarines.dme | 3 + .../tgui/interfaces/OpenSearchQuery.tsx | 427 ++++++++++++++++++ 7 files changed, 841 insertions(+) create mode 100644 code/__DEFINES/opensearch.dm create mode 100644 code/controllers/subsystem/opensearch.dm create mode 100644 code/datums/opensearch/opensearch_query.dm create mode 100644 tgui/packages/tgui/interfaces/OpenSearchQuery.tsx diff --git a/code/__DEFINES/opensearch.dm b/code/__DEFINES/opensearch.dm new file mode 100644 index 000000000000..9d8aae721238 --- /dev/null +++ b/code/__DEFINES/opensearch.dm @@ -0,0 +1,35 @@ + +// Modes for the query builder + +/// Send the query as raw DSL without changes - for debugging only +#define OPENSEARCH_QUERY_MODE_DSL_RAW 0 +/// Use a DSL query as input, but add-in parameters from the query builder (such as time range) +/// This needs a compatible query that is structured around an outer 'bool' clause to work +#define OPENSEARCH_QUERY_MODE_DSL 1 +/// Send the query as a Lucene query_string together with query builder params +#define OPENSEARCH_QUERY_MODE_LUCENE 2 + +// This could also support PPL and SQL querying. DQL is only available in Opensearch-Dashboards. +// Ultimately, the query builder should have custom modes that create more complex queries for the user. + + +// Modes for the time selector in query builder + +/// Relative mode, for input compared to present time +#define OPENSEARCH_QUERY_TIME_MODE_RELATIVE 0 +/// Absolute mode, to specify exact points in time +#define OPENSEARCH_QUERY_TIME_MODE_ABSOLUTE 1 + + +// States of query + +/// Query is ready to be built +#define OPENSEARCH_QUERY_STATUS_READY 1 +/// Query was unable to be built +#define OPENSEARCH_QUERY_STATUS_BUILD_ERROR 2 +/// Query is executing +#define OPENSEARCH_QUERY_STATUS_EXECUTING 3 +/// Query failed to execute +#define OPENSEARCH_QUERY_STATUS_FAILED 4 +/// Query executed successfully +#define OPENSEARCH_QUERY_STATUS_SUCCESS 5 diff --git a/code/__DEFINES/subsystems.dm b/code/__DEFINES/subsystems.dm index 447d212cc6d8..7ed879f98050 100644 --- a/code/__DEFINES/subsystems.dm +++ b/code/__DEFINES/subsystems.dm @@ -193,6 +193,7 @@ #define SS_PRIORITY_FZ_TRANSITIONS 88 #define SS_PRIORITY_ROUND_RECORDING 83 #define SS_PRIORITY_SHUTTLE 80 +#define SS_PRIORITY_OPENSEARCH 75 #define SS_PRIORITY_EVENT 65 #define SS_PRIORITY_DISEASE 60 #define SS_PRIORITY_DEFENSES 55 diff --git a/code/controllers/subsystem/opensearch.dm b/code/controllers/subsystem/opensearch.dm new file mode 100644 index 000000000000..9f36860f020b --- /dev/null +++ b/code/controllers/subsystem/opensearch.dm @@ -0,0 +1,101 @@ +/// Handles querying OpenSearch for logs information +SUBSYSTEM_DEF(opensearch) + name = "OpenSearch" + priority = SS_PRIORITY_OPENSEARCH + runlevels = RUNLEVELS_DEFAULT|RUNLEVEL_LOBBY + wait = 0.2 SECONDS + + /// Incrementing id counter to assign to queries + var/query_counter = 1 + /// Incrementing counter of completed requests + var/total_requests = 0 + /// AList of ID -> /datum/opensearch_query + var/alist/queries = alist() + /// List of /datum/opensearch_query currently performing a request, mapped to their http_request object + var/list/datum/opensearch_query/active = list() + /// Snapshot of the above for scheduling + var/list/datum/opensearch_query/currentrun + +/datum/controller/subsystem/opensearch/Initialize() + if(!CONFIG_GET(string/opensearch_host)) + can_fire = FALSE + return SS_INIT_NO_NEED + return SS_INIT_SUCCESS + +/datum/controller/subsystem/opensearch/stat_entry(msg) + if(!can_fire) + msg = "Disabled" + else + msg = "Completed: [total_requests], Queries: [length(queries)], Active: [length(active)]" + return ..() + +/datum/controller/subsystem/opensearch/fire(resumed) + if(!resumed) + currentrun = active.Copy() + while(length(currentrun)) + var/datum/opensearch_query/query = currentrun[currentrun.len] + var/datum/http_request/http_request = currentrun[query] + currentrun.len-- + handle_request(query, http_request) + if(MC_TICK_CHECK) + return + +/// Create a new query and register it +/datum/controller/subsystem/opensearch/proc/new_query() + RETURN_TYPE(/datum/opensearch_query) + var/id = query_counter++ + var/datum/opensearch_query/query = new(id) + queries[id] = query + return query + +/// Remove a query from controller tracking +/datum/controller/subsystem/opensearch/proc/forget(datum/opensearch_query/query) + if(length(currentrun)) + currentrun -= query + active -= query + queries -= query.id + +/// Handle a request completion during fire +/datum/controller/subsystem/opensearch/proc/handle_request(datum/opensearch_query/query, datum/http_request/http_request) + PRIVATE_PROC(TRUE) + var/completed = http_request.is_complete() + if(completed) + total_requests++ + active -= query + var/datum/http_response/response = http_request.into_response() + if(!response || response.errored) + query.on_error(response) + else if(response.status_code == 200) + query.on_success(response) + else + query.on_failure(response) + +/// Queues a new HTTP request to the OpenSearch backend: +/// * [query] : The query this request is attributed to. Has no real effect, is just to know who to fire callbacks to. +/// * [endpoint] : The HTTP endpoint to make the request to. For example, /_search +/// * [payload] : The full HTTP payload with a properly built query for the backend +/datum/controller/subsystem/opensearch/proc/queue(datum/opensearch_query/query, endpoint, list/payload) + if(!can_fire || query.query_status != OPENSEARCH_QUERY_STATUS_READY || (query in active)) + return FALSE + var/bare_payload = payload + if(islist(payload)) + bare_payload = json_encode(payload) + var/datum/http_request/request = new + request.prepare(RUSTG_HTTP_METHOD_GET, "[CONFIG_GET(string/opensearch_host)]/[CONFIG_GET(string/opensearch_pattern)]/[endpoint]", bare_payload, list("Content-Type" = "application/json", "Authorization" = "Basic [CONFIG_GET(string/opensearch_authtoken)]")) + request.begin_async() + active[query] = request + query.query_status = OPENSEARCH_QUERY_STATUS_EXECUTING + . = TRUE + + +/// The host to make OpenSearch queries to, for example "https://opensearch.cm-ss13.com" +/datum/config_entry/string/opensearch_host + protection = CONFIG_ENTRY_LOCKED + +/// The Basic HTTP auth token to use for authentication to OpenSearch, in the form of user:password base64ed +/datum/config_entry/string/opensearch_authtoken + protection = CONFIG_ENTRY_HIDDEN | CONFIG_ENTRY_LOCKED | CONFIG_ENTRY_SENSITIVE + +/// Index patterns to be accessed by OpenSearch query tool +/datum/config_entry/string/opensearch_pattern + protection = CONFIG_ENTRY_LOCKED diff --git a/code/datums/opensearch/opensearch_query.dm b/code/datums/opensearch/opensearch_query.dm new file mode 100644 index 000000000000..0f6773e5de3a --- /dev/null +++ b/code/datums/opensearch/opensearch_query.dm @@ -0,0 +1,273 @@ +/// Models a single instance of an OpenSearch query to be used +/// The query can be changed over time, and this datum also serves as the +/// handler of the tgui associated with viewing the search + +/datum/opensearch_query + /// Numerical ID used to reference the search internally and in links + var/id = NONE + /// User given name to the query + var/name = "" + + /// BYOND-side representation of the query DSL that will be sent to + /// OpenSearch - This is either direct user input, or the cached query + /// buiilt from the user arguments + /// Note that PPL/SQL modes do not use this + var/list/dsl = list() + + /// Mode the query builder is in, providing user the appropriate UI + /// before finally turning it into the final query DSL + var/query_mode = OPENSEARCH_QUERY_MODE_LUCENE + /// If true, we sort by query ranks instead of by time + var/ranking_mode = FALSE + + + /// Current status of the query + var/query_status = NONE + /// Status text, human readable, including errors + var/status_text = "Ready" + /// Raw results of the last query, to be passed as stringified JSON to TGUI + var/query_results + + + // Basic query parameters go below + + /// Index pattern to query against, basically a config option for now, + /// but you might want to let it be selectable later for performance + var/target_index = "gamelogs-*" + /// Max amount of results to be fetched. Be conservative, BYOND's json decode is a killer + var/max_results = 200 + + /// What mode the time selector is in + var/time_mode = OPENSEARCH_QUERY_TIME_MODE_RELATIVE + /// Start time (or time offset in the past from now) in seconds + var/time_start = (240 * 3600) + /// End time for the selected time range, defaults to 0 so now in relative mode + var/time_end = 0 + + /// Selected round_id if any. If null, the filter is inactive. If zero, autofilled to current round. + var/target_roundid = NONE + + /// User input query text if any - such as DSL/PPL/Lucene + var/user_query = "" + /// Filter for the type of logs to view. If empty, show everything. + var/list/log_types = list() + + +/datum/opensearch_query/New(id) + . = ..() + src.id = id // To be attributed by SSopensearch, don't instantiate this directly + if(target_roundid == NONE) + //target_roundid = GLOB.round_id + target_roundid = 35086 // DEBUG + name = "Unnamed Query &[id]" + query_status = OPENSEARCH_QUERY_STATUS_READY + +/datum/opensearch_query/Destroy(force) + SStgui.close_uis(src) + SSopensearch.forget(src) + return ..() + +/// Launches the query, returning an error text if something went wrong +/datum/opensearch_query/proc/execute() + PRIVATE_PROC(TRUE) + . = "Unknown Error" + if(query_status == OPENSEARCH_QUERY_STATUS_EXECUTING) + return "Already in progress" + . = build_dsl() + if(!SSopensearch.queue(src, "_search", query_mode == OPENSEARCH_QUERY_MODE_DSL_RAW ? user_query : dsl)) + return "Failed to start query" + . = null + +/// Builds the query DSL based on the query builder parameters +/// This returns either null on success or an error message +/datum/opensearch_query/proc/build_dsl() + PRIVATE_PROC(TRUE) + if(query_status == OPENSEARCH_QUERY_STATUS_EXECUTING) + return "Already in progress" + + . = "Unknown error" + + switch(query_mode) + if(OPENSEARCH_QUERY_MODE_DSL_RAW) + . = null // Nothing to do, this will send the user input directly + + if(OPENSEARCH_QUERY_MODE_DSL) + try + dsl = json_decode(user_query) + catch + . = "Failed to parse DSL into JSON" + if(!.) + . = inject_parameters_in_dsl() + + if(OPENSEARCH_QUERY_MODE_LUCENE) + . = bootstrap_lucene_dsl() + if(!.) + . = inject_parameters_in_dsl() + + query_status = . ? OPENSEARCH_QUERY_STATUS_BUILD_ERROR : OPENSEARCH_QUERY_STATUS_READY + +/// Creates the base DSL for executing a Lucene based search +/datum/opensearch_query/proc/bootstrap_lucene_dsl() + PRIVATE_PROC(TRUE) + . = "Internal error creating the Lucene-based DSL" + dsl = list("query" = list("bool" = list("filter" = list()))) + var/list/filter = dsl["query"]["bool"]["filter"] + filter[++filter.len] = list("query_string" = list("query" = user_query)) + . = null + + +/// Adds additional query builder parameters into the DSL +/datum/opensearch_query/proc/inject_parameters_in_dsl() + PRIVATE_PROC(TRUE) + . = "Failed to inject query builder params into DSL" + dsl["size"] = max_results + . = "Malformed DSL query - it needs to use query.bool" + + var/list/bool = dsl["query"]["bool"] + if(!bool["filter"]) + bool["filter"] = list() + if(!bool["must"]) + bool["must"] = list() + + var/list/filter = bool["filter"] + var/list/must = bool["must"] + . = "Unknown error" + + // If requested, inject log_type filtering in the must close + if(length(log_types)) + var/list/lower_logtypes = list() + for(var/logtype in log_types) + lower_logtypes += lowertext(logtype) // OpenSearch wants them in lower case, presumably because it's stored as a keyword + must[++must.len] = list("terms" = list("logtype" = lower_logtypes)) + + // Inject Time range in the filter clause + var/timefilter = list("range" = list("@timestamp" = list("gte" = render_time_param(time_start), "lte" = render_time_param(time_end)))) + filter[++filter.len] = timefilter + + // Inject Round ID filtering in the filter clause + if(target_roundid) + var/roundidfilter = list("term" = list("roundid" = target_roundid)) + filter[++filter.len] = roundidfilter + + // Set sorting mode by most recent by default, unless we want to have ranking sorting + if(!ranking_mode) + dsl["sort"] = list(list("@timestamp" = list("order" = "desc"))) + + // Output the ranking information regardless of time sorting + dsl["track_scores"] = "true" // BYOND json won't let us get a real true, but OpenSearch is nice enough to accept it as string + . = null + + +/// Gets the correct time string for a given time +/datum/opensearch_query/proc/render_time_param(time_param) + SHOULD_BE_PURE(TRUE) + var/as_string = "[time_param]" + switch(time_mode) + if(OPENSEARCH_QUERY_TIME_MODE_RELATIVE) + return "now-[time_param]s" + else + CRASH("Unimplemented") + + +/// Callback for an errored HTTP request (probably at network level) +/datum/opensearch_query/proc/on_error(datum/http_response/response_object) + query_status = OPENSEARCH_QUERY_STATUS_FAILED + status_text = response_object.error + SStgui.update_uis(src) + +/// Callback for an request that was unsucessful at OpenSearch level +/datum/opensearch_query/proc/on_failure(datum/http_response/response_object) + query_status = OPENSEARCH_QUERY_STATUS_FAILED + var/list/parsed_response + try + parsed_response = json_decode(response_object.body) + catch + status_text = "Request failed, and failed to parse OpenSearch response" + return + var/list/error = parsed_response["error"] + if(error["root_cause"]) + status_text = "OpenSearch error: [json_encode(error["root_cause"])]" + return + status_text = "Unknown Opensearch Error" + SStgui.update_uis(src) + +/// Callback for a successful request +/datum/opensearch_query/proc/on_success(datum/http_response/response_object) + query_status = OPENSEARCH_QUERY_STATUS_SUCCESS + // We don't actually do anything here. For server performance reasons, we just pass the + // entire response object as string to TGUI, without any JSON encoding. + query_results = response_object.body + status_text = "Request successful" + SStgui.update_uis(src) + + +// ==== +// UI Work goes below +// ==== + +/datum/opensearch_query/tgui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if (!ui) + ui = new(user, src, "OpenSearchQuery", "Query &[id]") + ui.open() + +/datum/opensearch_query/ui_data(mob/user) + . = list() + .["queryId"] = id + .["queryName"] = name + .["queryStatus"] = query_status + .["queryResults"] = query_results + .["queryMode"] = query_mode + .["rankingMode"] = ranking_mode + .["queryTimeStart"] = time_start + .["queryTimeEnd"] = time_end + .["queryRoundId"] = target_roundid + .["userQuery"] = user_query + .["logTypes"] = log_types + .["statusText"] = status_text + +/datum/opensearch_query/ui_state(mob/user) + return GLOB.admin_state + +/datum/opensearch_query/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + . = ..() + if(.) + return FALSE + switch(action) + if("delete") + qdel(src) + if("update_query") + if(params["query_name"]) + name = params["query_name"] + if(params["query_mode"]) + query_mode = params["query_mode"] + if(params["ranking_mode"]) + ranking_mode = params["ranking_mode"] + if(params["query_time_start"]) + time_start = params["query_time_start"] + if(params["query_time_end"]) + time_end = params["query_time_end"] + if(!isnull(params["query_roundid"])) + target_roundid = params["query_roundid"] + if(!(text2num(target_roundid) < 1)) + target_roundid = null + if(params["user_query"]) + user_query = params["user_query"] + if(params["log_types"]) + log_types = params["log_types"] + if(params["execute"]) + execute() + SStgui.update_uis(src) + +// ==== +// DEBUG VERB, TODO FIXE make a more involved UI with selector +// ==== +/client/proc/opensearch_query_builder() + set name = "Open OpenSearch Query Builder" + set category = "Debug" + + var/datum/opensearch_query/query + if(!SSopensearch.queries[1]) + SSopensearch.new_query() + query = SSopensearch.queries[1] + query.tgui_interact(mob) diff --git a/code/modules/admin/admin_verbs.dm b/code/modules/admin/admin_verbs.dm index 86c8c3ceacee..90de0be3c009 100644 --- a/code/modules/admin/admin_verbs.dm +++ b/code/modules/admin/admin_verbs.dm @@ -221,6 +221,7 @@ GLOBAL_LIST_INIT(admin_verbs_debug, list( /client/proc/admin_blurb, /datum/admins/proc/open_shuttlepanel, /client/proc/allow_browser_inspect, + /client/proc/opensearch_query_builder )) GLOBAL_LIST_INIT(admin_verbs_debug_advanced, list( diff --git a/colonialmarines.dme b/colonialmarines.dme index 691301171f88..4c85396a746f 100644 --- a/colonialmarines.dme +++ b/colonialmarines.dme @@ -93,6 +93,7 @@ #include "code\__DEFINES\movement.dm" #include "code\__DEFINES\nightmare.dm" #include "code\__DEFINES\objects.dm" +#include "code\__DEFINES\opensearch.dm" #include "code\__DEFINES\origins.dm" #include "code\__DEFINES\pain.dm" #include "code\__DEFINES\paperwork.dm" @@ -305,6 +306,7 @@ #include "code\controllers\subsystem\nanoui.dm" #include "code\controllers\subsystem\nightmare.dm" #include "code\controllers\subsystem\objectives_controller.dm" +#include "code\controllers\subsystem\opensearch.dm" #include "code\controllers\subsystem\pager_status.dm" #include "code\controllers\subsystem\perf_logging.dm" #include "code\controllers\subsystem\ping.dm" @@ -660,6 +662,7 @@ #include "code\datums\looping_sounds\_looping_sound.dm" #include "code\datums\looping_sounds\item_sounds.dm" #include "code\datums\looping_sounds\misc_sounds.dm" +#include "code\datums\opensearch\opensearch_query.dm" #include "code\datums\origin\civilian.dm" #include "code\datums\origin\cmb.dm" #include "code\datums\origin\origin.dm" diff --git a/tgui/packages/tgui/interfaces/OpenSearchQuery.tsx b/tgui/packages/tgui/interfaces/OpenSearchQuery.tsx new file mode 100644 index 000000000000..635ae469c28e --- /dev/null +++ b/tgui/packages/tgui/interfaces/OpenSearchQuery.tsx @@ -0,0 +1,427 @@ +import { useBackend, useSharedState } from 'tgui/backend'; +import { + Box, + Button, + Collapsible, + Dropdown, + Input, + Section, + Stack, + TextArea, +} from 'tgui/components'; +import { Window } from 'tgui/layouts'; + +type Data = { + queryId: number; + queryName: string; + queryStatus: number; + queryResults: string; + queryMode: number; + rankingMode: boolean; + queryTimeStart: number; + queryTimeEnd: number; + queryRoundId: string; // String because it can be empty + userQuery: string; + logTypes: Array; + statusText: string; +}; + +const OPENSEARCH_QUERY_STATUS_READY = 1; +const OPENSEARCH_QUERY_STATUS_BUILD_ERROR = 2; +const OPENSEARCH_QUERY_STATUS_EXECUTING = 3; +const OPENSEARCH_QUERY_STATUS_FAILED = 4; +const OPENSEARCH_QUERY_STATUS_SUCCESS = 5; +const STATUS_TO_NAME = [ + 'INVALID', + 'READY', + 'BUILD ERROR', + 'RUNNING', + 'FAILED', + 'SUCCESS', +]; +const STATUS_TO_ICON = [ + 'circle-xmark', + 'check', + 'triangle-exclamation', + 'spinner', + 'circle-xmark', + 'check', +]; +const STATUS_TO_COLOR = ['bad', 'good', 'average', 'blue', 'bad', 'good']; + +const OPENSEARCH_QUERY_MODE_DSL_RAW = 0; +const OPENSEARCH_QUERY_MODE_DSL = 1; +const OPENSEARCH_QUERY_MODE_LUCENE = 2; + +const QUERY_MODE_TO_NAME = ['DSL (RAW)', 'DSL', 'Lucene']; +const DEFAULT_QUERY_MODE = 'Lucene'; + +const LOGTYPE_TO_COLOR = { + ATTACK: 'bad', + SAY: 'purple', + GAME: 'blue', + ADMIN: 'good', +}; + +export const OpenSearchQuery = (props) => { + const { act, data } = useBackend(); + const { + queryId, + queryName, + queryStatus, + queryResults, + queryMode, + rankingMode, + queryTimeStart, + queryTimeEnd, + queryRoundId, + userQuery, + logTypes, + statusText, + } = data; + + const [localQueryName, setQueryName] = useSharedState('queryName', queryName); + const [localQueryMode, setQueryMode] = useSharedState('queryMode', queryMode); + const [localRankingMode, setRankingMode] = useSharedState( + 'rankingMode', + rankingMode, + ); + const [localQueryTimeStart, setQueryTimeStart] = useSharedState( + 'queryTimeStart', + queryTimeStart, + ); + const [localQueryTimeEnd, setQueryTimeEnd] = useSharedState( + 'queryTimeEnd', + queryTimeEnd, + ); + const [localQueryRoundId, setQueryRoundId] = useSharedState( + 'queryRoundId', + queryRoundId, + ); + const [localUserQuery, setUserQuery] = useSharedState('userQuery', userQuery); + const [localLogTypes, setLogTypes] = useSharedState('logTypes', logTypes); + + const parsedResults = queryResults ? JSON.parse(queryResults) : {}; + const resultsTotal = parsedResults?.hits?.total?.value; + const resultsFetched = parsedResults?.hits?.hits?.length; + const timeElapsed = parsedResults?.took; + + const toggleLogType = (logType: string) => { + let newLogTypes = [...localLogTypes]; + if (newLogTypes.indexOf(logType) !== -1) { + newLogTypes = newLogTypes.filter((e) => e !== logType); + } else { + newLogTypes.push(logType); + } + setLogTypes(newLogTypes); + }; + + const submitQuery = (executeNow: boolean) => { + act('update_query', { + execute: executeNow, + query_name: localQueryName, + query_mode: localQueryMode, + ranking_mode: localRankingMode, + query_time_start: localQueryTimeStart, + query_time_end: localQueryTimeEnd, + // query_roundid: localQueryRoundId, + log_types: localLogTypes, + user_query: localUserQuery, + }); + }; + + const localTimeOffsetToNumber = (string) => { + const suffix = string[string.length - 1]; + let numericPart = parseInt(string, 10); + switch (suffix) { + case 'm': + numericPart *= 60; + break; + case 'h': + numericPart *= 60 * 60; + break; + case 'd': + numericPart *= 24 * 60 * 60; + break; + case 'w': + numericPart *= 7 * 24 * 60 * 60; + break; + } + return numericPart; + }; + + const timeOffsetToDisplay = (offset) => { + let suffix = 's'; + let remaining = offset; + if (remaining && remaining % 60 === 0) { + remaining /= 60; + suffix = 'm'; + } + if (remaining && remaining % 60 === 0) { + remaining /= 60; + suffix = 'h'; + } + if (remaining && remaining % 24 === 0) { + remaining /= 24; + suffix = 'd'; + } + if (remaining && remaining % 7 === 0) { + remaining /= 7; + suffix = 'w'; + } + return `${remaining}${suffix}`; + }; + + const generateLogElements = (results) => { + let parsed = JSON.parse(results); + let documents = parsed?.hits?.hits; + return ( + + {documents + ? documents.map((onedoc) => generateSingleLogElement(onedoc)) + : ''} + + ); + }; + + const generateSingleLogElement = (onedoc) => { + return ( + + + + {onedoc._id} + + } + > + {onedoc._source.message} + + + ); + }; + + return ( + + + + + + + + + + + + {/* + + + ID: &{queryId} + + + */} + + setQueryName(value)} + /> + + + + + + act('delete')} + color="bad" + confirmColor="bad" + tooltip="Delete the query" + > + Delete + + + + + + + + + setQueryMode(QUERY_MODE_TO_NAME.indexOf(selected)) + } + /> + + + setRankingMode(!localRankingMode)} + tooltip="In Ranking mode, return the most relevant queries at top, irrespective of time." + color={localRankingMode ? 'good' : 'blue'} + > + {localRankingMode ? 'Sort: Rank' : 'Sort: Time'} + + + + + toggleLogType('GAME')} + > + GAME + + + + toggleLogType('ADMIN')} + > + ADMIN + + + + toggleLogType('SAY')} + > + SAY + + + + toggleLogType('ATTACK')} + > + ATTACK + + + + {/* Round ID picker. Potentially don't need it because if you do you can open OpenSearch-Dashboards which does a better job than this UI } + + round: + + + setQueryRoundId(value)} + /> + + */} + + {/* Start time picker. Potentially don't need it if you're gonna look at the whole round anyway + + from: + + + { + setQueryTimeStart(localTimeOffsetToNumber(value)); + }} + /> + + */} + + + rewind: + + + + setQueryTimeEnd(localTimeOffsetToNumber(value)) + } + /> + + + + + + +