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

Add search result statistics on the main search screen #16130

Merged
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
5 changes: 5 additions & 0 deletions changelog/unreleased/issue-7386.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
type = "a"
message = "Add search result statistics on the main search screen and execution info popover to each dashboard widget"

pulls = ["16130"]
issues=["7386"]
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe('<Sidebar />', () => {
};
const errors = [];
const executionStats = { effective_timerange: effectiveTimerange, duration, timestamp };
const searchTypes = {};
const searchTypes = { 'search-type-id': { total: 12345, type: 'generic' } };
const queryResult = new QueryResult({ execution_stats: executionStats, query, errors, search_types: searchTypes });

const openDescriptionSection = async () => fireEvent.click(await screen.findByRole('button', { name: /description/i }));
Expand Down Expand Up @@ -171,15 +171,17 @@ describe('<Sidebar />', () => {

await screen.findByText('2018-08-28 16:34:26.192');
await screen.findByText('2018-08-28 16:39:26.192');
await screen.findByText(/total results *12,345/i);
});

it('should not render the effective search execution time range for dashboards without global override', async () => {
it('should not render the effective search execution time range and total result for dashboards without global override', async () => {
asMock(useViewType).mockReturnValue(View.Type.Dashboard);
renderSidebar();

fireEvent.click(await screen.findByRole('button', { name: /description/i }));

await screen.findByText('Varies per widget');
await screen.findByText((_content, node) => (node.textContent === 'Effective time rangeVaries per widget'));
await screen.findByText((_content, node) => (node.textContent === 'Total resultsVaries per widget'));
});

it('should render the effective search execution time range for dashboards with global override', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ import isEmpty from 'lodash/isEmpty';
import styled from 'styled-components';

import { Timestamp } from 'components/common';
import type { AbsoluteTimeRange } from 'views/logic/queries/Query';
import useGlobalOverride from 'views/hooks/useGlobalOverride';
import useViewType from 'views/hooks/useViewType';
import View from 'views/logic/views/View';
import type QueryResult from 'views/logic/QueryResult';

const EffectiveTimeRange = styled.div`
margin-bottom: 10px;
Expand All @@ -39,11 +39,7 @@ const EffectiveTimeRangeTable = styled.table`
`;

type Props = {
results: {
timestamp?: string,
duration?: number,
effectiveTimerange: AbsoluteTimeRange
},
results: QueryResult,
};

const SearchResultOverview = ({ results }: Props) => {
Expand All @@ -54,7 +50,9 @@ const SearchResultOverview = ({ results }: Props) => {
return <i>No query executed yet.</i>;
}

const { timestamp, duration, effectiveTimerange } = results;
const { timestamp, duration, effectiveTimerange, searchTypes } = results;
const total = searchTypes && Object.values(searchTypes)?.[0]?.total;
const isVariesPerWidget = (viewType === View.Type.Dashboard && !globalOverrideTimeRange);

return (
<>
Expand All @@ -64,22 +62,26 @@ const SearchResultOverview = ({ results }: Props) => {
</p>
<EffectiveTimeRange>
Effective time range<br />
{(viewType === View.Type.Dashboard && !globalOverrideTimeRange) ? <i>Varies per widget</i>
{isVariesPerWidget ? <i>Varies per widget</i>
: (
<EffectiveTimeRangeTable>
<tbody>
<tr>
<td>From</td>
<td><Timestamp dateTime={effectiveTimerange.from} format="complete" /></td>
<td aria-label="Effective time range from"><Timestamp dateTime={effectiveTimerange.from} format="complete" /></td>
</tr>
<tr>
<td>To</td>
<td><Timestamp dateTime={effectiveTimerange.to} format="complete" /></td>
<td aria-label="Effective time range to"><Timestamp dateTime={effectiveTimerange.to} format="complete" /></td>
</tr>
</tbody>
</EffectiveTimeRangeTable>
)}
</EffectiveTimeRange>
<p>
Total results<br />
{isVariesPerWidget ? <i>Varies per widget</i> : numeral(total).format('0,0')}
</p>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import React from 'react';
import numeral from 'numeral';
import isEmpty from 'lodash/isEmpty';

import useAppSelector from 'stores/useAppSelector';
import { selectCurrentQueryResults } from 'views/logic/slices/viewSelectors';
import { Timestamp } from 'components/common';

const ExecutionInfo = () => {
const result = useAppSelector(selectCurrentQueryResults);
const total = result?.searchTypes && Object.values(result?.searchTypes)?.[0]?.total;

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently we show "Query executed in 0ms" + "Total results: 0" when executing search for the first time.

Maybe let's unify it with the sidebar, where we show <i>No query executed yet.</i> in this case?

if (isEmpty(result)) {
return <i>No query executed yet.</i>;
}

return (
<i>
Query executed in{' '}
{numeral(result?.duration).format('0,0')}ms at <Timestamp dateTime={result?.timestamp} />
{' '}Total results: {numeral(total).format('0,0')}
</i>
);
};

export default ExecutionInfo;
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import useAlertAndEventDefinitionData from 'hooks/useAlertAndEventDefinitionData
import { updateView } from 'views/logic/slices/viewSlice';
import useIsNew from 'views/hooks/useIsNew';
import { createGRN } from 'logic/permissions/GRN';
import ExecutionInfo from 'views/components/views/ExecutionInfo';

const links = {
[View.Type.Dashboard]: ({ id, title }) => [{
Expand Down Expand Up @@ -84,6 +85,10 @@ const Content = styled.div(({ theme }) => css`
gap: 4px;
`);

const ExecutionInfoContainer = styled.div`
margin-left: auto;
`;

const EditButton = styled.div(({ theme }) => css`
color: ${theme.colors.gray[60]};
font-size: ${theme.fonts.size.tiny};
Expand Down Expand Up @@ -141,6 +146,8 @@ const ViewHeader = () => {
return links[view.type]({ id: view.id, title });
}, [alertId, definitionId, definitionTitle, isAlert, isEvent, isEventDefinition, view, title]);

const showExecutionInfo = view.type === 'SEARCH';

return (
<Row>
<Content>
Expand Down Expand Up @@ -175,6 +182,7 @@ const ViewHeader = () => {
onSave={_onSaveView}
submitButtonText={`Save ${typeText}`} />
)}
{showExecutionInfo && <ExecutionInfoContainer><ExecutionInfo /></ExecutionInfoContainer>}
</Content>
</Row>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
/*
* Copyright (C) 2020 Graylog, Inc.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the Server Side Public License, version 1,
* as published by MongoDB, Inc.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* Server Side Public License for more details.
*
* You should have received a copy of the Server Side Public License
* along with this program. If not, see
* <http://www.mongodb.com/licensing/server-side-public-license>.
*/
import * as React from 'react';
import styled, { css } from 'styled-components';
import type { PropsWithChildren } from 'react';
import { useCallback, useMemo, useState } from 'react';
import numeral from 'numeral';
import isEmpty from 'lodash/isEmpty';

import { Timestamp } from 'components/common';
import { Table } from 'components/bootstrap';
import useAppSelector from 'stores/useAppSelector';
import { selectCurrentQueryResults } from 'views/logic/slices/viewSelectors';
import type { MessageResult, SearchTypeResult, SearchTypeResultTypes } from 'views/types';
import type { SearchTypeIds } from 'views/logic/views/types';
import Popover from 'components/common/Popover';

type Props = PropsWithChildren & {
currentWidgetMapping: SearchTypeIds,
};

const TargetContainer = styled.div`
cursor: pointer;
`;

const StyledTable = styled(Table)<{ $stickyHeader: boolean }>(({ theme }) => css`
margin-bottom: 0;
background-color: transparent;
> tbody td {
background-color: ${theme.colors.global.contentBackground};
color: ${theme.utils.contrastingColor(theme.colors.global.contentBackground)};
}
`);

type WidgetExecutionData = {
total: number,
duration: number,
timestamp: string,
effectiveTimerange: SearchTypeResult['effective_timerange'] | MessageResult['effectiveTimerange']
}

const HelpPopover = ({ widgetExecutionData }: { widgetExecutionData: WidgetExecutionData}) => (
<StyledTable condensed>
<tbody>
<tr>
<td><i>Executed at:</i></td>
<td aria-label="Executed at"><Timestamp dateTime={widgetExecutionData?.timestamp} /></td>
</tr>
<tr>
<td><i>Executed in:</i> </td>
<td>{numeral(widgetExecutionData?.duration).format('0,0')}ms</td>
</tr>
<tr>
<td colSpan={2}><i>Effective time range:</i></td>
</tr>
<tr>
<td>From</td>
<td aria-label="Effective time range from"><Timestamp dateTime={widgetExecutionData?.effectiveTimerange?.from} format="complete" /></td>
</tr>
<tr>
<td>To</td>
<td aria-label="Effective time range to"><Timestamp dateTime={widgetExecutionData?.effectiveTimerange?.to} format="complete" /></td>
</tr>
<tr>
<td><i>Total results:</i></td>
<td>{numeral(widgetExecutionData?.total).format('0,0')}</td>
</tr>
</tbody>
</StyledTable>
);

const SearchQueryExecutionInfoHelper = ({ currentWidgetMapping, children }: Props) => {
const [open, setOpen] = useState(false);
const result = useAppSelector(selectCurrentQueryResults);
const currentWidgetSearchType = useMemo<SearchTypeResultTypes[keyof SearchTypeResultTypes]>(() => {
const searchTypeId = currentWidgetMapping?.toJS()?.[0];

return result?.searchTypes?.[searchTypeId];
}, [currentWidgetMapping, result?.searchTypes]);

const widgetExecutionData = useMemo<WidgetExecutionData>(() => ({
effectiveTimerange: (currentWidgetSearchType as MessageResult)?.effectiveTimerange || (currentWidgetSearchType as SearchTypeResult)?.effective_timerange,
total: currentWidgetSearchType?.total,
duration: result?.duration,
timestamp: result?.timestamp,

}), [currentWidgetSearchType, result?.duration, result?.timestamp]);

const onClose = useCallback(() => {
setOpen(false);
}, []);

const onToggle = useCallback(() => {
setOpen((cur) => !cur);
}, []);

return (
<Popover position="bottom" opened={open} onClose={onClose} closeOnClickOutside>
<Popover.Target>
<TargetContainer role="presentation" onClick={onToggle}>{children}</TargetContainer>
</Popover.Target>
<Popover.Dropdown title="Execution Info">
{isEmpty(result) ? <i>No query executed yet.</i> : <HelpPopover widgetExecutionData={widgetExecutionData} />}
</Popover.Dropdown>
</Popover>
);
};

export default SearchQueryExecutionInfoHelper;
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,15 @@
import * as React from 'react';
import styled, { css } from 'styled-components';

import { TextOverflowEllipsis } from 'components/common';
import { Icon, TextOverflowEllipsis } from 'components/common';
import type Widget from 'views/logic/widgets/Widget';
import timerangeToString from 'views/logic/queries/TimeRangeToString';
import { DEFAULT_TIMERANGE } from 'views/Constants';
import useUserDateTime from 'hooks/useUserDateTime';
import type { DateTime } from 'util/DateTime';
import useGlobalOverride from 'views/hooks/useGlobalOverride';
import useSearchResult from 'views/hooks/useSearchResult';
import SearchQueryExecutionInfoHelper from 'views/components/widgets/SearchQueryExecutionInfoHelper';

type Props = {
className?: string,
Expand All @@ -37,8 +38,15 @@ const Wrapper = styled.div(({ theme }) => css`
font-size: ${theme.fonts.size.tiny};
color: ${theme.colors.gray[30]};
width: max-content;
display: flex;
gap: 5px;
align-items: center;
margin-right: 10px;
`);

const StyledIcon = styled(Icon)(({ theme }) => css`
color: ${theme.colors.gray[60]}
`);
const getEffectiveWidgetTimerange = (result, activeQuery, searchTypeId) => result?.results?.[activeQuery]?.searchTypes[searchTypeId]?.effective_timerange;

const TimerangeInfo = ({ className, widget, activeQuery, widgetId }: Props) => {
Expand All @@ -58,12 +66,17 @@ const TimerangeInfo = ({ className, widget, activeQuery, widgetId }: Props) => {
const effectiveTimerange = (activeQuery && searchTypeId) ? getEffectiveWidgetTimerange(result, activeQuery, searchTypeId) : undefined;
const effectiveTimerangeString = effectiveTimerange ? timerangeToString(effectiveTimerange, toInternalTime) : 'Effective widget time range is currently not available.';

const currentWidgetMapping = widgetMapping?.get(widgetId);

return (
<Wrapper className={className}>
<TextOverflowEllipsis titleOverride={effectiveTimerangeString}>
{globalTimerangeString || configuredTimerange}
</TextOverflowEllipsis>
</Wrapper>
<SearchQueryExecutionInfoHelper currentWidgetMapping={currentWidgetMapping}>
<Wrapper className={className}>
<TextOverflowEllipsis titleOverride={effectiveTimerangeString}>
{globalTimerangeString || configuredTimerange}
</TextOverflowEllipsis>
<StyledIcon name="question-circle" />
</Wrapper>
</SearchQueryExecutionInfoHelper>
);
};

Expand Down
3 changes: 2 additions & 1 deletion graylog2-web-interface/src/views/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ export interface SystemConfiguration {

export type SearchTypeResult = {
type: string,
effective_timerange: TimeRange,
effective_timerange: AbsoluteTimeRange,
total: number,
};

export type MessageResult = {
Expand Down
Loading