Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@

import static org.graylog.events.event.EventDto.FIELD_ALERT;
import static org.graylog.events.event.EventDto.FIELD_PRIORITY;
import static org.graylog.events.event.EventDto.FIELD_SCORES;
import static org.graylog2.plugin.streams.Stream.DEFAULT_EVENTS_STREAM_ID;

public class EventsSearchService extends AbstractEventsSearchService {
Expand Down Expand Up @@ -124,37 +125,47 @@ public Slices slices(String query, TimeRange timeRange, Subject subject, SearchU
/**
* finding the overall count for one column for MongoDB based queries so we can calculate the "empty" case
*/
public Optional<Slice> count(String query, TimeRange timeRange, Subject subject, SearchUser searchUser, final String slicingColumn) {
public Optional<Slice> count(String query, TimeRange timeRange, Subject subject, SearchUser searchUser, final String type) {
try {
return scriptingApiService.executeAggregation(
new AggregationRequestSpec(query, allowedEventStreams(subject), Set.of(), timeRange, List.of(), List.of(new Metric("count", null))),
searchUser
)
.datarows()
.stream()
.map(r -> new Slice(r.getFirst().toString(), r.getFirst().toString(), Integer.valueOf(r.getLast().toString())))
.map(r -> new Slice(r.getFirst().toString(), r.getFirst().toString(), type, Integer.valueOf(r.getLast().toString())))
.findFirst();
} catch (QueryFailedException e) {
throw new RuntimeException(e);
}
}

public String getTypeBySliceColumn(final String column) {
return switch (column) {
case FIELD_PRIORITY -> FIELD_PRIORITY;
case FIELD_ALERT -> FIELD_ALERT;
default -> null;
};
}

/**
* In the Open Source part, we only map priority and type, both of which are augmented in the FE regarding the title.
* So we only need a simple mapping function here.
*/
public Slice mapAggregationResultsToSlice(final String slicingColumn, final List<Object> result) {
return new Slice(result.getFirst().toString(), null, Integer.valueOf(result.getLast().toString()));
return new Slice(result.getFirst().toString(), null, getTypeBySliceColumn(slicingColumn), Integer.valueOf(result.getLast().toString()));
}

// the alert can either be true or false
List<Slice> handleAlertColumn(final List<Slice> slices) {
final var type = getTypeBySliceColumn(FIELD_ALERT);

if (slices.size() == 2) {
return slices;
}

final var TRUE = new Slice("true", null, 0);
final var FALSE = new Slice("false", null, 0);
final var TRUE = new Slice("true", null, type, 0);
final var FALSE = new Slice("false", null, type, 0);

if (slices.isEmpty()) {
return List.of(TRUE, FALSE);
Expand All @@ -169,15 +180,17 @@ List<Slice> handleAlertColumn(final List<Slice> slices) {

// priority can be 0 (info) to 4 (critical), see EventDefinitionPriorityEnum.ts
List<Slice> handlePriorityColumn(final List<Slice> slices) {
final var type = getTypeBySliceColumn(FIELD_PRIORITY);

if (slices.size() == 5) {
return slices;
}

final var INFO = new Slice("0", null, 0);
final var LOW = new Slice("1", null, 0);
final var MEDIUM = new Slice("2", null, 0);
final var HIGH = new Slice("3", null, 0);
final var CRITICAL = new Slice("4", null, 0);
final var INFO = new Slice("0", null, type, 0);
final var LOW = new Slice("1", null, type, 0);
final var MEDIUM = new Slice("2", null, type, 0);
final var HIGH = new Slice("3", null, type, 0);
final var CRITICAL = new Slice("4", null, type, 0);

if (slices.isEmpty()) {
return List.of(INFO, LOW, MEDIUM, HIGH, CRITICAL);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@

import com.fasterxml.jackson.annotation.JsonProperty;

public record Slice(@JsonProperty(FIELD_ID) String value, @JsonProperty(FIELD_TITLE) String title, @JsonProperty(FIELD_COUNT) Integer count) {
public record Slice(@JsonProperty(FIELD_ID) String value, @JsonProperty(FIELD_TITLE) String title, @JsonProperty(FIELD_TYPE) String type, @JsonProperty(FIELD_COUNT) Integer count) {
private static final String FIELD_ID = "value";
private static final String FIELD_TITLE = "title";
private static final String FIELD_TYPE = "type";
private static final String FIELD_COUNT = "count";
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const SliceList = ({
onClick={() => onChangeSlicing(sliceCol, String(slice.value))}
active={String(activeSlice) === String(slice.value)}>
<SliceInner>
<Title>{sliceRenderers?.[sliceCol]?.render?.(slice.value) ?? slice.title ?? String(slice.value)}</Title>
<Title>{sliceRenderers?.[sliceCol]?.render?.(slice) ?? slice.title ?? String(slice.value)}</Title>

<CountBadge title={String(slice.count)}>{formatReadableNumber(slice.count)}</CountBadge>
</SliceInner>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@
*/

import * as React from 'react';
import { useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import styled, { css } from 'styled-components';

import { Button } from 'components/bootstrap';
import { Spinner } from 'components/common';
import { PaginatedList, Spinner } from 'components/common';
import { TELEMETRY_EVENT_TYPE } from 'logic/telemetry/Constants';
import useSendTelemetry from 'logic/telemetry/useSendTelemetry';

import SliceFilters, { type SortMode } from './SliceFilters';
import SliceList from './SliceList';
import useSlices from './useSlices';
import type { SliceRenderers } from './Slicing';
import type { SliceRenderers, Slices } from './Slicing';
import type { FetchSlices } from './useFetchSlices';

const EmptySlicesHeader = styled.div(
Expand All @@ -48,11 +48,19 @@ const EmptySlicesLabel = styled.span`
gap: 4px;
`;

const Slices = styled.div`
const SlicesLists = styled.div`
max-height: 700px;
overflow: auto;
`;

const SLICES_PAGE_SIZE = 10;

const paginatedSlices = (slices: Slices, page: number, pageSize: number) => {
const from = (page - 1) * pageSize;

return slices.slice(from, from + pageSize);
};

type Props = {
appSection: string;
sliceCol: string | undefined;
Expand All @@ -65,6 +73,47 @@ type Props = {
onSortModeChange: (mode: SortMode) => void;
};

type UseAutoExpandEmptySlicesArgs = {
activeSlice: string | undefined;
showEmptySlices: boolean;
visibleEmptySlices: Slices;
setShowEmptySlices: React.Dispatch<React.SetStateAction<boolean>>;
setEmptyPage: React.Dispatch<React.SetStateAction<number>>;
};

const useAutoExpandEmptySlices = ({
activeSlice,
showEmptySlices,
visibleEmptySlices,
setShowEmptySlices,
setEmptyPage,
}: UseAutoExpandEmptySlicesArgs) => {
const lastAutoExpandedSliceRef = useRef<string | undefined>(undefined);

useEffect(() => {
if (!activeSlice || showEmptySlices) {
return;
}

const activeSliceValue = String(activeSlice);
const activeSliceMovedToEmpty = visibleEmptySlices.some((slice) => String(slice.value) === activeSliceValue);

if (!activeSliceMovedToEmpty) {
lastAutoExpandedSliceRef.current = undefined;

return;
}

if (lastAutoExpandedSliceRef.current === activeSliceValue) {
return;
}

lastAutoExpandedSliceRef.current = activeSliceValue;
setShowEmptySlices(true);
setEmptyPage(1);
}, [activeSlice, showEmptySlices, visibleEmptySlices, setShowEmptySlices, setEmptyPage]);
};

const SlicesOverview = ({
appSection,
sliceCol,
Expand All @@ -78,13 +127,34 @@ const SlicesOverview = ({
}: Props) => {
const [showEmptySlices, setShowEmptySlices] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const [nonEmptyPage, setNonEmptyPage] = useState(1);
const [emptyPage, setEmptyPage] = useState(1);
const sendTelemetry = useSendTelemetry();
const { isLoading, hasEmptySlices, emptySliceCount, visibleNonEmptySlices, visibleEmptySlices } = useSlices({
fetchSlices,
searchQuery,
sortMode,
sliceRenderers,
});
const { isLoading, refetchSlices, hasEmptySlices, emptySliceCount, visibleNonEmptySlices, visibleEmptySlices } =
useSlices({
fetchSlices,
activeSlice,
searchQuery,
sortMode,
sliceRenderers,
});

useAutoExpandEmptySlices({ activeSlice, showEmptySlices, visibleEmptySlices, setShowEmptySlices, setEmptyPage });

const currentNonEmptySlices = paginatedSlices(visibleNonEmptySlices, nonEmptyPage, SLICES_PAGE_SIZE);
const currentEmptySlices = paginatedSlices(visibleEmptySlices, emptyPage, SLICES_PAGE_SIZE);

const onSearchQueryChange = (newQuery: string) => {
setSearchQuery(newQuery);
setNonEmptyPage(1);
setEmptyPage(1);
};
const onSortModeUpdate = (mode: SortMode) => {
onSortModeChange(mode);
setNonEmptyPage(1);
setEmptyPage(1);
};

const onToggleEmptySlices = () => {
setShowEmptySlices((current) => {
const next = !current;
Expand All @@ -97,10 +167,22 @@ const SlicesOverview = ({
},
});

if (next) {
setEmptyPage(1);
}

return next;
});
};

const onSliceSelection = (newSliceCol: string | undefined, newSlice?: string | undefined) => {
onChangeSlicing(newSliceCol, newSlice);

if (newSlice !== undefined && newSlice !== activeSlice) {
void refetchSlices();
}
};

if (isLoading) {
return <Spinner />;
}
Expand All @@ -112,20 +194,32 @@ const SlicesOverview = ({
activeColumnTitle={activeColumnTitle}
sliceCol={sliceCol}
searchQuery={searchQuery}
onSearchQueryChange={setSearchQuery}
onSearchReset={() => setSearchQuery('')}
onSearchQueryChange={onSearchQueryChange}
onSearchReset={() => onSearchQueryChange('')}
sortMode={sortMode}
onSortModeChange={onSortModeChange}
onSortModeChange={onSortModeUpdate}
/>
<Slices>
<SliceList
slices={visibleNonEmptySlices}
activeSlice={activeSlice}
sliceCol={sliceCol}
onChangeSlicing={onChangeSlicing}
sliceRenderers={sliceRenderers}
listTestId="slices-list"
/>
<SlicesLists>
<PaginatedList
activePage={nonEmptyPage}
pageSize={SLICES_PAGE_SIZE}
totalItems={visibleNonEmptySlices.length}
showPageSizeSelect={false}
hideFirstAndLastPageLinks
useQueryParameter={false}
onChange={(newPage, pageSize) => {
void pageSize;
setNonEmptyPage(newPage);
}}>
<SliceList
slices={currentNonEmptySlices}
activeSlice={activeSlice}
sliceCol={sliceCol}
onChangeSlicing={onSliceSelection}
sliceRenderers={sliceRenderers}
listTestId="slices-list"
/>
</PaginatedList>
<EmptySlicesHeader>
{hasEmptySlices ? (
<Button
Expand All @@ -140,17 +234,29 @@ const SlicesOverview = ({
)}
</EmptySlicesHeader>
{showEmptySlices && visibleEmptySlices.length > 0 && (
<SliceList
slices={visibleEmptySlices}
activeSlice={activeSlice}
sliceCol={sliceCol}
onChangeSlicing={onChangeSlicing}
sliceRenderers={sliceRenderers}
keyPrefix="empty-"
listTestId="empty-slices-list"
/>
<PaginatedList
activePage={emptyPage}
hideFirstAndLastPageLinks
pageSize={SLICES_PAGE_SIZE}
totalItems={visibleEmptySlices.length}
showPageSizeSelect={false}
useQueryParameter={false}
onChange={(newPage, pageSize) => {
void pageSize;
setEmptyPage(newPage);
}}>
<SliceList
slices={currentEmptySlices}
activeSlice={activeSlice}
sliceCol={sliceCol}
onChangeSlicing={onSliceSelection}
sliceRenderers={sliceRenderers}
keyPrefix="empty-"
listTestId="empty-slices-list"
/>
</PaginatedList>
)}
</Slices>
</SlicesLists>
</>
);
};
Expand Down
Loading
Loading