mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[RAM] Alerts Table- Inspect Modal (#149586)
## Summary
closes https://github.com/elastic/kibana/issues/149411
⚠️ you'll see it working in o11y but it's actually not going to be
activated there. It's just to be able to see working while siem hasn't
merged their alerts table migration. This feature is implemented so it
can be used there.
To test this add a new prop called `showInspectButton: true` to [o11y
alerts table
registry](92cb000a2f/x-pack/plugins/observability/public/config/register_alerts_table_configuration.tsx (L43)
).
This will activate it for o11y
https://user-images.githubusercontent.com/17549662/215747436-f33a069c-22a3-4a75-8db5-41689e1085be.mov
### Checklist
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
This commit is contained in:
parent
eafbc47468
commit
1b8960bee6
17 changed files with 867 additions and 107 deletions
|
@ -34,4 +34,11 @@ export type EcsFieldsResponse = BasicFields & {
|
|||
} & {
|
||||
[x: string]: unknown[];
|
||||
};
|
||||
export type RuleRegistrySearchResponse = IEsSearchResponse<EcsFieldsResponse>;
|
||||
|
||||
export interface RuleRegistryInspect {
|
||||
dsl: string[];
|
||||
}
|
||||
|
||||
export interface RuleRegistrySearchResponse extends IEsSearchResponse<EcsFieldsResponse> {
|
||||
inspect?: RuleRegistryInspect;
|
||||
}
|
||||
|
|
|
@ -49,6 +49,7 @@ export const ruleRegistrySearchStrategyProvider = (
|
|||
// is different than every other solution so we need to special case
|
||||
// those requests.
|
||||
let siemRequest = false;
|
||||
let params = {};
|
||||
if (request.featureIds.length === 1 && request.featureIds[0] === AlertConsumers.SIEM) {
|
||||
siemRequest = true;
|
||||
} else if (request.featureIds.includes(AlertConsumers.SIEM)) {
|
||||
|
@ -123,7 +124,7 @@ export const ruleRegistrySearchStrategyProvider = (
|
|||
}),
|
||||
};
|
||||
const size = request.pagination ? request.pagination.pageSize : MAX_ALERT_SEARCH_SIZE;
|
||||
const params = {
|
||||
params = {
|
||||
allow_no_indices: true,
|
||||
index: indices,
|
||||
ignore_unavailable: true,
|
||||
|
@ -143,7 +144,7 @@ export const ruleRegistrySearchStrategyProvider = (
|
|||
deps
|
||||
);
|
||||
}),
|
||||
map((response) => {
|
||||
map((response: RuleRegistrySearchResponse) => {
|
||||
// Do we have to loop over each hit? Yes.
|
||||
// ecs auditLogger requires that we log each alert independently
|
||||
if (securityAuditLogger != null) {
|
||||
|
@ -157,6 +158,14 @@ export const ruleRegistrySearchStrategyProvider = (
|
|||
);
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
response.inspect = { dsl: [JSON.stringify(params)] };
|
||||
} catch (error) {
|
||||
logger.error(`Failed to stringify rule registry search strategy params: ${error}`);
|
||||
response.inspect = { dsl: [] };
|
||||
}
|
||||
|
||||
return response;
|
||||
}),
|
||||
catchError((err) => {
|
||||
|
|
|
@ -28,6 +28,7 @@ import { AlertLifecycleStatusBadge } from '../../components/alert_lifecycle_stat
|
|||
|
||||
import './alerts_table.scss';
|
||||
import { getToolbarVisibility } from './toolbar';
|
||||
import { InspectButtonContainer } from './toolbar/components/inspect';
|
||||
|
||||
export const ACTIVE_ROW_CLASS = 'alertsTableActiveRow';
|
||||
|
||||
|
@ -83,6 +84,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
|||
onPageChange,
|
||||
onSortChange,
|
||||
sort: sortingFields,
|
||||
getInspectQuery,
|
||||
} = alertsData;
|
||||
const { sortingColumns, onSort } = useSorting(onSortChange, sortingFields);
|
||||
|
||||
|
@ -123,6 +125,7 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
|||
browserFields,
|
||||
onChangeVisibleColumns,
|
||||
showAlertStatusWithFlapping = false,
|
||||
showInspectButton = false,
|
||||
} = props;
|
||||
|
||||
// TODO when every solution is using this table, we will be able to simplify it by just passing the alert index
|
||||
|
@ -151,6 +154,8 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
|||
browserFields,
|
||||
controls: props.controls,
|
||||
setIsBulkActionsLoading,
|
||||
getInspectQuery,
|
||||
showInspectButton,
|
||||
});
|
||||
}, [
|
||||
bulkActionsState,
|
||||
|
@ -165,6 +170,8 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
|||
browserFields,
|
||||
props.controls,
|
||||
setIsBulkActionsLoading,
|
||||
getInspectQuery,
|
||||
showInspectButton,
|
||||
])();
|
||||
|
||||
const leadingControlColumns = useMemo(() => {
|
||||
|
@ -298,43 +305,45 @@ const AlertsTable: React.FunctionComponent<AlertsTableProps> = (props: AlertsTab
|
|||
);
|
||||
|
||||
return (
|
||||
<section style={{ width: '100%' }} data-test-subj={props['data-test-subj']}>
|
||||
<Suspense fallback={null}>
|
||||
{flyoutAlertIndex > -1 && (
|
||||
<AlertsFlyout
|
||||
alert={alerts[flyoutAlertIndex]}
|
||||
alertsCount={alertsCount}
|
||||
onClose={handleFlyoutClose}
|
||||
alertsTableConfiguration={props.alertsTableConfiguration}
|
||||
flyoutIndex={flyoutAlertIndex + pagination.pageIndex * pagination.pageSize}
|
||||
onPaginate={onPaginateFlyout}
|
||||
isLoading={isLoading}
|
||||
id={props.id}
|
||||
<InspectButtonContainer>
|
||||
<section style={{ width: '100%' }} data-test-subj={props['data-test-subj']}>
|
||||
<Suspense fallback={null}>
|
||||
{flyoutAlertIndex > -1 && (
|
||||
<AlertsFlyout
|
||||
alert={alerts[flyoutAlertIndex]}
|
||||
alertsCount={alertsCount}
|
||||
onClose={handleFlyoutClose}
|
||||
alertsTableConfiguration={props.alertsTableConfiguration}
|
||||
flyoutIndex={flyoutAlertIndex + pagination.pageIndex * pagination.pageSize}
|
||||
onPaginate={onPaginateFlyout}
|
||||
isLoading={isLoading}
|
||||
id={props.id}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
{alertsCount > 0 && (
|
||||
<EuiDataGrid
|
||||
aria-label="Alerts table"
|
||||
data-test-subj="alertsTable"
|
||||
columns={props.columns}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns: onChangeVisibleColumns }}
|
||||
trailingControlColumns={props.trailingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
rowCount={alertsCount}
|
||||
renderCellValue={handleRenderCellValue}
|
||||
gridStyle={{ ...GridStyles, rowClasses }}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
pagination={{
|
||||
...pagination,
|
||||
pageSizeOptions: props.pageSizeOptions,
|
||||
onChangeItemsPerPage: onChangePageSize,
|
||||
onChangePage: onChangePageIndex,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Suspense>
|
||||
{alertsCount > 0 && (
|
||||
<EuiDataGrid
|
||||
aria-label="Alerts table"
|
||||
data-test-subj="alertsTable"
|
||||
columns={props.columns}
|
||||
columnVisibility={{ visibleColumns, setVisibleColumns: onChangeVisibleColumns }}
|
||||
trailingControlColumns={props.trailingControlColumns}
|
||||
leadingControlColumns={leadingControlColumns}
|
||||
rowCount={alertsCount}
|
||||
renderCellValue={handleRenderCellValue}
|
||||
gridStyle={{ ...GridStyles, rowClasses }}
|
||||
sorting={{ columns: sortingColumns, onSort }}
|
||||
toolbarVisibility={toolbarVisibility}
|
||||
pagination={{
|
||||
...pagination,
|
||||
pageSizeOptions: props.pageSizeOptions,
|
||||
onChangeItemsPerPage: onChangePageSize,
|
||||
onChangePage: onChangePageIndex,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
</InspectButtonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import React from 'react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { get } from 'lodash';
|
||||
import { fireEvent, render, waitFor } from '@testing-library/react';
|
||||
import { fireEvent, render, waitFor, screen } from '@testing-library/react';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -166,6 +166,26 @@ describe('AlertsTableState', () => {
|
|||
showExpandToDetails: true,
|
||||
};
|
||||
|
||||
const mockCustomProps = (customProps: Partial<AlertsTableConfigurationRegistry>) => {
|
||||
const getMockWithUsePersistentControls = jest.fn().mockImplementation((plugin: string) => {
|
||||
return {
|
||||
...{
|
||||
columns,
|
||||
sort: DefaultSort,
|
||||
},
|
||||
...customProps,
|
||||
};
|
||||
});
|
||||
const alertsTableConfigurationRegistryWithPersistentControlsMock = {
|
||||
has: hasMock,
|
||||
get: getMockWithUsePersistentControls,
|
||||
} as unknown as TypeRegistry<AlertsTableConfigurationRegistry>;
|
||||
return {
|
||||
...tableProps,
|
||||
alertsTableConfigurationRegistry: alertsTableConfigurationRegistryWithPersistentControlsMock,
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
hasMock.mockClear();
|
||||
getMock.mockClear();
|
||||
|
@ -340,32 +360,28 @@ describe('AlertsTableState', () => {
|
|||
).toBe(AlertsField.uuid);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when persistent controls are set', () => {
|
||||
let props: AlertsTableStateProps;
|
||||
beforeEach(() => {
|
||||
const getMockWithUsePersistentControls = jest.fn().mockImplementation((plugin: string) => {
|
||||
return {
|
||||
columns,
|
||||
sort: DefaultSort,
|
||||
usePersistentControls: () => ({ right: <span>This is a persistent control</span> }),
|
||||
};
|
||||
});
|
||||
const alertsTableConfigurationRegistryWithPersistentControlsMock = {
|
||||
has: hasMock,
|
||||
get: getMockWithUsePersistentControls,
|
||||
} as unknown as TypeRegistry<AlertsTableConfigurationRegistry>;
|
||||
props = {
|
||||
...tableProps,
|
||||
alertsTableConfigurationRegistry:
|
||||
alertsTableConfigurationRegistryWithPersistentControlsMock,
|
||||
};
|
||||
describe('persistent controls', () => {
|
||||
it('should show persistent controls if set', () => {
|
||||
const props = mockCustomProps({
|
||||
usePersistentControls: () => ({ right: <span>This is a persistent control</span> }),
|
||||
});
|
||||
const result = render(<AlertsTableWithLocale {...props} />);
|
||||
expect(result.getByText('This is a persistent control')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('should show persistent controls if set', () => {
|
||||
const result = render(<AlertsTableWithLocale {...props} />);
|
||||
expect(result.getByText('This is a persistent control')).toBeInTheDocument();
|
||||
});
|
||||
describe('inspect button', () => {
|
||||
it('should hide the inspect button by default', () => {
|
||||
render(<AlertsTableWithLocale {...tableProps} />);
|
||||
expect(screen.queryByTestId('inspect-icon-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the inspect button if the right prop is set', async () => {
|
||||
const props = mockCustomProps({ showInspectButton: true });
|
||||
render(<AlertsTableWithLocale {...props} />);
|
||||
expect(await screen.findByTestId('inspect-icon-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -390,25 +406,22 @@ describe('AlertsTableState', () => {
|
|||
expect(result.getByTestId('alertsStateTableEmptyState')).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('inspect button', () => {
|
||||
it('should hide the inspect button by default', () => {
|
||||
render(<AlertsTableWithLocale {...tableProps} />);
|
||||
expect(screen.queryByTestId('inspect-icon-button')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should show the inspect button if the right prop is set', async () => {
|
||||
const props = mockCustomProps({ showInspectButton: true });
|
||||
render(<AlertsTableWithLocale {...props} />);
|
||||
expect(await screen.findByTestId('inspect-icon-button')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when persisten controls are set', () => {
|
||||
let props: AlertsTableStateProps;
|
||||
beforeEach(() => {
|
||||
const getMockWithUsePersistentControls = jest.fn().mockImplementation((plugin: string) => {
|
||||
return {
|
||||
columns,
|
||||
sort: DefaultSort,
|
||||
usePersistentControls: () => ({ right: <span>This is a persistent control</span> }),
|
||||
};
|
||||
});
|
||||
const alertsTableConfigurationRegistryWithPersistentControlsMock = {
|
||||
has: hasMock,
|
||||
get: getMockWithUsePersistentControls,
|
||||
} as unknown as TypeRegistry<AlertsTableConfigurationRegistry>;
|
||||
props = {
|
||||
...tableProps,
|
||||
alertsTableConfigurationRegistry:
|
||||
alertsTableConfigurationRegistryWithPersistentControlsMock,
|
||||
};
|
||||
const props = mockCustomProps({
|
||||
usePersistentControls: () => ({ right: <span>This is a persistent control</span> }),
|
||||
});
|
||||
|
||||
it('should show persistent controls if set', () => {
|
||||
|
|
|
@ -38,6 +38,7 @@ import { TypeRegistry } from '../../type_registry';
|
|||
import { bulkActionsReducer } from './bulk_actions/reducer';
|
||||
import { useGetUserCasesPermissions } from './hooks/use_get_user_cases_permissions';
|
||||
import { useColumns } from './hooks/use_columns';
|
||||
import { InspectButtonContainer } from './toolbar/components/inspect';
|
||||
|
||||
const DefaultPagination = {
|
||||
pageSize: 10,
|
||||
|
@ -110,6 +111,7 @@ const AlertsTableState = ({
|
|||
const storage = useRef(new Storage(window.localStorage));
|
||||
const localAlertsTableConfig = storage.current.get(id) as Partial<AlertsTableStorage>;
|
||||
const persistentControls = alertsTableConfiguration?.usePersistentControls?.();
|
||||
const showInspectButton = alertsTableConfiguration?.showInspectButton ?? false;
|
||||
|
||||
const columnsLocal =
|
||||
localAlertsTableConfig &&
|
||||
|
@ -260,6 +262,7 @@ const AlertsTableState = ({
|
|||
onColumnsChange,
|
||||
onChangeVisibleColumns,
|
||||
controls: persistentControls,
|
||||
showInspectButton,
|
||||
}),
|
||||
[
|
||||
alertsTableConfiguration,
|
||||
|
@ -278,6 +281,7 @@ const AlertsTableState = ({
|
|||
onColumnsChange,
|
||||
onChangeVisibleColumns,
|
||||
persistentControls,
|
||||
showInspectButton,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -286,7 +290,15 @@ const AlertsTableState = ({
|
|||
|
||||
return hasAlertsTableConfiguration ? (
|
||||
<>
|
||||
{!isLoading && alertsCount === 0 && <EmptyState controls={persistentControls} />}
|
||||
{!isLoading && alertsCount === 0 && (
|
||||
<InspectButtonContainer>
|
||||
<EmptyState
|
||||
controls={persistentControls}
|
||||
getInspectQuery={getInspectQuery}
|
||||
showInpectButton={showInspectButton}
|
||||
/>
|
||||
</InspectButtonContainer>
|
||||
)}
|
||||
{(isLoading || isBrowserFieldDataLoading) && (
|
||||
<EuiProgress size="xs" color="accent" data-test-subj="internalAlertsPageLoading" />
|
||||
)}
|
||||
|
|
|
@ -61,7 +61,7 @@ describe('AlertsTable.BulkActions', () => {
|
|||
alertsCount: alerts.length,
|
||||
isInitializing: false,
|
||||
isLoading: false,
|
||||
getInspectQuery: () => ({ request: {}, response: {} }),
|
||||
getInspectQuery: () => ({ request: [], response: [] }),
|
||||
onColumnsChange: () => {},
|
||||
onPageChange: () => {},
|
||||
onSortChange: () => {},
|
||||
|
|
|
@ -16,7 +16,9 @@ import {
|
|||
EuiDataGridToolBarAdditionalControlsOptions,
|
||||
} from '@elastic/eui';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { GetInspectQuery } from '../../../types';
|
||||
import icon from './assets/illustration_product_no_results_magnifying_glass.svg';
|
||||
import { InspectButton } from './toolbar/components/inspect';
|
||||
|
||||
const heights = {
|
||||
tall: 490,
|
||||
|
@ -30,14 +32,19 @@ const panelStyle = {
|
|||
export const EmptyState: React.FC<{
|
||||
height?: keyof typeof heights;
|
||||
controls?: EuiDataGridToolBarAdditionalControlsOptions;
|
||||
}> = ({ height = 'tall', controls }) => {
|
||||
getInspectQuery: GetInspectQuery;
|
||||
showInpectButton?: boolean;
|
||||
}> = ({ height = 'tall', controls, getInspectQuery, showInpectButton }) => {
|
||||
return (
|
||||
<EuiPanel color="subdued" data-test-subj="alertsStateTableEmptyState">
|
||||
{controls?.right && (
|
||||
<EuiFlexGroup alignItems="flexEnd" justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>{controls.right}</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
)}
|
||||
<EuiFlexGroup alignItems="flexEnd" justifyContent="flexEnd">
|
||||
{showInpectButton && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<InspectButton getInspectQuery={getInspectQuery} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{controls?.right && <EuiFlexItem grow={false}>{controls.right}</EuiFlexItem>}
|
||||
</EuiFlexGroup>
|
||||
<EuiFlexGroup style={{ height: heights[height] }} alignItems="center" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPanel hasBorder={true} style={panelStyle}>
|
||||
|
|
|
@ -22,6 +22,7 @@ import type {
|
|||
QueryDslQueryContainer,
|
||||
SortCombinations,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { GetInspectQuery, InspectQuery } from '../../../../types';
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
import { DefaultSort } from './constants';
|
||||
import * as i18n from './translations';
|
||||
|
@ -42,12 +43,6 @@ type AlertRequest = Omit<FetchAlertsArgs, 'featureIds' | 'skip'>;
|
|||
|
||||
type Refetch = () => void;
|
||||
|
||||
interface InspectQuery {
|
||||
request: string[];
|
||||
response: string[];
|
||||
}
|
||||
type GetInspectQuery = () => InspectQuery;
|
||||
|
||||
export interface FetchAlertResp {
|
||||
alerts: EcsFieldsResponse[];
|
||||
isInitializing: boolean;
|
||||
|
@ -183,12 +178,12 @@ const useFetchAlerts = ({
|
|||
}
|
||||
)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
next: (response: RuleRegistrySearchResponse) => {
|
||||
if (isCompleteResponse(response)) {
|
||||
const { rawResponse } = response;
|
||||
inspectQuery.current = {
|
||||
request: [],
|
||||
response: [],
|
||||
request: response?.inspect?.dsl ?? [],
|
||||
response: [JSON.stringify(rawResponse)] ?? [],
|
||||
};
|
||||
let totalAlerts = 0;
|
||||
if (rawResponse.hits.total && typeof rawResponse.hits.total === 'number') {
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import { HoverVisibilityContainer } from '.';
|
||||
|
||||
describe('HoverVisibilityContainer', () => {
|
||||
const targetClass1 = 'Component1';
|
||||
const targetClass2 = 'Component2';
|
||||
const Component1 = () => <div className={targetClass1} data-test-subj="component1" />;
|
||||
const Component2 = () => <div className={targetClass2} data-test-subj="component2" />;
|
||||
|
||||
test('it renders a transparent inspect button by default', async () => {
|
||||
render(
|
||||
<HoverVisibilityContainer targetClassNames={[targetClass1, targetClass2]}>
|
||||
<Component1 />
|
||||
<Component2 />
|
||||
</HoverVisibilityContainer>
|
||||
);
|
||||
|
||||
expect(await screen.findByTestId('hoverVisibilityContainer')).toHaveStyleRule('opacity', '0', {
|
||||
modifier: `.${targetClass1}`,
|
||||
});
|
||||
|
||||
expect(await screen.getByTestId(`hoverVisibilityContainer`)).toHaveStyleRule('opacity', '1', {
|
||||
modifier: `:hover .${targetClass2}`,
|
||||
});
|
||||
});
|
||||
|
||||
test('it renders an opaque inspect button when it has mouse focus', async () => {
|
||||
render(
|
||||
<HoverVisibilityContainer targetClassNames={[targetClass1, targetClass2]}>
|
||||
<Component1 />
|
||||
<Component2 />
|
||||
</HoverVisibilityContainer>
|
||||
);
|
||||
expect(await screen.findByTestId('hoverVisibilityContainer')).toHaveStyleRule('opacity', '1', {
|
||||
modifier: `:hover .${targetClass1}`,
|
||||
});
|
||||
expect(await screen.findByTestId('hoverVisibilityContainer')).toHaveStyleRule('opacity', '1', {
|
||||
modifier: `:hover .${targetClass2}`,
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { getOr } from 'lodash/fp';
|
||||
|
||||
interface StyledDivProps {
|
||||
targetClassNames: string[];
|
||||
}
|
||||
|
||||
const StyledDiv = euiStyled.div<StyledDivProps>`
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
> * {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
${({ targetClassNames, theme }) => {
|
||||
return `
|
||||
${targetClassNames.map((cn) => `.${cn}`).join(', ')} {
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity ${getOr(250, 'eui.euiAnimSpeedNormal', theme)} ease;
|
||||
}
|
||||
|
||||
${targetClassNames.map((cn) => `&:hover .${cn}`).join(', ')} {
|
||||
pointer-events: auto;
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
interface HoverVisibilityContainerProps {
|
||||
hide?: boolean;
|
||||
children: React.ReactNode;
|
||||
targetClassNames: string[];
|
||||
}
|
||||
|
||||
export const HoverVisibilityContainer = React.memo<HoverVisibilityContainerProps>(
|
||||
({ hide, targetClassNames, children }) => {
|
||||
if (hide) return <>{children}</>;
|
||||
|
||||
return (
|
||||
<StyledDiv data-test-subj="hoverVisibilityContainer" targetClassNames={targetClassNames}>
|
||||
{children}
|
||||
</StyledDiv>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
HoverVisibilityContainer.displayName = 'HoverVisibilityContainer';
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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 { render, screen, cleanup, fireEvent } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { InspectButton } from '.';
|
||||
|
||||
jest.mock('./modal', () => ({
|
||||
ModalInspectQuery: jest.fn(() => <div data-test-subj="mocker-modal" />),
|
||||
}));
|
||||
|
||||
describe('Inspect Button', () => {
|
||||
const getInspectQuery = () => {
|
||||
return {
|
||||
request: [''],
|
||||
response: [''],
|
||||
};
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
test('open Inspect Modal', async () => {
|
||||
render(<InspectButton showInspectButton getInspectQuery={getInspectQuery} />);
|
||||
fireEvent.click(await screen.findByTestId('inspect-icon-button'));
|
||||
|
||||
expect(await screen.findByTestId('mocker-modal')).toBeInTheDocument();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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 { EuiButtonIcon } from '@elastic/eui';
|
||||
import React, { useState } from 'react';
|
||||
import { GetInspectQuery } from '../../../../../../types';
|
||||
|
||||
import { HoverVisibilityContainer } from './hover_visibility_container';
|
||||
|
||||
import { ModalInspectQuery } from './modal';
|
||||
import * as i18n from './translations';
|
||||
|
||||
export const BUTTON_CLASS = 'inspectButtonComponent';
|
||||
|
||||
interface InspectButtonContainerProps {
|
||||
hide?: boolean;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const InspectButtonContainer: React.FC<InspectButtonContainerProps> = ({
|
||||
children,
|
||||
hide,
|
||||
}) => (
|
||||
<HoverVisibilityContainer hide={hide} targetClassNames={[BUTTON_CLASS]}>
|
||||
{children}
|
||||
</HoverVisibilityContainer>
|
||||
);
|
||||
|
||||
interface InspectButtonProps {
|
||||
onCloseInspect?: () => void;
|
||||
showInspectButton?: boolean;
|
||||
getInspectQuery: GetInspectQuery;
|
||||
}
|
||||
|
||||
const InspectButtonComponent: React.FC<InspectButtonProps> = ({ getInspectQuery }) => {
|
||||
const [isShowingModal, setIsShowingModal] = useState(false);
|
||||
|
||||
const onOpenModal = () => {
|
||||
setIsShowingModal(true);
|
||||
};
|
||||
|
||||
const onCloseModal = () => {
|
||||
setIsShowingModal(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonIcon
|
||||
className={BUTTON_CLASS}
|
||||
aria-label={i18n.INSPECT}
|
||||
data-test-subj="inspect-icon-button"
|
||||
iconSize="m"
|
||||
iconType="inspect"
|
||||
title={i18n.INSPECT}
|
||||
onClick={onOpenModal}
|
||||
/>
|
||||
{isShowingModal && (
|
||||
<ModalInspectQuery
|
||||
closeModal={onCloseModal}
|
||||
data-test-subj="inspect-modal"
|
||||
getInspectQuery={getInspectQuery}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
InspectButtonComponent.displayName = 'InspectButtonComponent';
|
||||
export const InspectButton = React.memo(InspectButtonComponent);
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { EuiThemeProvider } from '@kbn/kibana-react-plugin/common';
|
||||
import type { ModalInspectProps } from './modal';
|
||||
import { ModalInspectQuery } from './modal';
|
||||
|
||||
jest.mock('react-router-dom', () => {
|
||||
const original = jest.requireActual('react-router-dom');
|
||||
|
||||
return {
|
||||
...original,
|
||||
useLocation: jest.fn().mockReturnValue([{ pathname: '/overview' }]),
|
||||
};
|
||||
});
|
||||
|
||||
const getRequest = (
|
||||
indices: string[] = ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*']
|
||||
) =>
|
||||
`{"index": ${JSON.stringify(
|
||||
indices
|
||||
)},"allowNoIndices": true, "ignoreUnavailable": true, "body": { "aggregations": {"hosts": {"cardinality": {"field": "host.name" } }, "hosts_histogram": {"auto_date_histogram": {"field": "@timestamp","buckets": "6"},"aggs": { "count": {"cardinality": {"field": "host.name" }}}}}, "query": {"bool": {"filter": [{"range": { "@timestamp": {"gte": 1562290224506,"lte": 1562376624506 }}}]}}, "size": 0, "track_total_hits": false}}`;
|
||||
|
||||
const response =
|
||||
'{"took": 880,"timed_out": false,"_shards": {"total": 26,"successful": 26,"skipped": 0,"failed": 0},"hits": {"max_score": null,"hits": []},"aggregations": {"hosts": {"value": 541},"hosts_histogram": {"buckets": [{"key_as_string": "2019 - 07 - 05T01: 00: 00.000Z", "key": 1562288400000, "doc_count": 1492321, "count": { "value": 105 }}, {"key_as_string": "2019 - 07 - 05T13: 00: 00.000Z", "key": 1562331600000, "doc_count": 2412761, "count": { "value": 453}},{"key_as_string": "2019 - 07 - 06T01: 00: 00.000Z", "key": 1562374800000, "doc_count": 111658, "count": { "value": 15}}],"interval": "12h"}},"status": 200}';
|
||||
|
||||
describe('Modal Inspect', () => {
|
||||
const closeModal = jest.fn();
|
||||
const defaultProps: ModalInspectProps = {
|
||||
closeModal,
|
||||
getInspectQuery: () => ({
|
||||
request: [getRequest()],
|
||||
response: [response],
|
||||
}),
|
||||
};
|
||||
|
||||
const renderModalInspectQuery = () => {
|
||||
return render(<ModalInspectQuery {...defaultProps} />, {
|
||||
wrapper: ({ children }) => <EuiThemeProvider>{children}</EuiThemeProvider>,
|
||||
});
|
||||
};
|
||||
|
||||
describe('functionality from tab statistics/request/response', () => {
|
||||
test('Click on statistic Tab', async () => {
|
||||
renderModalInspectQuery();
|
||||
fireEvent.click(await screen.findByText('Statistics'));
|
||||
|
||||
expect(await screen.findByText('Index pattern')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('index-pattern-description').textContent).toBe(
|
||||
'auditbeat-*, filebeat-*, packetbeat-*, winlogbeat-*'
|
||||
);
|
||||
expect(screen.getByTestId('query-time-title').textContent).toContain('Query time ');
|
||||
expect(screen.getByTestId('query-time-description').textContent).toBe('880ms');
|
||||
expect(screen.getByTestId('request-timestamp-title').textContent).toContain(
|
||||
'Request timestamp '
|
||||
);
|
||||
});
|
||||
|
||||
test('Click on request Tab', async () => {
|
||||
renderModalInspectQuery();
|
||||
|
||||
fireEvent.click(await screen.findByText('Request'));
|
||||
|
||||
const el = await screen.findByRole('tabpanel');
|
||||
|
||||
expect(JSON.parse(el.textContent ?? '')).toEqual({
|
||||
index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'],
|
||||
allowNoIndices: true,
|
||||
ignoreUnavailable: true,
|
||||
body: {
|
||||
aggregations: {
|
||||
hosts: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
hosts_histogram: {
|
||||
auto_date_histogram: {
|
||||
field: '@timestamp',
|
||||
buckets: '6',
|
||||
},
|
||||
aggs: {
|
||||
count: {
|
||||
cardinality: {
|
||||
field: 'host.name',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: 1562290224506,
|
||||
lte: 1562376624506,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
size: 0,
|
||||
track_total_hits: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Click on response Tab', async () => {
|
||||
renderModalInspectQuery();
|
||||
|
||||
fireEvent.click(await screen.findByText('Response'));
|
||||
const el = await screen.findByRole('tabpanel');
|
||||
|
||||
expect(JSON.parse(el.textContent ?? '')).toEqual({
|
||||
took: 880,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 26,
|
||||
successful: 26,
|
||||
skipped: 0,
|
||||
failed: 0,
|
||||
},
|
||||
hits: {
|
||||
max_score: null,
|
||||
hits: [],
|
||||
},
|
||||
aggregations: {
|
||||
hosts: {
|
||||
value: 541,
|
||||
},
|
||||
hosts_histogram: {
|
||||
buckets: [
|
||||
{
|
||||
key_as_string: '2019 - 07 - 05T01: 00: 00.000Z',
|
||||
key: 1562288400000,
|
||||
doc_count: 1492321,
|
||||
count: {
|
||||
value: 105,
|
||||
},
|
||||
},
|
||||
{
|
||||
key_as_string: '2019 - 07 - 05T13: 00: 00.000Z',
|
||||
key: 1562331600000,
|
||||
doc_count: 2412761,
|
||||
count: {
|
||||
value: 453,
|
||||
},
|
||||
},
|
||||
{
|
||||
key_as_string: '2019 - 07 - 06T01: 00: 00.000Z',
|
||||
key: 1562374800000,
|
||||
doc_count: 111658,
|
||||
count: {
|
||||
value: 15,
|
||||
},
|
||||
},
|
||||
],
|
||||
interval: '12h',
|
||||
},
|
||||
},
|
||||
status: 200,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
test('Make sure that toggle function has been called when you click on the close button', async () => {
|
||||
renderModalInspectQuery();
|
||||
|
||||
fireEvent.click(await screen.findByTestId('modal-inspect-close'));
|
||||
expect(closeModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,219 @@
|
|||
/*
|
||||
* 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,
|
||||
EuiCodeBlock,
|
||||
EuiDescriptionList,
|
||||
EuiIconTip,
|
||||
EuiModal,
|
||||
EuiModalBody,
|
||||
EuiModalHeader,
|
||||
EuiModalHeaderTitle,
|
||||
EuiModalFooter,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
} from '@elastic/eui';
|
||||
import numeral from '@elastic/numeral';
|
||||
import { ReactNode } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { euiStyled, EuiTheme } from '@kbn/kibana-react-plugin/common';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { GetInspectQuery } from '../../../../../../types';
|
||||
import * as i18n from './translations';
|
||||
|
||||
const DescriptionListStyled = euiStyled(EuiDescriptionList)`
|
||||
@media only screen and (min-width: ${({ theme }: { theme: EuiTheme }) =>
|
||||
theme.eui.euiBreakpoints.s}) {
|
||||
.euiDescriptionList__title {
|
||||
width: 30% !important;
|
||||
}
|
||||
|
||||
.euiDescriptionList__description {
|
||||
width: 70% !important;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
DescriptionListStyled.displayName = 'DescriptionListStyled';
|
||||
|
||||
export interface ModalInspectProps {
|
||||
closeModal: () => void;
|
||||
getInspectQuery: GetInspectQuery;
|
||||
}
|
||||
|
||||
interface Request {
|
||||
index: string[];
|
||||
allowNoIndices: boolean;
|
||||
ignoreUnavailable: boolean;
|
||||
body: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface Response {
|
||||
took: number;
|
||||
timed_out: boolean;
|
||||
_shards: Record<string, unknown>;
|
||||
hits: Record<string, unknown>;
|
||||
aggregations: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const MyEuiModal = euiStyled(EuiModal)`
|
||||
width: min(768px, calc(100vw - 16px));
|
||||
min-height: 41vh;
|
||||
.euiModal__flex {
|
||||
width: 60vw;
|
||||
}
|
||||
.euiCodeBlock {
|
||||
height: auto !important;
|
||||
max-width: 718px;
|
||||
}
|
||||
`;
|
||||
|
||||
MyEuiModal.displayName = 'MyEuiModal';
|
||||
const parse = function <T>(str: string): T {
|
||||
try {
|
||||
return JSON.parse(str);
|
||||
} catch {
|
||||
return {} as T;
|
||||
}
|
||||
};
|
||||
|
||||
const stringify = (object: Request | Response): string => {
|
||||
try {
|
||||
return JSON.stringify(object, null, 2);
|
||||
} catch {
|
||||
return i18n.SOMETHING_WENT_WRONG;
|
||||
}
|
||||
};
|
||||
|
||||
const ModalInspectQueryComponent = ({ closeModal, getInspectQuery }: ModalInspectProps) => {
|
||||
const { request, response } = getInspectQuery();
|
||||
// using index 0 as there will be only one request and response for now
|
||||
const parsedRequest: Request = parse(request[0]);
|
||||
const parsedResponse: Response = parse(response[0]);
|
||||
const formattedRequest = stringify(parsedRequest);
|
||||
const formattedResponse = stringify(parsedResponse);
|
||||
|
||||
const statistics: Array<{
|
||||
title: NonNullable<ReactNode | string>;
|
||||
description: NonNullable<ReactNode | string>;
|
||||
}> = [
|
||||
{
|
||||
title: (
|
||||
<span data-test-subj="index-pattern-title">
|
||||
{i18n.INDEX_PATTERN}{' '}
|
||||
<EuiIconTip color="subdued" content={i18n.INDEX_PATTERN_DESC} type="iInCircle" />
|
||||
</span>
|
||||
),
|
||||
description: (
|
||||
<span data-test-subj="index-pattern-description">
|
||||
<p>{parsedRequest.index?.join(', ') ?? []}</p>
|
||||
</span>
|
||||
),
|
||||
},
|
||||
|
||||
{
|
||||
title: (
|
||||
<span data-test-subj="query-time-title">
|
||||
{i18n.QUERY_TIME}{' '}
|
||||
<EuiIconTip color="subdued" content={i18n.QUERY_TIME_DESC} type="iInCircle" />
|
||||
</span>
|
||||
),
|
||||
description: (
|
||||
<span data-test-subj="query-time-description">
|
||||
{parsedResponse.took === 0
|
||||
? '0ms'
|
||||
: parsedResponse.took
|
||||
? `${numeral(parsedResponse.took).format('0,0')}ms`
|
||||
: i18n.SOMETHING_WENT_WRONG}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: (
|
||||
<span data-test-subj="request-timestamp-title">
|
||||
{i18n.REQUEST_TIMESTAMP}{' '}
|
||||
<EuiIconTip color="subdued" content={i18n.REQUEST_TIMESTAMP_DESC} type="iInCircle" />
|
||||
</span>
|
||||
),
|
||||
description: (
|
||||
<span data-test-subj="request-timestamp-description">{new Date().toISOString()}</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
const tabs = [
|
||||
{
|
||||
id: 'statistics',
|
||||
name: 'Statistics',
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<DescriptionListStyled listItems={statistics} type="column" />
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'request',
|
||||
name: 'Request',
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiCodeBlock
|
||||
language="js"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
color="dark"
|
||||
overflowHeight={300}
|
||||
isCopyable
|
||||
>
|
||||
{isEmpty(parsedRequest) ? i18n.SOMETHING_WENT_WRONG : formattedRequest}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'response',
|
||||
name: 'Response',
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiCodeBlock
|
||||
language="js"
|
||||
fontSize="m"
|
||||
paddingSize="m"
|
||||
color="dark"
|
||||
overflowHeight={300}
|
||||
isCopyable
|
||||
>
|
||||
{isEmpty(parsedResponse) ? i18n.SOMETHING_WENT_WRONG : formattedResponse}
|
||||
</EuiCodeBlock>
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<MyEuiModal onClose={closeModal} data-test-subj="modal-inspect-euiModal">
|
||||
<EuiModalHeader>
|
||||
<EuiModalHeaderTitle>{i18n.INSPECT}</EuiModalHeaderTitle>
|
||||
</EuiModalHeader>
|
||||
|
||||
<EuiModalBody>
|
||||
<EuiTabbedContent tabs={tabs} initialSelectedTab={tabs[0]} autoFocus="selected" />
|
||||
</EuiModalBody>
|
||||
|
||||
<EuiModalFooter>
|
||||
<EuiButton onClick={closeModal} fill data-test-subj="modal-inspect-close">
|
||||
{i18n.CLOSE}
|
||||
</EuiButton>
|
||||
</EuiModalFooter>
|
||||
</MyEuiModal>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalInspectQuery = React.memo(ModalInspectQueryComponent);
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { i18n } from '@kbn/i18n';
|
||||
|
||||
export const INSPECT = i18n.translate('xpack.triggersActionsUI.inspectDescription', {
|
||||
defaultMessage: 'Inspect',
|
||||
});
|
||||
|
||||
export const CLOSE = i18n.translate('xpack.triggersActionsUI.inspect.modal.closeTitle', {
|
||||
defaultMessage: 'Close',
|
||||
});
|
||||
|
||||
export const SOMETHING_WENT_WRONG = i18n.translate(
|
||||
'xpack.triggersActionsUI.inspect.modal.somethingWentWrongDescription',
|
||||
{
|
||||
defaultMessage: 'Sorry about that, something went wrong.',
|
||||
}
|
||||
);
|
||||
export const INDEX_PATTERN = i18n.translate(
|
||||
'xpack.triggersActionsUI.inspect.modal.indexPatternLabel',
|
||||
{
|
||||
defaultMessage: 'Index pattern',
|
||||
}
|
||||
);
|
||||
|
||||
export const INDEX_PATTERN_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.inspect.modal.indexPatternDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'The index pattern that connected to the Elasticsearch indices. These indices can be configured in Kibana > Advanced Settings.',
|
||||
}
|
||||
);
|
||||
|
||||
export const QUERY_TIME = i18n.translate('xpack.triggersActionsUI.inspect.modal.queryTimeLabel', {
|
||||
defaultMessage: 'Query time',
|
||||
});
|
||||
|
||||
export const QUERY_TIME_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.inspect.modal.queryTimeDescription',
|
||||
{
|
||||
defaultMessage:
|
||||
'The time it took to process the query. Does not include the time to send the request or parse it in the browser.',
|
||||
}
|
||||
);
|
||||
|
||||
export const REQUEST_TIMESTAMP = i18n.translate(
|
||||
'xpack.triggersActionsUI.inspect.modal.reqTimestampLabel',
|
||||
{
|
||||
defaultMessage: 'Request timestamp',
|
||||
}
|
||||
);
|
||||
|
||||
export const REQUEST_TIMESTAMP_DESC = i18n.translate(
|
||||
'xpack.triggersActionsUI.inspect.modal.reqTimestampDescription',
|
||||
{
|
||||
defaultMessage: 'Time when the start of the request has been logged',
|
||||
}
|
||||
);
|
|
@ -13,24 +13,32 @@ import { EcsFieldsResponse } from '@kbn/rule-registry-plugin/common/search_strat
|
|||
import React, { lazy, Suspense } from 'react';
|
||||
import { BrowserFields } from '@kbn/rule-registry-plugin/common';
|
||||
import { AlertsCount } from './components/alerts_count/alerts_count';
|
||||
import { BulkActionsConfig, RowSelection } from '../../../../types';
|
||||
import { BulkActionsConfig, GetInspectQuery, RowSelection } from '../../../../types';
|
||||
import { LastUpdatedAt } from './components/last_updated_at';
|
||||
import { FieldBrowser } from '../../field_browser';
|
||||
import { InspectButton } from './components/inspect';
|
||||
|
||||
const BulkActionsToolbar = lazy(() => import('../bulk_actions/components/toolbar'));
|
||||
|
||||
const rightControl = ({
|
||||
controls,
|
||||
updatedAt,
|
||||
getInspectQuery,
|
||||
showInspectButton,
|
||||
}: {
|
||||
controls?: EuiDataGridToolBarAdditionalControlsOptions;
|
||||
updatedAt: number;
|
||||
}) => (
|
||||
<>
|
||||
<LastUpdatedAt updatedAt={updatedAt} />
|
||||
{controls?.right}
|
||||
</>
|
||||
);
|
||||
getInspectQuery: GetInspectQuery;
|
||||
showInspectButton: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
{showInspectButton && <InspectButton getInspectQuery={getInspectQuery} />}
|
||||
<LastUpdatedAt updatedAt={updatedAt} />
|
||||
{controls?.right}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getDefaultVisibility = ({
|
||||
alertsCount,
|
||||
|
@ -40,6 +48,8 @@ const getDefaultVisibility = ({
|
|||
onResetColumns,
|
||||
browserFields,
|
||||
controls,
|
||||
getInspectQuery,
|
||||
showInspectButton,
|
||||
}: {
|
||||
alertsCount: number;
|
||||
updatedAt: number;
|
||||
|
@ -48,10 +58,12 @@ const getDefaultVisibility = ({
|
|||
onResetColumns: () => void;
|
||||
browserFields: BrowserFields;
|
||||
controls?: EuiDataGridToolBarAdditionalControlsOptions;
|
||||
getInspectQuery: GetInspectQuery;
|
||||
showInspectButton: boolean;
|
||||
}): EuiDataGridToolBarVisibilityOptions => {
|
||||
const hasBrowserFields = Object.keys(browserFields).length > 0;
|
||||
const additionalControls = {
|
||||
right: rightControl({ controls, updatedAt }),
|
||||
right: rightControl({ controls, updatedAt, getInspectQuery, showInspectButton }),
|
||||
left: {
|
||||
append: (
|
||||
<>
|
||||
|
@ -91,6 +103,8 @@ export const getToolbarVisibility = ({
|
|||
browserFields,
|
||||
setIsBulkActionsLoading,
|
||||
controls,
|
||||
getInspectQuery,
|
||||
showInspectButton,
|
||||
}: {
|
||||
bulkActions: BulkActionsConfig[];
|
||||
alertsCount: number;
|
||||
|
@ -104,6 +118,8 @@ export const getToolbarVisibility = ({
|
|||
browserFields: any;
|
||||
setIsBulkActionsLoading: (isLoading: boolean) => void;
|
||||
controls?: EuiDataGridToolBarAdditionalControlsOptions;
|
||||
getInspectQuery: GetInspectQuery;
|
||||
showInspectButton: boolean;
|
||||
}): EuiDataGridToolBarVisibilityOptions => {
|
||||
const selectedRowsCount = rowSelection.size;
|
||||
const defaultVisibility = getDefaultVisibility({
|
||||
|
@ -114,6 +130,8 @@ export const getToolbarVisibility = ({
|
|||
onResetColumns,
|
||||
browserFields,
|
||||
controls,
|
||||
getInspectQuery,
|
||||
showInspectButton,
|
||||
});
|
||||
const isBulkActionsActive =
|
||||
selectedRowsCount === 0 || selectedRowsCount === undefined || bulkActions.length === 0;
|
||||
|
@ -124,7 +142,7 @@ export const getToolbarVisibility = ({
|
|||
showColumnSelector: false,
|
||||
showSortSelector: false,
|
||||
additionalControls: {
|
||||
right: rightControl({ controls, updatedAt }),
|
||||
right: rightControl({ controls, updatedAt, getInspectQuery, showInspectButton }),
|
||||
left: {
|
||||
append: (
|
||||
<>
|
||||
|
|
|
@ -450,13 +450,19 @@ export enum AlertsField {
|
|||
uuid = 'kibana.alert.rule.uuid',
|
||||
}
|
||||
|
||||
export interface InspectQuery {
|
||||
request: string[];
|
||||
response: string[];
|
||||
}
|
||||
export type GetInspectQuery = () => InspectQuery;
|
||||
|
||||
export interface FetchAlertData {
|
||||
activePage: number;
|
||||
alerts: EcsFieldsResponse[];
|
||||
alertsCount: number;
|
||||
isInitializing: boolean;
|
||||
isLoading: boolean;
|
||||
getInspectQuery: () => { request: {}; response: {} };
|
||||
getInspectQuery: GetInspectQuery;
|
||||
onPageChange: (pagination: RuleRegistrySearchRequestPagination) => void;
|
||||
onSortChange: (sort: EuiDataGridSorting['columns']) => void;
|
||||
refresh: () => void;
|
||||
|
@ -487,6 +493,7 @@ export interface AlertsTableProps {
|
|||
onColumnsChange: (columns: EuiDataGridColumn[], visibleColumns: string[]) => void;
|
||||
onChangeVisibleColumns: (newColumns: string[]) => void;
|
||||
controls?: EuiDataGridToolBarAdditionalControlsOptions;
|
||||
showInspectButton?: boolean;
|
||||
}
|
||||
|
||||
// TODO We need to create generic type between our plugin, right now we have different one because of the old alerts table
|
||||
|
@ -550,6 +557,7 @@ export interface AlertsTableConfigurationRegistry {
|
|||
usePersistentControls?: () => {
|
||||
right?: ReactNode;
|
||||
};
|
||||
showInspectButton?: boolean;
|
||||
}
|
||||
|
||||
export enum BulkActionsVerbs {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue