mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
feat(rca): add date range filter (#189527)
Resolves https://github.com/elastic/kibana/issues/189511 ## Summary Add the global date range filter. I've refactored a bit the layout as well to follow a bit more the design. The notes are still appended in the main timeline: this needs to be refactored entirely. 
This commit is contained in:
parent
7e0dab954b
commit
ee055ba500
8 changed files with 256 additions and 141 deletions
|
@ -4,7 +4,6 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { AuthenticatedUser } from '@kbn/core/public';
|
||||
import type { GlobalWidgetParameters, OnWidgetAdd } from '@kbn/investigate-plugin/public';
|
||||
import React from 'react';
|
||||
|
@ -16,11 +15,5 @@ type AddWidgetUIProps = {
|
|||
} & GlobalWidgetParameters;
|
||||
|
||||
export function AddNoteUI({ user, onWidgetAdd }: AddWidgetUIProps) {
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexItem grow={false}>
|
||||
<NoteWidgetControl user={user} onWidgetAdd={onWidgetAdd} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
return <NoteWidgetControl user={user} onWidgetAdd={onWidgetAdd} />;
|
||||
}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
import { useKibana } from '../hooks/use_kibana';
|
||||
|
||||
const pageSectionContentClassName = css`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
max-block-size: calc(100vh - 96px);
|
||||
`;
|
||||
|
||||
export function InvestigatePageTemplate({ children }: { children: React.ReactNode }) {
|
||||
const {
|
||||
dependencies: {
|
||||
start: { observabilityShared },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const { PageTemplate } = observabilityShared.navigation;
|
||||
|
||||
return (
|
||||
<PageTemplate
|
||||
children={children}
|
||||
pageSectionProps={{
|
||||
alignment: 'horizontalCenter',
|
||||
contentProps: {
|
||||
className: pageSectionContentClassName,
|
||||
},
|
||||
paddingSize: 'none',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { css } from '@emotion/css';
|
||||
import type { TimeRange } from '@kbn/es-query';
|
||||
import { SearchBar } from '@kbn/unified-search-plugin/public';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
|
||||
const parentClassName = css`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
onQuerySubmit: (payload: { dateRange: TimeRange }, isUpdate?: boolean) => void;
|
||||
onRefresh?: Required<React.ComponentProps<typeof SearchBar>>['onRefresh'];
|
||||
onFocus?: () => void;
|
||||
onBlur?: () => void;
|
||||
showSubmitButton?: boolean;
|
||||
}
|
||||
|
||||
export function InvestigateSearchBar({
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
onQuerySubmit,
|
||||
onRefresh,
|
||||
onFocus,
|
||||
onBlur,
|
||||
showSubmitButton = true,
|
||||
}: Props) {
|
||||
const {
|
||||
dependencies: {
|
||||
start: { unifiedSearch },
|
||||
},
|
||||
} = useKibana();
|
||||
const [element, setElement] = useState<HTMLElement | null>(null);
|
||||
|
||||
const onBlurRef = useRef(onBlur);
|
||||
onBlurRef.current = onBlur;
|
||||
|
||||
const onFocusRef = useRef(onFocus);
|
||||
onFocusRef.current = onFocus;
|
||||
|
||||
useEffect(() => {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
let inFocus = false;
|
||||
|
||||
function updateFocus(activeElement: Element | null | undefined) {
|
||||
const thisElementContainsActiveElement = activeElement && element?.contains(activeElement);
|
||||
|
||||
let nextInFocus = Boolean(thisElementContainsActiveElement);
|
||||
|
||||
if (!nextInFocus) {
|
||||
const popoverContent = document.querySelector(
|
||||
'[data-test-subj=superDatePickerQuickMenu], .euiDatePopoverContent, .kbnTypeahead'
|
||||
);
|
||||
|
||||
nextInFocus = Boolean(
|
||||
activeElement &&
|
||||
activeElement !== document.body &&
|
||||
(activeElement === popoverContent ||
|
||||
activeElement?.contains(popoverContent) ||
|
||||
popoverContent?.contains(activeElement))
|
||||
);
|
||||
}
|
||||
|
||||
if (inFocus !== nextInFocus) {
|
||||
inFocus = Boolean(nextInFocus);
|
||||
|
||||
if (inFocus) {
|
||||
onFocusRef.current?.();
|
||||
} else {
|
||||
onBlurRef.current?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function captureFocus() {
|
||||
updateFocus(document.activeElement);
|
||||
}
|
||||
|
||||
function captureBlur(event: FocusEvent) {
|
||||
updateFocus(event.relatedTarget as Element | null);
|
||||
}
|
||||
|
||||
window.addEventListener('focus', captureFocus, true);
|
||||
|
||||
window.addEventListener('blur', captureBlur, true);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('focus', captureFocus);
|
||||
window.removeEventListener('blur', captureBlur);
|
||||
};
|
||||
}, [element]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={parentClassName}
|
||||
ref={(nextElement) => {
|
||||
setElement(nextElement);
|
||||
}}
|
||||
>
|
||||
<unifiedSearch.ui.SearchBar
|
||||
appName="investigate"
|
||||
onQuerySubmit={({ dateRange }) => {
|
||||
onQuerySubmit({ dateRange });
|
||||
}}
|
||||
showQueryInput={false}
|
||||
showFilterBar={false}
|
||||
showQueryMenu={false}
|
||||
showDatePicker
|
||||
showSubmitButton={showSubmitButton}
|
||||
dateRangeFrom={rangeFrom}
|
||||
dateRangeTo={rangeTo}
|
||||
onRefresh={onRefresh}
|
||||
displayStyle="inPage"
|
||||
disableQueryLanguageSwitcher
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,9 +4,8 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import datemath from '@elastic/datemath';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { css } from '@emotion/css';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { InvestigateWidget, InvestigateWidgetCreate } from '@kbn/investigate-plugin/public';
|
||||
import { DATE_FORMAT_ID } from '@kbn/management-settings-ids';
|
||||
import { AuthenticatedUser } from '@kbn/security-plugin/common';
|
||||
|
@ -18,28 +17,9 @@ import { useKibana } from '../../hooks/use_kibana';
|
|||
import { getOverridesFromGlobalParameters } from '../../utils/get_overrides_from_global_parameters';
|
||||
import { AddNoteUI } from '../add_note_ui';
|
||||
import { AddObservationUI } from '../add_observation_ui';
|
||||
import { InvestigateSearchBar } from '../investigate_search_bar';
|
||||
import { InvestigateWidgetGrid } from '../investigate_widget_grid';
|
||||
|
||||
const containerClassName = css`
|
||||
overflow: auto;
|
||||
padding: 24px 24px 24px 24px;
|
||||
`;
|
||||
|
||||
const scrollContainerClassName = css`
|
||||
min-width: 1px;
|
||||
`;
|
||||
|
||||
const gridContainerClassName = css`
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
const sideBarClassName = css`
|
||||
width: 240px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
padding: 0px 12px 32px 12px;
|
||||
`;
|
||||
|
||||
function InvestigateViewWithUser({ user }: { user: AuthenticatedUser }) {
|
||||
const {
|
||||
core: { uiSettings },
|
||||
|
@ -47,10 +27,9 @@ function InvestigateViewWithUser({ user }: { user: AuthenticatedUser }) {
|
|||
start: { investigate },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const widgetDefinitions = useMemo(() => investigate.getWidgetDefinitions(), [investigate]);
|
||||
|
||||
const [range, setRange] = useDateRange();
|
||||
const [searchBarFocused, setSearchBarFocused] = useState(false);
|
||||
|
||||
const {
|
||||
addItem,
|
||||
|
@ -61,6 +40,7 @@ function InvestigateViewWithUser({ user }: { user: AuthenticatedUser }) {
|
|||
investigation,
|
||||
lockItem,
|
||||
setItemParameters,
|
||||
setGlobalParameters,
|
||||
unlockItem,
|
||||
revision,
|
||||
} = investigate.useInvestigation({
|
||||
|
@ -76,7 +56,6 @@ function InvestigateViewWithUser({ user }: { user: AuthenticatedUser }) {
|
|||
};
|
||||
|
||||
const createWidgetRef = useRef(createWidget);
|
||||
|
||||
createWidgetRef.current = createWidget;
|
||||
|
||||
useEffect(() => {
|
||||
|
@ -138,18 +117,41 @@ function InvestigateViewWithUser({ user }: { user: AuthenticatedUser }) {
|
|||
});
|
||||
}, [revision, widgetDefinitions, uiSettings]);
|
||||
|
||||
const [searchBarFocused] = useState(false);
|
||||
|
||||
if (!investigation || !revision || !gridItems) {
|
||||
return <EuiLoadingSpinner />;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" className={containerClassName}>
|
||||
<EuiFlexItem grow className={scrollContainerClassName}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s" justifyContent="flexEnd">
|
||||
<EuiFlexGroup direction="row">
|
||||
<EuiFlexItem grow={8}>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
<EuiFlexItem className={gridContainerClassName} grow={false}>
|
||||
<EuiFlexItem>
|
||||
<InvestigateSearchBar
|
||||
rangeFrom={range.from}
|
||||
rangeTo={range.to}
|
||||
onQuerySubmit={async ({ dateRange }) => {
|
||||
const nextDateRange = {
|
||||
from: datemath.parse(dateRange.from)!.toISOString(),
|
||||
to: datemath.parse(dateRange.to)!.toISOString(),
|
||||
};
|
||||
await setGlobalParameters({
|
||||
...revision.parameters,
|
||||
timeRange: nextDateRange,
|
||||
});
|
||||
|
||||
setRange(nextDateRange);
|
||||
}}
|
||||
onFocus={() => {
|
||||
setSearchBarFocused(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setSearchBarFocused(false);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false}>
|
||||
<InvestigateWidgetGrid
|
||||
items={gridItems}
|
||||
onItemsChange={async (nextGridItems) => {
|
||||
|
@ -189,17 +191,8 @@ function InvestigateViewWithUser({ user }: { user: AuthenticatedUser }) {
|
|||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<AddNoteUI
|
||||
user={user}
|
||||
filters={revision.parameters.filters}
|
||||
timeRange={revision.parameters.timeRange}
|
||||
onWidgetAdd={(widget) => {
|
||||
return createWidgetRef.current(widget);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
||||
<AddObservationUI
|
||||
filters={revision.parameters.filters}
|
||||
timeRange={revision.parameters.timeRange}
|
||||
|
@ -210,11 +203,15 @@ function InvestigateViewWithUser({ user }: { user: AuthenticatedUser }) {
|
|||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
|
||||
<EuiFlexItem grow={false} className={sideBarClassName}>
|
||||
{i18n.translate(
|
||||
'xpack.investigateApp.investigateViewWithUser.placeholderForRightSidebarFlexItemLabel',
|
||||
{ defaultMessage: 'placeholder for right sidebar' }
|
||||
)}
|
||||
<EuiFlexItem grow={2}>
|
||||
<AddNoteUI
|
||||
user={user}
|
||||
filters={revision.parameters.filters}
|
||||
timeRange={revision.parameters.timeRange}
|
||||
onWidgetAdd={(widget) => {
|
||||
return createWidgetRef.current(widget);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { InvestigateWidgetCreate } from '@kbn/investigate-plugin/common';
|
||||
import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import type { AuthenticatedUser } from '@kbn/core/public';
|
||||
import { ResizableTextInput } from '../resizable_text_input';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { InvestigateWidgetCreate } from '@kbn/investigate-plugin/common';
|
||||
import React, { useState } from 'react';
|
||||
import { createNoteWidget } from '../../widgets/note_widget/create_note_widget';
|
||||
import { ResizableTextInput } from '../resizable_text_input';
|
||||
|
||||
interface NoteWidgetControlProps {
|
||||
user: Pick<AuthenticatedUser, 'full_name' | 'username'>;
|
||||
|
@ -46,8 +46,8 @@ export function NoteWidgetControl({ user, onWidgetAdd }: NoteWidgetControlProps)
|
|||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
|
||||
<EuiFlexItem grow>
|
||||
<EuiFlexGroup direction="column" gutterSize="s" justifyContent="flexStart">
|
||||
<EuiFlexItem grow={false}>
|
||||
<ResizableTextInput
|
||||
placeholder={i18n.translate('xpack.investigateApp.noteWidgetControl.placeholder', {
|
||||
defaultMessage: 'Add a note to the investigation',
|
||||
|
@ -63,20 +63,24 @@ export function NoteWidgetControl({ user, onWidgetAdd }: NoteWidgetControlProps)
|
|||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonIcon
|
||||
<EuiButton
|
||||
data-test-subj="investigateAppNoteWidgetControlButton"
|
||||
aria-label={i18n.translate('xpack.investigateApp.noteWidgetControl.submitLabel', {
|
||||
defaultMessage: 'Submit',
|
||||
fullWidth
|
||||
color="text"
|
||||
aria-label={i18n.translate('xpack.investigateApp.noteWidgetControl.addButtonLabel', {
|
||||
defaultMessage: 'Add',
|
||||
})}
|
||||
disabled={loading || note.trim() === ''}
|
||||
display="base"
|
||||
iconType="kqlFunction"
|
||||
isLoading={loading}
|
||||
size="m"
|
||||
onClick={() => {
|
||||
submit();
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{i18n.translate('xpack.investigateApp.noteWidgetControl.addButtonLabel', {
|
||||
defaultMessage: 'Add',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
|
|
|
@ -65,7 +65,7 @@ export function ResizableTextInput({ disabled, value, onChange, onSubmit, placeh
|
|||
inputRef={textAreaRef}
|
||||
placeholder={placeholder}
|
||||
resize="vertical"
|
||||
rows={1}
|
||||
rows={4}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
onKeyDown={(event) => {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiButton } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { InvestigateView } from '../../components/investigate_view';
|
||||
import { useKibana } from '../../hooks/use_kibana';
|
||||
|
||||
export function InvestigateDetailsPage() {
|
||||
const {
|
||||
dependencies: {
|
||||
start: { observabilityShared },
|
||||
},
|
||||
} = useKibana();
|
||||
|
||||
const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate;
|
||||
|
||||
return (
|
||||
<ObservabilityPageTemplate
|
||||
pageHeader={{
|
||||
pageTitle: i18n.translate('xpack.investigateApp.detailsPage.title', {
|
||||
defaultMessage: 'New investigation',
|
||||
}),
|
||||
rightSideItems: [
|
||||
<EuiButton fill data-test-subj="investigateAppInvestigateDetailsPageEscalateButton">
|
||||
{i18n.translate('xpack.investigateApp.investigateDetailsPage.escalateButtonLabel', {
|
||||
defaultMessage: 'Escalate',
|
||||
})}
|
||||
</EuiButton>,
|
||||
],
|
||||
}}
|
||||
>
|
||||
<InvestigateView />
|
||||
</ObservabilityPageTemplate>
|
||||
);
|
||||
}
|
|
@ -4,50 +4,40 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { createRouter } from '@kbn/typed-react-router-config';
|
||||
import * as t from 'io-ts';
|
||||
import { createRouter, Outlet } from '@kbn/typed-react-router-config';
|
||||
import React from 'react';
|
||||
import { Redirect } from 'react-router-dom';
|
||||
import { InvestigatePageTemplate } from '../components/investigate_page_template';
|
||||
import { InvestigateView } from '../components/investigate_view';
|
||||
import { InvestigateDetailsPage } from '../pages/details';
|
||||
|
||||
/**
|
||||
* The array of route definitions to be used when the application
|
||||
* creates the routes.
|
||||
*/
|
||||
const investigateRoutes = {
|
||||
'/': {
|
||||
element: (
|
||||
<InvestigatePageTemplate>
|
||||
<Outlet />
|
||||
</InvestigatePageTemplate>
|
||||
),
|
||||
children: {
|
||||
'/new': {
|
||||
element: <InvestigateView />,
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
revision: t.string,
|
||||
}),
|
||||
'/new': {
|
||||
element: <InvestigateDetailsPage />,
|
||||
params: t.partial({
|
||||
query: t.partial({
|
||||
revision: t.string,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
'/{id}': {
|
||||
element: <InvestigateDetailsPage />,
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
path: t.type({ id: t.string }),
|
||||
}),
|
||||
t.partial({
|
||||
query: t.partial({
|
||||
revision: t.string,
|
||||
}),
|
||||
},
|
||||
'/{id}': {
|
||||
element: <InvestigateView />,
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
path: t.type({ id: t.string }),
|
||||
}),
|
||||
t.partial({
|
||||
query: t.partial({
|
||||
revision: t.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
'/': {
|
||||
element: <Redirect to="/new" />,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]),
|
||||
},
|
||||
'/': {
|
||||
element: <Redirect to="/new" />,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue