[TIP] Add update status component (#142560)

This commit is contained in:
Luke Gmys 2022-10-04 16:41:56 +02:00 committed by GitHub
parent d95e690e9e
commit 001d44cb02
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 143 additions and 8 deletions

View file

@ -185,9 +185,7 @@ describe('Indicators', () => {
it('should render the inspector flyout', () => {
cy.get(INSPECTOR_BUTTON).last().click({ force: true });
cy.get(INSPECTOR_PANEL).should('be.visible');
cy.get(INSPECTOR_PANEL).contains('Index patterns');
cy.get(INSPECTOR_PANEL).contains('Indicators search requests');
});
});
});

View file

@ -31,7 +31,7 @@ export const FILTERS_GLOBAL_CONTAINER = '[data-test-subj="filters-global-contain
export const TIME_RANGE_PICKER = `[data-test-subj="superDatePickerToggleQuickMenuButton"]`;
export const QUERY_INPUT = `[data-test-subj="iocListPageQueryInput"]`;
export const QUERY_INPUT = `[data-test-subj="queryInput"]`;
export const EMPTY_STATE = '[data-test-subj="indicatorsTableEmptyState"]';

View file

@ -6,17 +6,23 @@
*/
import { EuiPageHeader, EuiPageHeaderSection, EuiSpacer, EuiText } from '@elastic/eui';
import React, { FC } from 'react';
import React, { FC, ReactNode } from 'react';
import { SecuritySolutionPageWrapper } from '../../containers/security_solution_page_wrapper';
export interface LayoutProps {
pageTitle?: string;
border?: boolean;
subHeader?: ReactNode;
}
export const TITLE_TEST_ID = 'tiDefaultPageLayoutTitle';
export const DefaultPageLayout: FC<LayoutProps> = ({ children, pageTitle, border = true }) => {
export const DefaultPageLayout: FC<LayoutProps> = ({
children,
pageTitle,
border = true,
subHeader,
}) => {
return (
<SecuritySolutionPageWrapper>
<EuiPageHeader alignItems="center" bottomBorder={border}>
@ -26,6 +32,12 @@ export const DefaultPageLayout: FC<LayoutProps> = ({ children, pageTitle, border
<h2 data-test-subj={TITLE_TEST_ID}>{pageTitle}</h2>
</EuiText>
)}
{subHeader ? (
<>
<EuiSpacer size="m" />
{subHeader}
</>
) : null}
</EuiPageHeaderSection>
</EuiPageHeader>
<EuiSpacer size="l" />

View file

@ -0,0 +1,8 @@
/*
* 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.
*/
export * from './update_status';

View file

@ -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 { render } from '@testing-library/react';
import React from 'react';
import { TestProvidersComponent } from '../../common/mocks/test_providers';
import { UpdateStatus } from './update_status';
describe('<UpdateStatus />', () => {
it('should render Updated now', () => {
const result = render(<UpdateStatus updatedAt={Date.now()} isUpdating={false} />, {
wrapper: TestProvidersComponent,
});
expect(result.asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiText emotion-euiText-xs-euiTextColor-subdued"
data-test-subj="updateStatus"
>
Updated now
</div>
</div>
</div>
</DocumentFragment>
`);
});
it('should render Updating when isUpdating', () => {
const result = render(<UpdateStatus updatedAt={Date.now()} isUpdating={true} />, {
wrapper: TestProvidersComponent,
});
expect(result.asFragment()).toMatchInlineSnapshot(`
<DocumentFragment>
<div
class="euiFlexGroup euiFlexGroup--gutterLarge euiFlexGroup--directionRow euiFlexGroup--responsive"
>
<div
class="euiFlexItem euiFlexItem--flexGrowZero"
>
<div
class="euiText emotion-euiText-xs-euiTextColor-subdued"
data-test-subj="updateStatus"
>
Updating...
</div>
</div>
</div>
</DocumentFragment>
`);
});
});

View file

@ -0,0 +1,43 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedRelative } from '@kbn/i18n-react';
interface UpdateStatusProps {
updatedAt: number;
isUpdating: boolean;
}
const UPDATING = i18n.translate('xpack.threatIntelligence.updateStatus.updating', {
defaultMessage: 'Updating...',
});
const UPDATED = i18n.translate('xpack.threatIntelligence.updateStatus.updated', {
defaultMessage: 'Updated',
});
export const UpdateStatus: React.FC<UpdateStatusProps> = ({ isUpdating, updatedAt }) => (
<EuiFlexGroup>
<EuiFlexItem grow={false}>
<EuiText size="xs" color="subdued" data-test-subj="updateStatus">
{isUpdating ? (
UPDATING
) : (
<>
{UPDATED}
&nbsp;
<FormattedRelative value={new Date(updatedAt)} />
</>
)}
</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);

View file

@ -105,6 +105,7 @@ describe('useIndicators()', () => {
expect(hookResult.result.current).toMatchInlineSnapshot(`
Object {
"dataUpdatedAt": 0,
"handleRefresh": [Function],
"indicatorCount": 0,
"indicators": Array [],

View file

@ -47,6 +47,8 @@ export interface UseIndicatorsValue {
* Data loading is in progress (see docs on `isFetching` here: https://tanstack.com/query/v4/docs/guides/queries)
*/
isFetching: boolean;
dataUpdatedAt: number;
}
export const useIndicators = ({
@ -95,7 +97,7 @@ export const useIndicators = ({
[inspectorAdapters, searchService]
);
const { isLoading, isFetching, data, refetch } = useQuery(
const { isLoading, isFetching, data, refetch, dataUpdatedAt } = useQuery(
[
'indicatorsTable',
{
@ -132,5 +134,6 @@ export const useIndicators = ({
isLoading,
isFetching,
handleRefresh,
dataUpdatedAt,
};
};

View file

@ -42,6 +42,7 @@ describe('<IndicatorsPage />', () => {
onChangeItemsPerPage: stub,
onChangePage: stub,
handleRefresh: stub,
dataUpdatedAt: Date.now(),
});
(useFilters as jest.MockedFunction<typeof useFilters>).mockReturnValue({

View file

@ -20,6 +20,7 @@ import { useColumnSettings } from './components/indicators_table/hooks/use_colum
import { useAggregatedIndicators } from './hooks/use_aggregated_indicators';
import { IndicatorsFilters } from './containers/indicators_filters';
import { useSecurityContext } from '../../hooks/use_security_context';
import { UpdateStatus } from '../../components/update_status';
const queryClient = new QueryClient();
@ -48,6 +49,7 @@ const IndicatorsPageContent: VFC = () => {
pagination,
isLoading: isLoadingIndicators,
isFetching: isFetchingIndicators,
dataUpdatedAt,
} = useIndicators({
filters,
filterQuery,
@ -72,10 +74,14 @@ const IndicatorsPageContent: VFC = () => {
return (
<FieldTypesProvider>
<DefaultPageLayout pageTitle="Indicators">
<DefaultPageLayout
pageTitle="Indicators"
subHeader={<UpdateStatus isUpdating={isFetchingIndicators} updatedAt={dataUpdatedAt} />}
>
<FiltersGlobal>
<SiemSearchBar indexPattern={indexPattern} id="global" />
</FiltersGlobal>
<IndicatorsBarChartWrapper
dateRange={dateRange}
series={series}