[SecuritySolution] Data Quality dashboard in serverless (#163733)

This commit is contained in:
Angela Chuang 2023-08-25 12:43:27 +01:00 committed by GitHub
parent e8127e275f
commit 8da14b7c4c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
67 changed files with 1107 additions and 132 deletions

View file

@ -24,6 +24,7 @@
"xpack.discoverLogExplorer": "plugins/discover_log_explorer",
"xpack.crossClusterReplication": "plugins/cross_cluster_replication",
"xpack.elasticAssistant": "packages/kbn-elastic-assistant",
"xpack.ecsDataQualityDashboard": "plugins/ecs_data_quality_dashboard",
"xpack.embeddableEnhanced": "plugins/embeddable_enhanced",
"xpack.endpoint": "plugins/endpoint",
"xpack.enterpriseSearch": "plugins/enterprise_search",

View file

@ -22,10 +22,13 @@ import { IlmPhasesEmptyPrompt } from '../../../ilm_phases_empty_prompt';
import { IndicesDetails } from './indices_details';
import { StorageDetails } from './storage_details';
import { PatternRollup, SelectedIndex } from '../../../types';
import { useDataQualityContext } from '../../data_quality_context';
export interface Props {
addSuccessToast: (toast: { title: string }) => void;
baseTheme: Theme;
canUserCreateAndReadCases: () => boolean;
endDate?: string | null;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
getGroupByFieldsOnClick: (
@ -53,8 +56,8 @@ export interface Props {
patternIndexNames: Record<string, string[]>;
patternRollups: Record<string, PatternRollup>;
patterns: string[];
startDate?: string | null;
theme?: PartialTheme;
baseTheme: Theme;
updatePatternIndexNames: ({
indexNames,
pattern,
@ -68,6 +71,7 @@ export interface Props {
const DataQualityDetailsComponent: React.FC<Props> = ({
addSuccessToast,
canUserCreateAndReadCases,
endDate,
formatBytes,
formatNumber,
getGroupByFieldsOnClick,
@ -77,18 +81,20 @@ const DataQualityDetailsComponent: React.FC<Props> = ({
patternIndexNames,
patternRollups,
patterns,
startDate,
theme,
baseTheme,
updatePatternIndexNames,
updatePatternRollup,
}) => {
const { isILMAvailable } = useDataQualityContext();
const [selectedIndex, setSelectedIndex] = useState<SelectedIndex | null>(null);
const onIndexSelected = useCallback(async ({ indexName, pattern }: SelectedIndex) => {
setSelectedIndex({ indexName, pattern });
}, []);
if (ilmPhases.length === 0) {
if (isILMAvailable && ilmPhases.length === 0) {
return <IlmPhasesEmptyPrompt />;
}
@ -107,6 +113,7 @@ const DataQualityDetailsComponent: React.FC<Props> = ({
<IndicesDetails
addSuccessToast={addSuccessToast}
canUserCreateAndReadCases={canUserCreateAndReadCases}
endDate={endDate}
formatBytes={formatBytes}
formatNumber={formatNumber}
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
@ -120,6 +127,7 @@ const DataQualityDetailsComponent: React.FC<Props> = ({
patternRollups={patternRollups}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
startDate={startDate}
updatePatternIndexNames={updatePatternIndexNames}
updatePatternRollup={updatePatternRollup}
/>

View file

@ -24,6 +24,7 @@ import { PatternRollup, SelectedIndex } from '../../../../types';
export interface Props {
addSuccessToast: (toast: { title: string }) => void;
canUserCreateAndReadCases: () => boolean;
endDate?: string | null;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
getGroupByFieldsOnClick: (
@ -53,6 +54,7 @@ export interface Props {
patterns: string[];
selectedIndex: SelectedIndex | null;
setSelectedIndex: (selectedIndex: SelectedIndex | null) => void;
startDate?: string | null;
theme?: PartialTheme;
baseTheme: Theme;
updatePatternIndexNames: ({
@ -68,6 +70,7 @@ export interface Props {
const IndicesDetailsComponent: React.FC<Props> = ({
addSuccessToast,
canUserCreateAndReadCases,
endDate,
formatBytes,
formatNumber,
getGroupByFieldsOnClick,
@ -79,6 +82,7 @@ const IndicesDetailsComponent: React.FC<Props> = ({
patterns,
selectedIndex,
setSelectedIndex,
startDate,
theme,
baseTheme,
updatePatternIndexNames,
@ -90,6 +94,7 @@ const IndicesDetailsComponent: React.FC<Props> = ({
<Pattern
addSuccessToast={addSuccessToast}
canUserCreateAndReadCases={canUserCreateAndReadCases}
endDate={endDate}
formatBytes={formatBytes}
formatNumber={formatNumber}
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
@ -101,6 +106,7 @@ const IndicesDetailsComponent: React.FC<Props> = ({
patternRollup={patternRollups[pattern]}
selectedIndex={selectedIndex}
setSelectedIndex={setSelectedIndex}
startDate={startDate}
theme={theme}
baseTheme={baseTheme}
updatePatternIndexNames={updatePatternIndexNames}

View file

@ -102,6 +102,7 @@ describe('helpers', () => {
const pattern = 'auditbeat-*';
const flattenedBuckets = getFlattenedBuckets({
ilmPhases,
isILMAvailable: true,
patternRollups,
});
@ -129,12 +130,45 @@ describe('helpers', () => {
},
]);
});
test('it returns the expected legend items when isILMAvailable is false', () => {
const pattern = 'auditbeat-*';
const flattenedBuckets = getFlattenedBuckets({
ilmPhases,
isILMAvailable: false,
patternRollups,
});
expect(getLegendItemsForPattern({ pattern, flattenedBuckets })).toEqual([
{
color: euiThemeVars.euiColorSuccess,
ilmPhase: null,
index: '.ds-auditbeat-8.6.1-2023.02.07-000001',
pattern: 'auditbeat-*',
sizeInBytes: 18791790,
},
{
color: euiThemeVars.euiColorDanger,
ilmPhase: null,
index: 'auditbeat-custom-index-1',
pattern: 'auditbeat-*',
sizeInBytes: 28409,
},
{
color: euiThemeVars.euiColorDanger,
ilmPhase: null,
index: 'auditbeat-custom-empty-index-1',
pattern: 'auditbeat-*',
sizeInBytes: 247,
},
]);
});
});
describe('getLegendItems', () => {
test('it returns the expected legend items', () => {
const flattenedBuckets = getFlattenedBuckets({
ilmPhases,
isILMAvailable: true,
patternRollups,
});
@ -205,6 +239,7 @@ describe('helpers', () => {
expect(
getFlattenedBuckets({
ilmPhases,
isILMAvailable: true,
patternRollups,
})
).toEqual([
@ -250,6 +285,57 @@ describe('helpers', () => {
},
]);
});
test('it returns the expected flattened buckets when isILMAvailable is false', () => {
expect(
getFlattenedBuckets({
ilmPhases,
isILMAvailable: false,
patternRollups,
})
).toEqual([
{
ilmPhase: undefined,
incompatible: 0,
indexName: '.internal.alerts-security.alerts-default-000001',
pattern: '.alerts-security.alerts-default',
sizeInBytes: 0,
},
{
ilmPhase: undefined,
incompatible: 0,
indexName: '.ds-auditbeat-8.6.1-2023.02.07-000001',
pattern: 'auditbeat-*',
sizeInBytes: 18791790,
},
{
ilmPhase: undefined,
incompatible: 1,
indexName: 'auditbeat-custom-empty-index-1',
pattern: 'auditbeat-*',
sizeInBytes: 247,
},
{
ilmPhase: undefined,
incompatible: 3,
indexName: 'auditbeat-custom-index-1',
pattern: 'auditbeat-*',
sizeInBytes: 28409,
},
{
ilmPhase: undefined,
indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001',
pattern: 'packetbeat-*',
sizeInBytes: 512194751,
},
{
ilmPhase: undefined,
indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001',
pattern: 'packetbeat-*',
sizeInBytes: 584326147,
},
]);
});
});
describe('getFillColor', () => {
@ -276,6 +362,7 @@ describe('helpers', () => {
test('it returns the expected map', () => {
const flattenedBuckets = getFlattenedBuckets({
ilmPhases,
isILMAvailable: true,
patternRollups,
});
@ -363,6 +450,7 @@ describe('helpers', () => {
const layer0FillColor = 'transparent';
const flattenedBuckets = getFlattenedBuckets({
ilmPhases,
isILMAvailable: true,
patternRollups,
});
const pathToFlattenedBucketMap = getPathToFlattenedBucketMap(flattenedBuckets);

View file

@ -98,9 +98,11 @@ export const getLegendItems = ({
export const getFlattenedBuckets = ({
ilmPhases,
isILMAvailable,
patternRollups,
}: {
ilmPhases: string[];
isILMAvailable: boolean;
patternRollups: Record<string, PatternRollup>;
}): FlattenedBucket[] =>
Object.values(patternRollups).reduce<FlattenedBucket[]>((acc, patternRollup) => {
@ -111,13 +113,15 @@ export const getFlattenedBuckets = ({
);
const { ilmExplain, pattern, results, stats } = patternRollup;
if (ilmExplain != null && stats != null) {
if (((isILMAvailable && ilmExplain != null) || !isILMAvailable) && stats != null) {
return [
...acc,
...Object.entries(stats).reduce<FlattenedBucket[]>(
(validStats, [indexName, indexStats]) => {
const ilmPhase = getIlmPhase(ilmExplain[indexName]);
const isSelectedPhase = ilmPhase != null && ilmPhasesMap[ilmPhase] != null;
const ilmPhase = getIlmPhase(ilmExplain?.[indexName], isILMAvailable);
const isSelectedPhase =
(isILMAvailable && ilmPhase != null && ilmPhasesMap[ilmPhase] != null) ||
!isILMAvailable;
if (isSelectedPhase) {
const incompatible =

View file

@ -12,6 +12,7 @@ import { getFlattenedBuckets } from './helpers';
import { StorageTreemap } from '../../../storage_treemap';
import { DEFAULT_MAX_CHART_HEIGHT, StorageTreemapContainer } from '../../../tabs/styles';
import { PatternRollup, SelectedIndex } from '../../../../types';
import { useDataQualityContext } from '../../../data_quality_context';
export interface Props {
formatBytes: (value: number | undefined) => string;
@ -32,13 +33,16 @@ const StorageDetailsComponent: React.FC<Props> = ({
theme,
baseTheme,
}) => {
const { isILMAvailable } = useDataQualityContext();
const flattenedBuckets = useMemo(
() =>
getFlattenedBuckets({
ilmPhases,
isILMAvailable,
patternRollups,
}),
[ilmPhases, patternRollups]
[ilmPhases, isILMAvailable, patternRollups]
);
return (

View file

@ -24,7 +24,9 @@ import { useResultsRollup } from '../../use_results_rollup';
interface Props {
addSuccessToast: (toast: { title: string }) => void;
baseTheme: Theme;
canUserCreateAndReadCases: () => boolean;
endDate?: string | null;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
getGroupByFieldsOnClick: (
@ -52,13 +54,14 @@ interface Props {
}) => void;
patterns: string[];
setLastChecked: (lastChecked: string) => void;
startDate?: string | null;
theme?: PartialTheme;
baseTheme: Theme;
}
const BodyComponent: React.FC<Props> = ({
addSuccessToast,
canUserCreateAndReadCases,
endDate,
formatBytes,
formatNumber,
getGroupByFieldsOnClick,
@ -68,6 +71,7 @@ const BodyComponent: React.FC<Props> = ({
openCreateCaseFlyout,
patterns,
setLastChecked,
startDate,
theme,
baseTheme,
}) => {
@ -115,7 +119,9 @@ const BodyComponent: React.FC<Props> = ({
<EuiFlexItem>
<DataQualityDetails
addSuccessToast={addSuccessToast}
baseTheme={baseTheme}
canUserCreateAndReadCases={canUserCreateAndReadCases}
endDate={endDate}
formatBytes={formatBytes}
formatNumber={formatNumber}
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
@ -125,8 +131,8 @@ const BodyComponent: React.FC<Props> = ({
patterns={patterns}
patternIndexNames={patternIndexNames}
patternRollups={patternRollups}
startDate={startDate}
theme={theme}
baseTheme={baseTheme}
updatePatternIndexNames={updatePatternIndexNames}
updatePatternRollup={updatePatternRollup}
/>

View file

@ -18,7 +18,11 @@ const mockTelemetryEvents = {
reportDataQualityCheckAllCompleted: mockReportDataQualityCheckAllClicked,
};
const ContextWrapper: React.FC = ({ children }) => (
<DataQualityProvider httpFetch={mockHttpFetch} telemetryEvents={mockTelemetryEvents}>
<DataQualityProvider
httpFetch={mockHttpFetch}
telemetryEvents={mockTelemetryEvents}
isILMAvailable={true}
>
{children}
</DataQualityProvider>
);
@ -52,4 +56,11 @@ describe('DataQualityContext', () => {
expect(telemetryEvents).toEqual(mockTelemetryEvents);
});
test('it should return the isILMAvailable param', async () => {
const { result } = renderHook(useDataQualityContext, { wrapper: ContextWrapper });
const isILMAvailable = await result.current.isILMAvailable;
expect(isILMAvailable).toEqual(true);
});
});

View file

@ -11,6 +11,7 @@ import { TelemetryEvents } from '../../types';
interface DataQualityProviderProps {
httpFetch: HttpHandler;
isILMAvailable: boolean;
telemetryEvents: TelemetryEvents;
}
@ -19,14 +20,16 @@ const DataQualityContext = React.createContext<DataQualityProviderProps | undefi
export const DataQualityProvider: React.FC<DataQualityProviderProps> = ({
children,
httpFetch,
isILMAvailable,
telemetryEvents,
}) => {
const value = useMemo(
() => ({
httpFetch,
isILMAvailable,
telemetryEvents,
}),
[httpFetch, telemetryEvents]
[httpFetch, isILMAvailable, telemetryEvents]
);
return <DataQualityContext.Provider value={value}>{children}</DataQualityContext.Provider>;

View file

@ -139,6 +139,48 @@ describe('CheckAll', () => {
expect(screen.getByTestId('checkAll')).toHaveTextContent(CHECK_ALL);
});
test('it renders a disabled button when ILM available and ilmPhases is an empty array', () => {
render(
<TestProviders>
<CheckAll
formatBytes={mockFormatBytes}
formatNumber={mockFormatNumber}
ilmPhases={[]}
incrementCheckAllIndiciesChecked={jest.fn()}
onCheckCompleted={jest.fn()}
patternIndexNames={patternIndexNames}
patterns={[]}
setCheckAllIndiciesChecked={jest.fn()}
setCheckAllTotalIndiciesToCheck={jest.fn()}
setIndexToCheck={jest.fn()}
/>
</TestProviders>
);
expect(screen.getByTestId('checkAll').hasAttribute('disabled')).toBeTruthy();
});
test('it renders the expected button when ILM is NOT available', () => {
render(
<TestProviders isILMAvailable={false}>
<CheckAll
formatBytes={mockFormatBytes}
formatNumber={mockFormatNumber}
ilmPhases={[]}
incrementCheckAllIndiciesChecked={jest.fn()}
onCheckCompleted={jest.fn()}
patternIndexNames={patternIndexNames}
patterns={[]}
setCheckAllIndiciesChecked={jest.fn()}
setCheckAllTotalIndiciesToCheck={jest.fn()}
setIndexToCheck={jest.fn()}
/>
</TestProviders>
);
expect(screen.getByTestId('checkAll').hasAttribute('disabled')).toBeFalsy();
});
test('it renders the expected button text when a check is running', () => {
render(
<TestProviders>

View file

@ -60,7 +60,7 @@ const CheckAllComponent: React.FC<Props> = ({
setCheckAllTotalIndiciesToCheck,
setIndexToCheck,
}) => {
const { httpFetch } = useDataQualityContext();
const { httpFetch, isILMAvailable } = useDataQualityContext();
const abortController = useRef(new AbortController());
const [isRunning, setIsRunning] = useState<boolean>(false);
@ -157,7 +157,7 @@ const CheckAllComponent: React.FC<Props> = ({
};
}, [abortController]);
const disabled = ilmPhases.length === 0;
const disabled = isILMAvailable && ilmPhases.length === 0;
return (
<CheckAllButton

View file

@ -32,6 +32,7 @@ import type {
PatternRollup,
} from '../../../types';
import { getSizeInBytes } from '../../../helpers';
import { useDataQualityContext } from '../../data_quality_context';
const SummaryActionsFlexGroup = styled(EuiFlexGroup)`
gap: ${({ theme }) => theme.eui.euiSizeS};
@ -45,11 +46,13 @@ export const getResultsSortedByDocsCount = (
export const getAllMarkdownCommentsFromResults = ({
formatBytes,
formatNumber,
isILMAvailable,
patternIndexNames,
patternRollup,
}: {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
isILMAvailable: boolean;
patternIndexNames: Record<string, string[]>;
patternRollup: PatternRollup;
}): string[] => {
@ -59,6 +62,7 @@ export const getAllMarkdownCommentsFromResults = ({
const summaryTableItems = getSummaryTableItems({
ilmExplain: patternRollup.ilmExplain,
indexNames: patternIndexNames[patternRollup.pattern] ?? [],
isILMAvailable,
pattern: patternRollup.pattern,
patternDocsCount: patternRollup.docsCount ?? 0,
results: patternRollup.results,
@ -78,6 +82,7 @@ export const getAllMarkdownCommentsFromResults = ({
ilmPhase: item.ilmPhase,
indexName: item.indexName,
incompatible: result?.incompatible,
isILMAvailable,
patternDocsCount: patternRollup.docsCount ?? 0,
sizeInBytes: getSizeInBytes({ indexName: item.indexName, stats: patternRollup.stats }),
}).trim();
@ -85,7 +90,7 @@ export const getAllMarkdownCommentsFromResults = ({
const initialComments: string[] =
summaryTableMarkdownRows.length > 0
? [getSummaryTableMarkdownHeader(), ...summaryTableMarkdownRows]
? [getSummaryTableMarkdownHeader(isILMAvailable), ...summaryTableMarkdownRows]
: [];
return sortedResults.reduce<string[]>(
@ -97,11 +102,13 @@ export const getAllMarkdownCommentsFromResults = ({
export const getAllMarkdownComments = ({
formatBytes,
formatNumber,
isILMAvailable,
patternIndexNames,
patternRollups,
}: {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
isILMAvailable: boolean;
patternIndexNames: Record<string, string[]>;
patternRollups: Record<string, PatternRollup>;
}): string[] => {
@ -123,6 +130,7 @@ export const getAllMarkdownComments = ({
...getAllMarkdownCommentsFromResults({
formatBytes,
formatNumber,
isILMAvailable,
patternRollup: patternRollups[pattern],
patternIndexNames,
}),
@ -178,6 +186,7 @@ const SummaryActionsComponent: React.FC<Props> = ({
totalIndicesChecked,
sizeInBytes,
}) => {
const { isILMAvailable } = useDataQualityContext();
const [indexToCheck, setIndexToCheck] = useState<IndexToCheck | null>(null);
const [checkAllIndiciesChecked, setCheckAllIndiciesChecked] = useState<number>(0);
const [checkAllTotalIndiciesToCheck, setCheckAllTotalIndiciesToCheck] = useState<number>(0);
@ -199,6 +208,7 @@ const SummaryActionsComponent: React.FC<Props> = ({
...getAllMarkdownComments({
formatBytes,
formatNumber,
isILMAvailable,
patternIndexNames,
patternRollups,
}),
@ -213,6 +223,7 @@ const SummaryActionsComponent: React.FC<Props> = ({
errorSummary,
formatBytes,
formatNumber,
isILMAvailable,
patternIndexNames,
patternRollups,
sizeInBytes,

View file

@ -103,7 +103,7 @@ const IndexPropertiesComponent: React.FC<Props> = ({
updatePatternRollup,
}) => {
const { error: mappingsError, indexes, loading: loadingMappings } = useMappings(indexName);
const { telemetryEvents } = useDataQualityContext();
const { telemetryEvents, isILMAvailable } = useDataQualityContext();
const requestItems = useMemo(
() =>
@ -236,6 +236,7 @@ const IndexPropertiesComponent: React.FC<Props> = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount: patternRollup.docsCount ?? 0,
sizeInBytes: patternRollup.sizeInBytes,
@ -290,6 +291,7 @@ const IndexPropertiesComponent: React.FC<Props> = ({
ilmPhase,
indexId,
indexName,
isILMAvailable,
loadingMappings,
loadingUnallowedValues,
mappingsError,

View file

@ -423,10 +423,18 @@ describe('helpers', () => {
describe('getSummaryTableMarkdownHeader', () => {
test('it returns the expected header', () => {
expect(getSummaryTableMarkdownHeader()).toEqual(
const isILMAvailable = true;
expect(getSummaryTableMarkdownHeader(isILMAvailable)).toEqual(
'| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|'
);
});
test('it returns the expected header when isILMAvailable is false', () => {
const isILMAvailable = false;
expect(getSummaryTableMarkdownHeader(isILMAvailable)).toEqual(
'| Result | Index | Docs | Incompatible fields | Size |\n|--------|-------|------|---------------------|------|'
);
});
});
describe('getSummaryTableMarkdownRow', () => {
@ -438,6 +446,7 @@ describe('helpers', () => {
formatNumber,
incompatible: 3,
ilmPhase: 'unmanaged',
isILMAvailable: true,
indexName: 'auditbeat-custom-index-1',
patternDocsCount: 57410,
sizeInBytes: 28413,
@ -454,11 +463,28 @@ describe('helpers', () => {
incompatible: undefined, // <--
ilmPhase: undefined, // <--
indexName: 'auditbeat-custom-index-1',
isILMAvailable: true,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- | -- | 27.7KB |\n');
});
test('it returns the expected row when isILMAvailable is false', () => {
expect(
getSummaryTableMarkdownRow({
docsCount: 4,
formatBytes,
formatNumber,
incompatible: undefined, // <--
ilmPhase: undefined, // <--
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual('| -- | auditbeat-custom-index-1 | 4 (0.0%) | -- | 27.7KB |\n');
});
});
describe('getSummaryTableMarkdownComment', () => {
@ -470,6 +496,7 @@ describe('helpers', () => {
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: true,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: 28413,
@ -478,6 +505,24 @@ describe('helpers', () => {
'| Result | Index | Docs | Incompatible fields | ILM Phase | Size |\n|--------|-------|------|---------------------|-----------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | `unmanaged` | 27.7KB |\n\n'
);
});
test('it returns the expected comment when isILMAvailable is false', () => {
expect(
getSummaryTableMarkdownComment({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual(
'| Result | Index | Docs | Incompatible fields | Size |\n|--------|-------|------|---------------------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | 27.7KB |\n\n'
);
});
});
describe('getStatsRollupMarkdownComment', () => {

View file

@ -216,13 +216,18 @@ export const getResultEmoji = (incompatible: number | undefined): string => {
}
};
export const getSummaryTableMarkdownHeader = (): string =>
`| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} | ${ILM_PHASE} | ${SIZE} |
export const getSummaryTableMarkdownHeader = (isILMAvailable: boolean): string =>
isILMAvailable
? `| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} | ${ILM_PHASE} | ${SIZE} |
|${getHeaderSeparator(RESULT)}|${getHeaderSeparator(INDEX)}|${getHeaderSeparator(
DOCS
)}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(
ILM_PHASE
)}|${getHeaderSeparator(SIZE)}|`;
DOCS
)}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(
ILM_PHASE
)}|${getHeaderSeparator(SIZE)}|`
: `| ${RESULT} | ${INDEX} | ${DOCS} | ${INCOMPATIBLE_FIELDS} | ${SIZE} |
|${getHeaderSeparator(RESULT)}|${getHeaderSeparator(INDEX)}|${getHeaderSeparator(
DOCS
)}|${getHeaderSeparator(INCOMPATIBLE_FIELDS)}|${getHeaderSeparator(SIZE)}|`;
export const getSummaryTableMarkdownRow = ({
docsCount,
@ -231,6 +236,7 @@ export const getSummaryTableMarkdownRow = ({
ilmPhase,
incompatible,
indexName,
isILMAvailable,
patternDocsCount,
sizeInBytes,
}: {
@ -240,17 +246,26 @@ export const getSummaryTableMarkdownRow = ({
ilmPhase: IlmPhase | undefined;
incompatible: number | undefined;
indexName: string;
isILMAvailable: boolean;
patternDocsCount: number;
sizeInBytes: number | undefined;
}): string =>
`| ${getResultEmoji(incompatible)} | ${escape(indexName)} | ${formatNumber(
docsCount
)} (${getDocsCountPercent({
docsCount,
patternDocsCount,
})}) | ${incompatible ?? EMPTY_PLACEHOLDER} | ${
ilmPhase != null ? getCodeFormattedValue(ilmPhase) : EMPTY_PLACEHOLDER
} | ${formatBytes(sizeInBytes)} |
isILMAvailable
? `| ${getResultEmoji(incompatible)} | ${escape(indexName)} | ${formatNumber(
docsCount
)} (${getDocsCountPercent({
docsCount,
patternDocsCount,
})}) | ${incompatible ?? EMPTY_PLACEHOLDER} | ${
ilmPhase != null ? getCodeFormattedValue(ilmPhase) : EMPTY_PLACEHOLDER
} | ${formatBytes(sizeInBytes)} |
`
: `| ${getResultEmoji(incompatible)} | ${escape(indexName)} | ${formatNumber(
docsCount
)} (${getDocsCountPercent({
docsCount,
patternDocsCount,
})}) | ${incompatible ?? EMPTY_PLACEHOLDER} | ${formatBytes(sizeInBytes)} |
`;
export const getSummaryTableMarkdownComment = ({
@ -259,6 +274,7 @@ export const getSummaryTableMarkdownComment = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,
@ -268,17 +284,19 @@ export const getSummaryTableMarkdownComment = ({
formatNumber: (value: number | undefined) => string;
ilmPhase: IlmPhase | undefined;
indexName: string;
isILMAvailable: boolean;
partitionedFieldMetadata: PartitionedFieldMetadata;
patternDocsCount: number;
sizeInBytes: number | undefined;
}): string =>
`${getSummaryTableMarkdownHeader()}
`${getSummaryTableMarkdownHeader(isILMAvailable)}
${getSummaryTableMarkdownRow({
docsCount,
formatBytes,
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
incompatible: partitionedFieldMetadata.incompatible.length,
patternDocsCount,
sizeInBytes,

View file

@ -30,6 +30,7 @@ import { auditbeatWithAllResults } from '../../mock/pattern_rollup/mock_auditbea
import { mockStats } from '../../mock/stats/mock_stats';
import { IndexSummaryTableItem } from '../summary_table/helpers';
import { DataQualityCheckResult } from '../../types';
import { getIndexNames, getTotalDocsCount } from '../../helpers';
const hot: IlmExplainLifecycleLifecycleExplainManaged = {
index: '.ds-packetbeat-8.6.1-2023.02.04-000001',
@ -169,25 +170,26 @@ describe('helpers', () => {
});
describe('getIlmPhase', () => {
const isILMAvailable = true;
test('it returns undefined when the `ilmExplainRecord` is undefined', () => {
expect(getIlmPhase(undefined)).toBeUndefined();
expect(getIlmPhase(undefined, isILMAvailable)).toBeUndefined();
});
describe('when the `ilmExplainRecord` is a `IlmExplainLifecycleLifecycleExplainManaged` record', () => {
Object.keys(managed).forEach((phase) =>
test(`it returns the expected phase when 'phase' is '${phase}'`, () => {
expect(getIlmPhase(managed[phase])).toEqual(phase);
expect(getIlmPhase(managed[phase], isILMAvailable)).toEqual(phase);
})
);
test(`it returns undefined when the 'phase' is unknown`, () => {
expect(getIlmPhase(other)).toBeUndefined();
expect(getIlmPhase(other, isILMAvailable)).toBeUndefined();
});
});
describe('when the `ilmExplainRecord` is a `IlmExplainLifecycleLifecycleExplainUnmanaged` record', () => {
test('it returns `unmanaged`', () => {
expect(getIlmPhase(unmanaged)).toEqual('unmanaged');
expect(getIlmPhase(unmanaged, isILMAvailable)).toEqual('unmanaged');
});
});
});
@ -273,12 +275,14 @@ describe('helpers', () => {
pattern: 'auditbeat-*',
},
};
const isILMAvailable = true;
test('it returns the expected summary table items', () => {
expect(
getSummaryTableItems({
ilmExplain: mockIlmExplain,
indexNames,
isILMAvailable,
pattern,
patternDocsCount,
results,
@ -317,11 +321,56 @@ describe('helpers', () => {
]);
});
test('it returns the expected summary table items when isILMAvailable is false', () => {
expect(
getSummaryTableItems({
ilmExplain: mockIlmExplain,
indexNames,
isILMAvailable: false,
pattern,
patternDocsCount,
results,
sortByColumn: defaultSort.sort.field,
sortByDirection: defaultSort.sort.direction,
stats: mockStats,
})
).toEqual([
{
docsCount: 1630289,
ilmPhase: undefined,
incompatible: undefined,
indexName: '.ds-packetbeat-8.5.3-2023.02.04-000001',
pattern: 'auditbeat-*',
patternDocsCount: 4,
sizeInBytes: 733175040,
},
{
docsCount: 1628343,
ilmPhase: undefined,
incompatible: undefined,
indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001',
pattern: 'auditbeat-*',
patternDocsCount: 4,
sizeInBytes: 731583142,
},
{
docsCount: 4,
ilmPhase: undefined,
incompatible: 3,
indexName: 'auditbeat-custom-index-1',
pattern: 'auditbeat-*',
patternDocsCount: 4,
sizeInBytes: 28413,
},
]);
});
test('it returns the expected summary table items when `sortByDirection` is ascending', () => {
expect(
getSummaryTableItems({
ilmExplain: mockIlmExplain,
indexNames,
isILMAvailable,
pattern,
patternDocsCount,
results,
@ -365,6 +414,7 @@ describe('helpers', () => {
getSummaryTableItems({
ilmExplain: null, // <-- no data
indexNames,
isILMAvailable,
pattern,
patternDocsCount,
results: undefined, // <-- no data
@ -410,12 +460,27 @@ describe('helpers', () => {
'.ds-packetbeat-8.5.3-2023.02.04-000001',
'auditbeat-custom-index-1',
];
const isILMAvailable = true;
test('returns true when `indexNames` does NOT exist, and the required `stats` and `ilmExplain` are available', () => {
expect(
shouldCreateIndexNames({
ilmExplain: mockIlmExplain,
indexNames: undefined,
isILMAvailable,
newIndexNames: [],
stats: mockStats,
})
).toBe(true);
});
test('returns true when `isILMAvailable` is false, and the required `stats` is available, and `ilmExplain` is not available', () => {
expect(
shouldCreateIndexNames({
ilmExplain: null,
indexNames: undefined,
isILMAvailable: false,
newIndexNames: [],
stats: mockStats,
})
).toBe(true);
@ -426,6 +491,8 @@ describe('helpers', () => {
shouldCreateIndexNames({
ilmExplain: mockIlmExplain,
indexNames,
isILMAvailable,
newIndexNames: indexNames,
stats: mockStats,
})
).toBe(false);
@ -436,6 +503,8 @@ describe('helpers', () => {
shouldCreateIndexNames({
ilmExplain: mockIlmExplain,
indexNames: undefined,
isILMAvailable,
newIndexNames: [],
stats: null,
})
).toBe(false);
@ -446,6 +515,8 @@ describe('helpers', () => {
shouldCreateIndexNames({
ilmExplain: null,
indexNames: undefined,
isILMAvailable,
newIndexNames: [],
stats: mockStats,
})
).toBe(false);
@ -456,6 +527,8 @@ describe('helpers', () => {
shouldCreateIndexNames({
ilmExplain: null,
indexNames: undefined,
isILMAvailable,
newIndexNames: [],
stats: null,
})
).toBe(false);
@ -466,6 +539,8 @@ describe('helpers', () => {
shouldCreateIndexNames({
ilmExplain: null,
indexNames,
isILMAvailable,
newIndexNames: [],
stats: null,
})
).toBe(false);
@ -473,22 +548,47 @@ describe('helpers', () => {
});
describe('shouldCreatePatternRollup', () => {
test('it returns false when the `patternRollup` already exists', () => {
const isILMAvailable = true;
const newIndexNames = getIndexNames({
stats: mockStats,
ilmExplain: mockIlmExplain,
ilmPhases: ['hot', 'unmanaged'],
isILMAvailable,
});
const newDocsCount = getTotalDocsCount({ indexNames: newIndexNames, stats: mockStats });
test('it returns false when the `patternRollup.docsCount` equals newDocsCount', () => {
expect(
shouldCreatePatternRollup({
error: null,
ilmExplain: mockIlmExplain,
isILMAvailable,
newDocsCount: auditbeatWithAllResults.docsCount as number,
patternRollup: auditbeatWithAllResults,
stats: mockStats,
})
).toBe(false);
});
test('it returns true when all data was loaded', () => {
test('it returns true when all data and ILMExplain were loaded', () => {
expect(
shouldCreatePatternRollup({
error: null,
ilmExplain: mockIlmExplain,
isILMAvailable,
newDocsCount,
patternRollup: undefined,
stats: mockStats,
})
).toBe(true);
});
test('it returns true when all data was loaded and ILM is not available', () => {
expect(
shouldCreatePatternRollup({
error: null,
ilmExplain: null,
isILMAvailable: false,
newDocsCount,
patternRollup: undefined,
stats: mockStats,
})
@ -500,6 +600,8 @@ describe('helpers', () => {
shouldCreatePatternRollup({
error: null,
ilmExplain: null,
isILMAvailable,
newDocsCount,
patternRollup: undefined,
stats: mockStats,
})
@ -511,6 +613,8 @@ describe('helpers', () => {
shouldCreatePatternRollup({
error: null,
ilmExplain: mockIlmExplain,
isILMAvailable,
newDocsCount,
patternRollup: undefined,
stats: null,
})
@ -522,6 +626,8 @@ describe('helpers', () => {
shouldCreatePatternRollup({
error: 'whoops',
ilmExplain: null,
isILMAvailable,
newDocsCount,
patternRollup: undefined,
stats: null,
})
@ -533,6 +639,8 @@ describe('helpers', () => {
shouldCreatePatternRollup({
error: 'something went',
ilmExplain: null,
isILMAvailable,
newDocsCount,
patternRollup: undefined,
stats: mockStats,
})
@ -544,6 +652,8 @@ describe('helpers', () => {
shouldCreatePatternRollup({
error: 'horribly wrong',
ilmExplain: mockIlmExplain,
isILMAvailable,
newDocsCount,
patternRollup: undefined,
stats: null,
})
@ -555,6 +665,8 @@ describe('helpers', () => {
shouldCreatePatternRollup({
error: 'over here',
ilmExplain: mockIlmExplain,
isILMAvailable,
newDocsCount,
patternRollup: undefined,
stats: mockStats,
})

View file

@ -9,7 +9,7 @@ import type {
IlmExplainLifecycleLifecycleExplain,
IndicesStatsIndicesStats,
} from '@elastic/elasticsearch/lib/api/types';
import { orderBy } from 'lodash/fp';
import { isEqual, orderBy } from 'lodash/fp';
import type { IndexSummaryTableItem } from '../summary_table/helpers';
import type {
@ -46,9 +46,10 @@ export const getPhaseCount = ({
};
export const getIlmPhase = (
ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined
ilmExplainRecord: IlmExplainLifecycleLifecycleExplain | undefined,
isILMAvailable: boolean
): IlmPhase | undefined => {
if (ilmExplainRecord == null) {
if (ilmExplainRecord == null || !isILMAvailable) {
return undefined;
}
@ -142,6 +143,7 @@ export const getIndexIncompatible = ({
export const getSummaryTableItems = ({
ilmExplain,
indexNames,
isILMAvailable,
pattern,
patternDocsCount,
results,
@ -151,6 +153,7 @@ export const getSummaryTableItems = ({
}: {
ilmExplain: Record<string, IlmExplainLifecycleLifecycleExplain> | null;
indexNames: string[];
isILMAvailable: boolean;
pattern: string;
patternDocsCount: number;
results: Record<string, DataQualityCheckResult> | undefined;
@ -162,7 +165,10 @@ export const getSummaryTableItems = ({
docsCount: getDocsCount({ stats, indexName }),
incompatible: getIndexIncompatible({ indexName, results }),
indexName,
ilmPhase: ilmExplain != null ? getIlmPhase(ilmExplain[indexName]) : undefined,
ilmPhase:
isILMAvailable && ilmExplain != null
? getIlmPhase(ilmExplain[indexName], isILMAvailable)
: undefined,
pattern,
patternDocsCount,
sizeInBytes: getSizeInBytes({ stats, indexName }),
@ -174,29 +180,44 @@ export const getSummaryTableItems = ({
export const shouldCreateIndexNames = ({
ilmExplain,
indexNames,
isILMAvailable,
newIndexNames,
stats,
}: {
ilmExplain: Record<string, IlmExplainLifecycleLifecycleExplain> | null;
indexNames: string[] | undefined;
isILMAvailable: boolean;
newIndexNames: string[];
stats: Record<string, IndicesStatsIndicesStats> | null;
}): boolean => indexNames == null && stats != null && ilmExplain != null;
}): boolean => {
return (
!isEqual(newIndexNames, indexNames) &&
stats != null &&
((isILMAvailable && ilmExplain != null) || !isILMAvailable)
);
};
export const shouldCreatePatternRollup = ({
error,
ilmExplain,
isILMAvailable,
newDocsCount,
patternRollup,
stats,
}: {
error: string | null;
ilmExplain: Record<string, IlmExplainLifecycleLifecycleExplain> | null;
isILMAvailable: boolean;
newDocsCount: number;
patternRollup: PatternRollup | undefined;
stats: Record<string, IndicesStatsIndicesStats> | null;
}): boolean => {
if (patternRollup != null) {
return false; // the rollup already exists
if (patternRollup?.docsCount === newDocsCount) {
return false;
}
const allDataLoaded: boolean = stats != null && ilmExplain != null;
const allDataLoaded: boolean =
stats != null && ((isILMAvailable && ilmExplain != null) || !isILMAvailable);
const errorOccurred: boolean = error != null;
return allDataLoaded || errorOccurred;

View file

@ -50,6 +50,7 @@ import * as i18n from './translations';
import type { PatternRollup, SelectedIndex, SortConfig } from '../../types';
import { useIlmExplain } from '../../use_ilm_explain';
import { useStats } from '../../use_stats';
import { useDataQualityContext } from '../data_quality_context';
const IndexPropertiesContainer = styled.div`
margin-bottom: ${euiThemeVars.euiSizeS};
@ -60,7 +61,9 @@ const EMPTY_INDEX_NAMES: string[] = [];
interface Props {
addSuccessToast: (toast: { title: string }) => void;
baseTheme: Theme;
canUserCreateAndReadCases: () => boolean;
endDate?: string | null;
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
getGroupByFieldsOnClick: (
@ -90,8 +93,8 @@ interface Props {
patternRollup: PatternRollup | undefined;
selectedIndex: SelectedIndex | null;
setSelectedIndex: (selectedIndex: SelectedIndex | null) => void;
startDate?: string | null;
theme?: PartialTheme;
baseTheme: Theme;
updatePatternIndexNames: ({
indexNames,
pattern,
@ -105,6 +108,7 @@ interface Props {
const PatternComponent: React.FC<Props> = ({
addSuccessToast,
canUserCreateAndReadCases,
endDate,
formatBytes,
formatNumber,
getGroupByFieldsOnClick,
@ -116,17 +120,23 @@ const PatternComponent: React.FC<Props> = ({
patternRollup,
selectedIndex,
setSelectedIndex,
startDate,
theme,
baseTheme,
updatePatternIndexNames,
updatePatternRollup,
}) => {
const containerRef = useRef<HTMLDivElement | null>(null);
const { isILMAvailable } = useDataQualityContext();
const [sorting, setSorting] = useState<SortConfig>(defaultSort);
const [pageIndex, setPageIndex] = useState<number>(0);
const [pageSize, setPageSize] = useState<number>(MIN_PAGE_SIZE);
const { error: statsError, loading: loadingStats, stats } = useStats(pattern);
const {
error: statsError,
loading: loadingStats,
stats,
} = useStats({ pattern, startDate, endDate });
const { error: ilmExplainError, loading: loadingIlmExplain, ilmExplain } = useIlmExplain(pattern);
const loading = useMemo(
@ -154,7 +164,11 @@ const PatternComponent: React.FC<Props> = ({
formatNumber={formatNumber}
docsCount={getDocsCount({ stats, indexName })}
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
ilmPhase={ilmExplain != null ? getIlmPhase(ilmExplain[indexName]) : undefined}
ilmPhase={
isILMAvailable && ilmExplain != null
? getIlmPhase(ilmExplain?.[indexName], isILMAvailable)
: undefined
}
indexId={getIndexId({ stats, indexName })}
indexName={indexName}
isAssistantEnabled={isAssistantEnabled}
@ -179,6 +193,7 @@ const PatternComponent: React.FC<Props> = ({
stats,
getGroupByFieldsOnClick,
ilmExplain,
isILMAvailable,
isAssistantEnabled,
openCreateCaseFlyout,
pattern,
@ -189,13 +204,17 @@ const PatternComponent: React.FC<Props> = ({
]
);
const ilmExplainPhaseCounts = useMemo(() => getIlmExplainPhaseCounts(ilmExplain), [ilmExplain]);
const ilmExplainPhaseCounts = useMemo(
() => (isILMAvailable ? getIlmExplainPhaseCounts(ilmExplain) : undefined),
[ilmExplain, isILMAvailable]
);
const items = useMemo(
() =>
getSummaryTableItems({
ilmExplain,
indexNames: indexNames ?? EMPTY_INDEX_NAMES,
isILMAvailable,
pattern,
patternDocsCount: patternRollup?.docsCount ?? 0,
results: patternRollup?.results,
@ -206,6 +225,7 @@ const PatternComponent: React.FC<Props> = ({
[
ilmExplain,
indexNames,
isILMAvailable,
pattern,
patternRollup?.docsCount,
patternRollup?.results,
@ -216,27 +236,44 @@ const PatternComponent: React.FC<Props> = ({
);
useEffect(() => {
if (shouldCreateIndexNames({ indexNames, stats, ilmExplain })) {
const newIndexNames = getIndexNames({ stats, ilmExplain, ilmPhases, isILMAvailable });
const newDocsCount = getTotalDocsCount({ indexNames: newIndexNames, stats });
if (
shouldCreateIndexNames({
indexNames,
ilmExplain,
isILMAvailable,
newIndexNames,
stats,
})
) {
updatePatternIndexNames({
indexNames: getIndexNames({ stats, ilmExplain, ilmPhases }),
indexNames: newIndexNames,
pattern,
});
}
if (shouldCreatePatternRollup({ error, patternRollup, stats, ilmExplain })) {
if (
shouldCreatePatternRollup({
error,
ilmExplain,
isILMAvailable,
newDocsCount,
patternRollup,
stats,
})
) {
updatePatternRollup({
docsCount: getTotalDocsCount({
indexNames: getIndexNames({ stats, ilmExplain, ilmPhases }),
stats,
}),
docsCount: newDocsCount,
error,
ilmExplain,
ilmExplainPhaseCounts,
indices: getIndexNames({ stats, ilmExplain, ilmPhases }).length,
indices: getIndexNames({ stats, ilmExplain, ilmPhases, isILMAvailable }).length,
pattern,
results: undefined,
sizeInBytes: getTotalSizeInBytes({
indexNames: getIndexNames({ stats, ilmExplain, ilmPhases }),
indexNames: getIndexNames({ stats, ilmExplain, ilmPhases, isILMAvailable }),
stats,
}),
stats,
@ -248,6 +285,7 @@ const PatternComponent: React.FC<Props> = ({
ilmExplainPhaseCounts,
ilmPhases,
indexNames,
isILMAvailable,
pattern,
patternRollup,
stats,

View file

@ -15,7 +15,7 @@ import { StatsRollup } from './stats_rollup';
interface Props {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
ilmExplainPhaseCounts: IlmExplainPhaseCounts;
ilmExplainPhaseCounts: IlmExplainPhaseCounts | undefined;
incompatible: number | undefined;
indices: number | undefined;
indicesChecked: number | undefined;

View file

@ -23,7 +23,7 @@ interface Props {
incompatible: number | undefined;
indices: number | undefined;
indicesChecked: number | undefined;
ilmExplainPhaseCounts: IlmExplainPhaseCounts;
ilmExplainPhaseCounts: IlmExplainPhaseCounts | undefined;
pattern: string;
}
@ -63,7 +63,9 @@ const PatternLabelComponent: React.FC<Props> = ({
</EuiFlexGroup>
<EuiSpacer size="xs" />
<IlmPhaseCounts ilmExplainPhaseCounts={ilmExplainPhaseCounts} pattern={pattern} />
{ilmExplainPhaseCounts && (
<IlmPhaseCounts ilmExplainPhaseCounts={ilmExplainPhaseCounts} pattern={pattern} />
)}
</>
);

View file

@ -42,6 +42,7 @@ const patternRollups: Record<string, PatternRollup> = {
const flattenedBuckets = getFlattenedBuckets({
ilmPhases,
isILMAvailable: true,
patternRollups,
});

View file

@ -21,6 +21,7 @@ import {
getResultToolTip,
getShowPagination,
getSummaryTableColumns,
getSummaryTableILMPhaseColumn,
getToggleButtonId,
IndexSummaryTableItem,
} from './helpers';
@ -132,6 +133,7 @@ describe('helpers', () => {
describe('getSummaryTableColumns', () => {
const indexName = '.ds-auditbeat-8.6.1-2023.02.07-000001';
const isILMAvailable = true;
const indexSummaryTableItem: IndexSummaryTableItem = {
indexName,
@ -153,6 +155,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
}).map((x) => omit('render', x));
@ -194,6 +197,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -219,6 +223,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap,
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -242,6 +247,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded,
});
@ -273,6 +279,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -295,6 +302,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -321,6 +329,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -344,6 +353,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -367,6 +377,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -401,6 +412,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -422,6 +434,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -440,12 +453,26 @@ describe('helpers', () => {
});
});
describe('getSummaryTableILMPhaseColumn', () => {
test('it returns the expected column configuration when `isILMAvailable` is true', () => {
const column = getSummaryTableILMPhaseColumn(isILMAvailable);
expect(column.length).toEqual(1);
expect(column[0].name).toEqual('ILM Phase');
});
test('it returns an emptry array when `isILMAvailable` is false', () => {
const column = getSummaryTableILMPhaseColumn(false);
expect(column.length).toEqual(0);
});
});
describe('ilmPhase column render()', () => {
test('it renders the expected ilmPhase badge content', () => {
const columns = getSummaryTableColumns({
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -471,6 +498,32 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
const ilmPhaseRender = (columns[5] as EuiTableFieldDataColumnType<IndexSummaryTableItem>)
.render;
render(
<TestProviders>
{ilmPhaseRender != null && ilmPhaseRender(ilmPhaseIsUndefined, ilmPhaseIsUndefined)}
</TestProviders>
);
expect(screen.queryByTestId('ilmPhase')).not.toBeInTheDocument();
});
test('it does NOT render the ilmPhase badge when `isILMAvailable` is false', () => {
const ilmPhaseIsUndefined: IndexSummaryTableItem = {
...indexSummaryTableItem,
};
const columns = getSummaryTableColumns({
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable: false,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});
@ -493,6 +546,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
itemIdToExpandedRowMap: {},
isILMAvailable,
pattern: 'auditbeat-*',
toggleExpanded: jest.fn(),
});

View file

@ -95,16 +95,40 @@ export const getToggleButtonId = ({
pattern: string;
}): string => (isExpanded ? `collapse${indexName}${pattern}` : `expand${indexName}${pattern}`);
export const getSummaryTableILMPhaseColumn = (
isILMAvailable: boolean
): Array<EuiBasicTableColumn<IndexSummaryTableItem>> =>
isILMAvailable
? [
{
field: 'ilmPhase',
name: i18n.ILM_PHASE,
render: (_, { ilmPhase }) =>
ilmPhase != null ? (
<EuiToolTip content={getIlmPhaseDescription(ilmPhase)}>
<EuiBadge color="hollow" data-test-subj="ilmPhase">
{ilmPhase}
</EuiBadge>
</EuiToolTip>
) : null,
sortable: true,
truncateText: false,
},
]
: [];
export const getSummaryTableColumns = ({
formatBytes,
formatNumber,
itemIdToExpandedRowMap,
isILMAvailable,
pattern,
toggleExpanded,
}: {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
itemIdToExpandedRowMap: Record<string, React.ReactNode>;
isILMAvailable: boolean;
pattern: string;
toggleExpanded: (indexName: string) => void;
}): Array<EuiBasicTableColumn<IndexSummaryTableItem>> => [
@ -201,20 +225,7 @@ export const getSummaryTableColumns = ({
sortable: true,
truncateText: false,
},
{
field: 'ilmPhase',
name: i18n.ILM_PHASE,
render: (_, { ilmPhase }) =>
ilmPhase != null ? (
<EuiToolTip content={getIlmPhaseDescription(ilmPhase)}>
<EuiBadge color="hollow" data-test-subj="ilmPhase">
{ilmPhase}
</EuiBadge>
</EuiToolTip>
) : null,
sortable: true,
truncateText: false,
},
...getSummaryTableILMPhaseColumn(isILMAvailable),
{
field: 'sizeInBytes',
name: i18n.SIZE,

View file

@ -48,6 +48,7 @@ const pattern = 'auditbeat-*';
const items = getSummaryTableItems({
ilmExplain: mockIlmExplain,
indexNames: indexNames ?? [],
isILMAvailable: true,
pattern,
patternDocsCount: auditbeatWithAllResults?.docsCount ?? 0,
results: auditbeatWithAllResults?.results,

View file

@ -13,6 +13,7 @@ import type { IndexSummaryTableItem } from './helpers';
import { getShowPagination } from './helpers';
import { defaultSort, MIN_PAGE_SIZE } from '../pattern/helpers';
import { SortConfig } from '../../types';
import { useDataQualityContext } from '../data_quality_context';
export interface Props {
formatBytes: (value: number | undefined) => string;
@ -21,12 +22,14 @@ export interface Props {
formatBytes,
formatNumber,
itemIdToExpandedRowMap,
isILMAvailable,
pattern,
toggleExpanded,
}: {
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
itemIdToExpandedRowMap: Record<string, React.ReactNode>;
isILMAvailable: boolean;
pattern: string;
toggleExpanded: (indexName: string) => void;
}) => Array<EuiBasicTableColumn<IndexSummaryTableItem>>;
@ -57,16 +60,26 @@ const SummaryTableComponent: React.FC<Props> = ({
sorting,
toggleExpanded,
}) => {
const { isILMAvailable } = useDataQualityContext();
const columns = useMemo(
() =>
getTableColumns({
formatBytes,
formatNumber,
itemIdToExpandedRowMap,
isILMAvailable,
pattern,
toggleExpanded,
}),
[formatBytes, formatNumber, getTableColumns, itemIdToExpandedRowMap, pattern, toggleExpanded]
[
formatBytes,
formatNumber,
getTableColumns,
isILMAvailable,
itemIdToExpandedRowMap,
pattern,
toggleExpanded,
]
);
const getItemId = useCallback((item: IndexSummaryTableItem) => item.indexName, []);

View file

@ -79,6 +79,7 @@ ${ECS_IS_A_PERMISSIVE_SCHEMA}
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: true,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: 28413,
@ -91,5 +92,27 @@ ${ECS_IS_A_PERMISSIVE_SCHEMA}
'#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n',
]);
});
test('it returns the expected comment without ILM Phase when isILMAvailable is false', () => {
expect(
getAllCustomMarkdownComments({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual([
'### auditbeat-custom-index-1\n',
'| Result | Index | Docs | Incompatible fields | Size |\n|--------|-------|------|---------------------|------|\n| ❌ | auditbeat-custom-index-1 | 4 (0.0%) | 3 | 27.7KB |\n\n',
'### **Incompatible fields** `3` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n',
`#### 4 Custom field mappings\n\nThese fields are not defined by the Elastic Common Schema (ECS), version ${EcsVersion}.\n\nECS is a permissive schema. If your events have additional data that cannot be mapped to ECS, you can simply add them to your events, using custom field names.\n`,
'#### Custom fields - auditbeat-custom-index-1\n\n\n| Field | Index mapping type | \n|-------|--------------------|\n| host.name.keyword | `keyword` | `--` |\n| some.field | `text` | `--` |\n| some.field.keyword | `keyword` | `--` |\n| source.ip.keyword | `keyword` | `--` |\n',
]);
});
});
});

View file

@ -51,6 +51,7 @@ export const getAllCustomMarkdownComments = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,
@ -59,6 +60,7 @@ export const getAllCustomMarkdownComments = ({
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
ilmPhase: IlmPhase | undefined;
isILMAvailable: boolean;
indexName: string;
partitionedFieldMetadata: PartitionedFieldMetadata;
patternDocsCount: number;
@ -71,6 +73,7 @@ export const getAllCustomMarkdownComments = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,

View file

@ -24,6 +24,7 @@ import { getAllCustomMarkdownComments, showCustomCallout } from './helpers';
import * as i18n from '../../index_properties/translations';
import { COPIED_RESULTS_TOAST_TITLE } from '../../../translations';
import type { IlmPhase, PartitionedFieldMetadata } from '../../../types';
import { useDataQualityContext } from '../../data_quality_context';
interface Props {
addSuccessToast: (toast: { title: string }) => void;
@ -48,6 +49,7 @@ const CustomTabComponent: React.FC<Props> = ({
patternDocsCount,
sizeInBytes,
}) => {
const { isILMAvailable } = useDataQualityContext();
const markdownComments: string[] = useMemo(
() =>
getAllCustomMarkdownComments({
@ -56,6 +58,7 @@ const CustomTabComponent: React.FC<Props> = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,
@ -66,6 +69,7 @@ const CustomTabComponent: React.FC<Props> = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,

View file

@ -349,6 +349,7 @@ ${MAPPINGS_THAT_CONFLICT_WITH_ECS}
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
isILMAvailable: true,
indexName: 'auditbeat-custom-index-1',
partitionedFieldMetadata: mockPartitionedFieldMetadata,
patternDocsCount: 57410,
@ -376,6 +377,7 @@ ${MAPPINGS_THAT_CONFLICT_WITH_ECS}
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: true,
partitionedFieldMetadata: emptyIncompatible,
patternDocsCount: 57410,
sizeInBytes: 28413,
@ -387,5 +389,31 @@ ${MAPPINGS_THAT_CONFLICT_WITH_ECS}
'\n\n\n',
]);
});
test('it returns the expected comment when `isILMAvailable` is false', () => {
const emptyIncompatible: PartitionedFieldMetadata = {
...mockPartitionedFieldMetadata,
incompatible: [], // <-- empty
};
expect(
getAllIncompatibleMarkdownComments({
docsCount: 4,
formatBytes,
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable: false,
partitionedFieldMetadata: emptyIncompatible,
patternDocsCount: 57410,
sizeInBytes: 28413,
})
).toEqual([
'### auditbeat-custom-index-1\n',
'| Result | Index | Docs | Incompatible fields | Size |\n|--------|-------|------|---------------------|------|\n| ✅ | auditbeat-custom-index-1 | 4 (0.0%) | 0 | 27.7KB |\n\n',
'### **Incompatible fields** `0` **Custom fields** `4` **ECS compliant fields** `2` **All fields** `9`\n',
'\n\n\n',
]);
});
});
});

View file

@ -134,6 +134,7 @@ export const getAllIncompatibleMarkdownComments = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,
@ -143,6 +144,7 @@ export const getAllIncompatibleMarkdownComments = ({
formatNumber: (value: number | undefined) => string;
ilmPhase: IlmPhase | undefined;
indexName: string;
isILMAvailable: boolean;
partitionedFieldMetadata: PartitionedFieldMetadata;
patternDocsCount: number;
sizeInBytes: number | undefined;
@ -169,6 +171,7 @@ export const getAllIncompatibleMarkdownComments = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,

View file

@ -42,6 +42,7 @@ import {
} from '../../../translations';
import type { IlmPhase, PartitionedFieldMetadata } from '../../../types';
import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from '../summary_tab/callout_summary/translations';
import { useDataQualityContext } from '../../data_quality_context';
interface Props {
addSuccessToast: (toast: { title: string }) => void;
@ -72,6 +73,7 @@ const IncompatibleTabComponent: React.FC<Props> = ({
patternDocsCount,
sizeInBytes,
}) => {
const { isILMAvailable } = useDataQualityContext();
const body = useMemo(() => <EmptyPromptBody body={i18n.INCOMPATIBLE_EMPTY} />, []);
const title = useMemo(() => <EmptyPromptTitle title={i18n.INCOMPATIBLE_EMPTY_TITLE} />, []);
const incompatibleMappings = useMemo(
@ -90,6 +92,7 @@ const IncompatibleTabComponent: React.FC<Props> = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,
@ -100,6 +103,7 @@ const IncompatibleTabComponent: React.FC<Props> = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,

View file

@ -24,6 +24,7 @@ import {
} from '../../../../translations';
import type { IlmPhase, PartitionedFieldMetadata } from '../../../../types';
import { DATA_QUALITY_DASHBOARD_CONVERSATION_ID } from './translations';
import { useDataQualityContext } from '../../../data_quality_context';
interface Props {
addSuccessToast: (toast: { title: string }) => void;
@ -56,6 +57,7 @@ const CalloutSummaryComponent: React.FC<Props> = ({
patternDocsCount,
sizeInBytes,
}) => {
const { isILMAvailable } = useDataQualityContext();
const markdownComments: string[] = useMemo(
() =>
getMarkdownComments({
@ -64,6 +66,7 @@ const CalloutSummaryComponent: React.FC<Props> = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
pattern,
patternDocsCount,
@ -75,6 +78,7 @@ const CalloutSummaryComponent: React.FC<Props> = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
pattern,
patternDocsCount,

View file

@ -146,6 +146,7 @@ describe('helpers', () => {
const defaultNumberFormat = '0,0.[000]';
const formatNumber = (value: number | undefined) =>
value != null ? numeral(value).format(defaultNumberFormat) : EMPTY_STAT;
const isILMAvailable = true;
test('it returns the expected comment when the index has incompatible fields ', () => {
expect(
@ -155,6 +156,7 @@ describe('helpers', () => {
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
pattern: 'auditbeat-*',
patternDocsCount: 57410,
@ -182,6 +184,7 @@ describe('helpers', () => {
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-index-1',
isILMAvailable,
partitionedFieldMetadata: noIncompatible,
pattern: 'auditbeat-*',
patternDocsCount: 57410,
@ -217,6 +220,7 @@ describe('helpers', () => {
formatNumber,
ilmPhase: 'unmanaged',
indexName: 'auditbeat-custom-empty-index-1',
isILMAvailable,
partitionedFieldMetadata: emptyIndex,
pattern: 'auditbeat-*',
patternDocsCount: 57410,

View file

@ -83,6 +83,7 @@ export const getMarkdownComments = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,
@ -92,6 +93,7 @@ export const getMarkdownComments = ({
formatNumber: (value: number | undefined) => string;
ilmPhase: IlmPhase | undefined;
indexName: string;
isILMAvailable: boolean;
partitionedFieldMetadata: PartitionedFieldMetadata;
pattern: string;
patternDocsCount: number;
@ -104,6 +106,7 @@ export const getMarkdownComments = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,

View file

@ -79,6 +79,7 @@ const ecsMetadata: Record<string, EcsMetadata> = EcsFlat as unknown as Record<st
describe('helpers', () => {
describe('getIndexNames', () => {
const isILMAvailable = true;
const ilmPhases = ['hot', 'warm', 'unmanaged'];
test('returns the expected index names when they have an ILM phase included in the ilmPhases list', () => {
@ -86,6 +87,7 @@ describe('helpers', () => {
getIndexNames({
ilmExplain: mockIlmExplain, // <-- the mock indexes have 'hot' ILM phases
ilmPhases,
isILMAvailable,
stats: mockStats,
})
).toEqual([
@ -100,6 +102,7 @@ describe('helpers', () => {
getIndexNames({
ilmExplain: mockIlmExplain, // <-- the mock indexes have 'hot' and 'unmanaged' ILM phases...
ilmPhases: ['warm', 'unmanaged'], // <-- ...but we don't ask for 'hot'
isILMAvailable,
stats: mockStats,
})
).toEqual(['auditbeat-custom-index-1']); // <-- the 'unmanaged' index
@ -116,6 +119,7 @@ describe('helpers', () => {
getIndexNames({
ilmExplain: ilmExplainWithMissingIndex, // <-- the mock indexes have 'hot' ILM phases...
ilmPhases: ['hot', 'warm', 'unmanaged'],
isILMAvailable,
stats: mockStats,
})
).toEqual(['.ds-packetbeat-8.5.3-2023.02.04-000001', 'auditbeat-custom-index-1']); // <-- only includes two of the three indices, because the other one is missing an ILM explain record
@ -126,6 +130,7 @@ describe('helpers', () => {
getIndexNames({
ilmExplain: mockIlmExplain,
ilmPhases: [],
isILMAvailable,
stats: mockStats,
})
).toEqual([]);
@ -136,6 +141,7 @@ describe('helpers', () => {
getIndexNames({
ilmExplain: null,
ilmPhases,
isILMAvailable,
stats: mockStats,
})
).toEqual([]);
@ -146,6 +152,7 @@ describe('helpers', () => {
getIndexNames({
ilmExplain: mockIlmExplain,
ilmPhases,
isILMAvailable,
stats: null,
})
).toEqual([]);
@ -156,6 +163,7 @@ describe('helpers', () => {
getIndexNames({
ilmExplain: null,
ilmPhases,
isILMAvailable,
stats: null,
})
).toEqual([]);

View file

@ -31,17 +31,21 @@ const EMPTY_INDEX_NAMES: string[] = [];
export const getIndexNames = ({
ilmExplain,
ilmPhases,
isILMAvailable,
stats,
}: {
ilmExplain: Record<string, IlmExplainLifecycleLifecycleExplain> | null;
ilmPhases: string[];
isILMAvailable: boolean;
stats: Record<string, IndicesStatsIndicesStats> | null;
}): string[] => {
if (ilmExplain != null && stats != null) {
if (((isILMAvailable && ilmExplain != null) || !isILMAvailable) && stats != null) {
const allIndexNames = Object.keys(stats);
const filteredByIlmPhase = allIndexNames.filter((indexName) =>
ilmPhases.includes(getIlmPhase(ilmExplain[indexName]) ?? '')
);
const filteredByIlmPhase = isILMAvailable
? allIndexNames.filter((indexName) =>
ilmPhases.includes(getIlmPhase(ilmExplain?.[indexName], isILMAvailable) ?? '')
)
: allIndexNames;
return filteredByIlmPhase;
} else {

View file

@ -28,6 +28,7 @@ describe('DataQualityPanel', () => {
httpFetch={jest.fn()}
ilmPhases={ilmPhases}
isAssistantEnabled={true}
isILMAvailable={true}
lastChecked={''}
openCreateCaseFlyout={jest.fn()}
patterns={[]}
@ -63,6 +64,7 @@ describe('DataQualityPanel', () => {
httpFetch={jest.fn()}
ilmPhases={ilmPhases}
isAssistantEnabled={true}
isILMAvailable={true}
lastChecked={''}
openCreateCaseFlyout={jest.fn()}
patterns={[]}

View file

@ -26,9 +26,11 @@ import { ReportDataQualityCheckAllCompleted, ReportDataQualityIndexChecked } fro
interface Props {
addSuccessToast: (toast: { title: string }) => void;
baseTheme: Theme;
canUserCreateAndReadCases: () => boolean;
defaultNumberFormat: string;
defaultBytesFormat: string;
endDate?: string | null;
getGroupByFieldsOnClick: (
elements: Array<
| FlameElementEvent
@ -45,6 +47,7 @@ interface Props {
httpFetch: HttpHandler;
ilmPhases: string[];
isAssistantEnabled: boolean;
isILMAvailable: boolean;
lastChecked: string;
openCreateCaseFlyout: ({
comments,
@ -57,8 +60,8 @@ interface Props {
reportDataQualityIndexChecked?: ReportDataQualityIndexChecked;
reportDataQualityCheckAllCompleted?: ReportDataQualityCheckAllCompleted;
setLastChecked: (lastChecked: string) => void;
startDate?: string | null;
theme?: PartialTheme;
baseTheme: Theme;
}
/** Renders the `Data Quality` dashboard content */
@ -68,16 +71,19 @@ const DataQualityPanelComponent: React.FC<Props> = ({
canUserCreateAndReadCases,
defaultBytesFormat,
defaultNumberFormat,
endDate,
getGroupByFieldsOnClick,
httpFetch,
ilmPhases,
isAssistantEnabled,
isILMAvailable,
lastChecked,
openCreateCaseFlyout,
patterns,
reportDataQualityIndexChecked,
reportDataQualityCheckAllCompleted,
setLastChecked,
startDate,
theme,
}) => {
const formatBytes = useCallback(
@ -98,10 +104,15 @@ const DataQualityPanelComponent: React.FC<Props> = ({
);
return (
<DataQualityProvider httpFetch={httpFetch} telemetryEvents={telemetryEvents}>
<DataQualityProvider
httpFetch={httpFetch}
telemetryEvents={telemetryEvents}
isILMAvailable={isILMAvailable}
>
<Body
addSuccessToast={addSuccessToast}
canUserCreateAndReadCases={canUserCreateAndReadCases}
endDate={endDate}
formatBytes={formatBytes}
formatNumber={formatNumber}
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
@ -111,6 +122,7 @@ const DataQualityPanelComponent: React.FC<Props> = ({
openCreateCaseFlyout={openCreateCaseFlyout}
patterns={patterns}
setLastChecked={setLastChecked}
startDate={startDate}
theme={theme}
baseTheme={baseTheme}
/>

View file

@ -17,12 +17,13 @@ import { DataQualityProvider } from '../../data_quality_panel/data_quality_conte
interface Props {
children: React.ReactNode;
isILMAvailable?: boolean;
}
window.scrollTo = jest.fn();
/** A utility for wrapping children in the providers required to run tests */
export const TestProvidersComponent: React.FC<Props> = ({ children }) => {
export const TestProvidersComponent: React.FC<Props> = ({ children, isILMAvailable = true }) => {
const http = httpServiceMock.createSetupContract({ basePath: '/test' });
const actionTypeRegistry = actionTypeRegistryMock.create();
const mockGetInitialConversations = jest.fn(() => ({}));
@ -61,7 +62,11 @@ export const TestProvidersComponent: React.FC<Props> = ({ children }) => {
setDefaultAllowReplacement={jest.fn()}
http={mockHttp}
>
<DataQualityProvider httpFetch={http.fetch} telemetryEvents={mockTelemetryEvents}>
<DataQualityProvider
httpFetch={http.fetch}
isILMAvailable={isILMAvailable}
telemetryEvents={mockTelemetryEvents}
>
{children}
</DataQualityProvider>
</AssistantProvider>

View file

@ -20,8 +20,15 @@ const mockTelemetryEvents = {
reportDataQualityIndexChecked: mockReportDataQualityIndexChecked,
reportDataQualityCheckAllCompleted: mockReportDataQualityCheckAllClicked,
};
const ContextWrapper: React.FC = ({ children }) => (
<DataQualityProvider httpFetch={mockHttpFetch} telemetryEvents={mockTelemetryEvents}>
const ContextWrapper: React.FC<{ children: React.ReactNode; isILMAvailable: boolean }> = ({
children,
isILMAvailable = true,
}) => (
<DataQualityProvider
httpFetch={mockHttpFetch}
telemetryEvents={mockTelemetryEvents}
isILMAvailable={isILMAvailable}
>
{children}
</DataQualityProvider>
);
@ -59,6 +66,34 @@ describe('useIlmExplain', () => {
});
});
describe('skip ilm api when isILMAvailable is false', () => {
let ilmExplainResult: UseIlmExplain | undefined;
beforeEach(async () => {
const { result, waitForNextUpdate } = renderHook(() => useIlmExplain(pattern), {
wrapper: ({ children }) => (
<DataQualityProvider
httpFetch={mockHttpFetch}
telemetryEvents={mockTelemetryEvents}
isILMAvailable={false}
>
{children}
</DataQualityProvider>
),
});
await waitForNextUpdate();
ilmExplainResult = await result.current;
});
test('it returns the expected ilmExplain map', async () => {
expect(ilmExplainResult?.ilmExplain).toEqual(null);
});
test('it returns loading: false, because the request is aborted', async () => {
expect(ilmExplainResult?.loading).toBe(false);
});
});
describe('fetch rejects with an error', () => {
let ilmExplainResult: UseIlmExplain | undefined;
const errorMessage = 'simulated error';

View file

@ -20,7 +20,7 @@ export interface UseIlmExplain {
}
export const useIlmExplain = (pattern: string): UseIlmExplain => {
const { httpFetch } = useDataQualityContext();
const { httpFetch, isILMAvailable } = useDataQualityContext();
const [ilmExplain, setIlmExplain] = useState<Record<
string,
IlmExplainLifecycleLifecycleExplain
@ -34,6 +34,9 @@ export const useIlmExplain = (pattern: string): UseIlmExplain => {
async function fetchData() {
try {
const encodedIndexName = encodeURIComponent(`${pattern}`);
if (!isILMAvailable) {
abortController.abort();
}
const response = await httpFetch<Record<string, IlmExplainLifecycleLifecycleExplain>>(
`${ILM_EXPLAIN_ENDPOINT}/${encodedIndexName}`,
@ -51,9 +54,7 @@ export const useIlmExplain = (pattern: string): UseIlmExplain => {
setError(i18n.ERROR_LOADING_ILM_EXPLAIN(e.message));
}
} finally {
if (!abortController.signal.aborted) {
setLoading(false);
}
setLoading(false);
}
}
@ -62,7 +63,7 @@ export const useIlmExplain = (pattern: string): UseIlmExplain => {
return () => {
abortController.abort();
};
}, [httpFetch, pattern, setError]);
}, [httpFetch, isILMAvailable, pattern, setError]);
return { ilmExplain, error, loading };
};

View file

@ -22,7 +22,11 @@ const mockTelemetryEvents = {
};
const ContextWrapper: React.FC = ({ children }) => (
<DataQualityProvider httpFetch={mockHttpFetch} telemetryEvents={mockTelemetryEvents}>
<DataQualityProvider
httpFetch={mockHttpFetch}
telemetryEvents={mockTelemetryEvents}
isILMAvailable={true}
>
{children}
</DataQualityProvider>
);

View file

@ -158,6 +158,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001',
isILMAvailable: true,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
pattern: 'packetbeat-*',
patternRollups: {
@ -257,6 +258,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001',
isILMAvailable: true,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
pattern: 'packetbeat-*',
patternRollups: {
@ -351,6 +353,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001',
isILMAvailable: true,
partitionedFieldMetadata: null, // <--
pattern: 'packetbeat-*',
patternRollups: {
@ -444,6 +447,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001',
isILMAvailable: true,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
pattern: 'packetbeat-*',
patternRollups: {
@ -501,6 +505,7 @@ describe('helpers', () => {
formatBytes,
formatNumber,
indexName: '.ds-packetbeat-8.6.1-2023.02.04-000001',
isILMAvailable: true,
partitionedFieldMetadata: mockPartitionedFieldMetadata,
pattern: 'this-pattern-is-not-in-pattern-rollups', // <--
patternRollups: shouldNotBeModified,

View file

@ -90,6 +90,7 @@ export const updateResultOnCheckCompleted = ({
formatBytes,
formatNumber,
indexName,
isILMAvailable,
partitionedFieldMetadata,
pattern,
patternRollups,
@ -98,6 +99,7 @@ export const updateResultOnCheckCompleted = ({
formatBytes: (value: number | undefined) => string;
formatNumber: (value: number | undefined) => string;
indexName: string;
isILMAvailable: boolean;
partitionedFieldMetadata: PartitionedFieldMetadata | null;
pattern: string;
patternRollups: Record<string, PatternRollup>;
@ -108,7 +110,7 @@ export const updateResultOnCheckCompleted = ({
const ilmExplain = patternRollup.ilmExplain;
const ilmPhase: IlmPhase | undefined =
ilmExplain != null ? getIlmPhase(ilmExplain[indexName]) : undefined;
ilmExplain != null ? getIlmPhase(ilmExplain[indexName], isILMAvailable) : undefined;
const docsCount = getIndexDocsCountFromRollup({
indexName,
@ -127,6 +129,7 @@ export const updateResultOnCheckCompleted = ({
formatNumber,
ilmPhase,
indexName,
isILMAvailable,
partitionedFieldMetadata,
patternDocsCount,
sizeInBytes,

View file

@ -53,7 +53,7 @@ interface UseResultsRollup {
export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRollup => {
const [patternIndexNames, setPatternIndexNames] = useState<Record<string, string[]>>({});
const [patternRollups, setPatternRollups] = useState<Record<string, PatternRollup>>({});
const { telemetryEvents } = useDataQualityContext();
const { telemetryEvents, isILMAvailable } = useDataQualityContext();
const updatePatternRollup = useCallback((patternRollup: PatternRollup) => {
setPatternRollups((current) =>
onPatternRollupUpdated({ patternRollup, patternRollups: current })
@ -101,6 +101,7 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll
formatBytes,
formatNumber,
indexName,
isILMAvailable,
partitionedFieldMetadata,
pattern,
patternRollups: current,
@ -119,7 +120,7 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll
batchId,
ecsVersion: EcsVersion,
errorCount: error ? 1 : 0,
ilmPhase: getIlmPhase(ilmExplain[indexName]),
ilmPhase: getIlmPhase(ilmExplain[indexName], isILMAvailable),
indexId,
indexName,
isCheckAll: true,
@ -157,7 +158,7 @@ export const useResultsRollup = ({ ilmPhases, patterns }: Props): UseResultsRoll
return updated;
});
},
[patternRollups, telemetryEvents]
[isILMAvailable, patternRollups, telemetryEvents]
);
useEffect(() => {

View file

@ -22,25 +22,64 @@ const mockTelemetryEvents = {
};
const ContextWrapper: React.FC = ({ children }) => (
<DataQualityProvider httpFetch={mockHttpFetch} telemetryEvents={mockTelemetryEvents}>
<DataQualityProvider
httpFetch={mockHttpFetch}
telemetryEvents={mockTelemetryEvents}
isILMAvailable={true}
>
{children}
</DataQualityProvider>
);
const ContextWrapperILMNotAvailable: React.FC = ({ children }) => (
<DataQualityProvider
httpFetch={mockHttpFetch}
telemetryEvents={mockTelemetryEvents}
isILMAvailable={false}
>
{children}
</DataQualityProvider>
);
const pattern = 'auditbeat-*';
const startDate = `now-7d`;
const endDate = `now`;
const params = {
pattern,
};
describe('useStats', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('query with date range when ILM is not available', () => {
const queryParams = {
isILMAvailable: false,
startDate,
endDate,
};
beforeEach(async () => {
mockHttpFetch.mockResolvedValue(mockStatsGreenIndex);
const { waitForNextUpdate } = renderHook(() => useStats({ pattern, startDate, endDate }), {
wrapper: ContextWrapperILMNotAvailable,
});
await waitForNextUpdate();
});
test(`it calls the stats api with the expected params`, async () => {
expect(mockHttpFetch.mock.calls[0][1].query).toEqual(queryParams);
});
});
describe('successful response from the stats api', () => {
let statsResult: UseStats | undefined;
beforeEach(async () => {
mockHttpFetch.mockResolvedValue(mockStatsGreenIndex);
const { result, waitForNextUpdate } = renderHook(() => useStats(pattern), {
const { result, waitForNextUpdate } = renderHook(() => useStats(params), {
wrapper: ContextWrapper,
});
await waitForNextUpdate();
@ -58,6 +97,10 @@ describe('useStats', () => {
test('it returns a null error, because no errors occurred', async () => {
expect(statsResult?.error).toBeNull();
});
test(`it calls the stats api with the expected params`, async () => {
expect(mockHttpFetch.mock.calls[0][1].query).toEqual({ isILMAvailable: true });
});
});
describe('fetch rejects with an error', () => {
@ -67,7 +110,7 @@ describe('useStats', () => {
beforeEach(async () => {
mockHttpFetch.mockRejectedValue(new Error(errorMessage));
const { result, waitForNextUpdate } = renderHook(() => useStats(pattern), {
const { result, waitForNextUpdate } = renderHook(() => useStats(params), {
wrapper: ContextWrapper,
});
await waitForNextUpdate();

View file

@ -7,6 +7,7 @@
import type { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types';
import { useEffect, useState } from 'react';
import { HttpFetchQuery } from '@kbn/core/public';
import { useDataQualityContext } from '../data_quality_panel/data_quality_context';
import * as i18n from '../translations';
@ -19,8 +20,16 @@ export interface UseStats {
loading: boolean;
}
export const useStats = (pattern: string): UseStats => {
const { httpFetch } = useDataQualityContext();
export const useStats = ({
endDate,
pattern,
startDate,
}: {
endDate?: string | null;
pattern: string;
startDate?: string | null;
}): UseStats => {
const { httpFetch, isILMAvailable } = useDataQualityContext();
const [stats, setStats] = useState<Record<string, IndicesStatsIndicesStats> | null>(null);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState<boolean>(true);
@ -31,12 +40,22 @@ export const useStats = (pattern: string): UseStats => {
async function fetchData() {
try {
const encodedIndexName = encodeURIComponent(`${pattern}`);
const query: HttpFetchQuery = { isILMAvailable };
if (!isILMAvailable) {
if (startDate) {
query.startDate = startDate;
}
if (endDate) {
query.endDate = endDate;
}
}
const response = await httpFetch<Record<string, IndicesStatsIndicesStats>>(
`${STATS_ENDPOINT}/${encodedIndexName}`,
{
method: 'GET',
signal: abortController.signal,
query,
}
);
@ -59,7 +78,7 @@ export const useStats = (pattern: string): UseStats => {
return () => {
abortController.abort();
};
}, [httpFetch, pattern, setError]);
}, [endDate, httpFetch, isILMAvailable, pattern, setError, startDate]);
return { stats, error, loading };
};

View file

@ -25,7 +25,11 @@ const mockTelemetryEvents = {
};
const ContextWrapper: React.FC = ({ children }) => (
<DataQualityProvider httpFetch={mockHttpFetch} telemetryEvents={mockTelemetryEvents}>
<DataQualityProvider
httpFetch={mockHttpFetch}
telemetryEvents={mockTelemetryEvents}
isILMAvailable={true}
>
{children}
</DataQualityProvider>
);

View file

@ -24,5 +24,6 @@
"@kbn/core-http-browser-mocks",
"@kbn/elastic-assistant",
"@kbn/triggers-actions-ui-plugin",
"@kbn/core",
]
}

View file

@ -0,0 +1,46 @@
/*
* 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 { SearchRequest } from '@elastic/elasticsearch/lib/api/types';
export const getRequestBody = ({
indexPattern,
startDate = 'now-7d/d',
endDate = 'now/d',
}: {
indexPattern: string;
startDate: string;
endDate: string;
}): SearchRequest => ({
index: indexPattern,
aggs: {
index: {
terms: {
field: '_index',
},
},
},
size: 0,
query: {
bool: {
must: [],
filter: [
{
range: {
'@timestamp': {
format: 'strict_date_optional_time',
gte: startDate,
lte: endDate,
},
},
},
],
should: [],
must_not: [],
},
},
});

View file

@ -0,0 +1,21 @@
/*
* 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 type { ElasticsearchClient } from '@kbn/core/server';
import { getRequestBody } from '../helpers/get_available_indices';
type AggregateName = 'index';
interface Result {
index: {
buckets: Array<{ key: string }>;
doc_count: number;
};
}
export const fetchAvailableIndices = (
esClient: ElasticsearchClient,
params: { indexPattern: string; startDate: string; endDate: string }
) => esClient.search<AggregateName, Result>(getRequestBody(params));

View file

@ -8,7 +8,7 @@
import { IlmExplainLifecycleResponse } from '@elastic/elasticsearch/lib/api/types';
import type { IScopedClusterClient } from '@kbn/core/server';
export const fetchILMExplain = async (
export const fetchILMExplain = (
client: IScopedClusterClient,
indexPattern: string
): Promise<IlmExplainLifecycleResponse> =>

View file

@ -8,7 +8,7 @@
import { IndicesGetMappingIndexMappingRecord } from '@elastic/elasticsearch/lib/api/types';
import type { IScopedClusterClient } from '@kbn/core/server';
export const fetchMappings = async (
export const fetchMappings = (
client: IScopedClusterClient,
indexPattern: string
): Promise<Record<string, IndicesGetMappingIndexMappingRecord>> =>

View file

@ -8,7 +8,7 @@
import { IndicesStatsResponse } from '@elastic/elasticsearch/lib/api/types';
import type { IScopedClusterClient } from '@kbn/core/server';
export const fetchStats = async (
export const fetchStats = (
client: IScopedClusterClient,
indexPattern: string
): Promise<IndicesStatsResponse> =>

View file

@ -9,3 +9,4 @@ export * from './fetch_mappings';
export * from './fetch_stats';
export * from './get_unallowed_field_values';
export * from './fetch_ilm_explain';
export * from './fetch_available_indices';

View file

@ -6,7 +6,7 @@
*/
import { GET_INDEX_STATS } from '../../common/constants';
import { fetchStats } from '../lib';
import { fetchAvailableIndices, fetchStats } from '../lib';
import { serverMock } from '../__mocks__/server';
import { requestMock } from '../__mocks__/request';
@ -15,6 +15,7 @@ import { getIndexStatsRoute } from './get_index_stats';
jest.mock('../lib', () => ({
fetchStats: jest.fn(),
fetchAvailableIndices: jest.fn(),
}));
describe('getIndexStatsRoute route', () => {
@ -26,6 +27,11 @@ describe('getIndexStatsRoute route', () => {
params: {
pattern: 'auditbeat-*',
},
query: {
isILMAvailable: true,
startDate: `now-7d`,
endDate: `now`,
},
});
beforeEach(() => {
@ -56,6 +62,67 @@ describe('getIndexStatsRoute route', () => {
expect(response.status).toEqual(500);
expect(response.body).toEqual({ message: errorMessage, status_code: 500 });
});
test('requires date range when isILMAvailable is false', async () => {
const request = requestMock.create({
method: 'get',
path: GET_INDEX_STATS,
params: {
pattern: `auditbeat-*`,
},
query: {
isILMAvailable: false,
},
});
const mockIndices = { 'auditbeat-7.15.1-2022.12.06-000001': {} };
(fetchStats as jest.Mock).mockResolvedValue({
indices: mockIndices,
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(400);
expect(response.body.status_code).toEqual(400);
expect(response.body.message).toEqual(`startDate and endDate are required`);
});
test('returns available indices within the given date range when isILMAvailable is false', async () => {
const request = requestMock.create({
method: 'get',
path: GET_INDEX_STATS,
params: {
pattern: `auditbeat-*`,
},
query: {
isILMAvailable: false,
startDate: `now-7d`,
endDate: `now`,
},
});
const mockIndices = {
'auditbeat-7.15.1-2022.12.06-000001': {},
'auditbeat-7.15.1-2022.11.06-000001': {},
};
(fetchStats as jest.Mock).mockResolvedValue({
indices: mockIndices,
});
(fetchAvailableIndices as jest.Mock).mockResolvedValue({
aggregations: {
index: {
buckets: [
{
key: 'auditbeat-7.15.1-2022.12.06-000001',
},
],
},
},
});
const response = await server.inject(request, requestContextMock.convertContext(context));
expect(response.status).toEqual(200);
expect(response.body).toEqual({ 'auditbeat-7.15.1-2022.12.06-000001': {} });
});
});
describe('request validation', () => {

View file

@ -4,37 +4,83 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { i18n } from '@kbn/i18n';
import { IRouter } from '@kbn/core/server';
import { transformError } from '@kbn/securitysolution-es-utils';
import { fetchStats } from '../lib';
import { IndicesStatsIndicesStats } from '@elastic/elasticsearch/lib/api/types';
import { fetchStats, fetchAvailableIndices } from '../lib';
import { buildResponse } from '../lib/build_response';
import { GET_INDEX_STATS } from '../../common/constants';
import { buildRouteValidation } from '../schemas/common';
import { GetIndexStatsParams } from '../schemas/get_index_stats';
import { GetIndexStatsParams, GetIndexStatsQuery } from '../schemas/get_index_stats';
export const getIndexStatsRoute = (router: IRouter) => {
router.get(
{
path: GET_INDEX_STATS,
validate: { params: buildRouteValidation(GetIndexStatsParams) },
validate: {
params: buildRouteValidation(GetIndexStatsParams),
query: buildRouteValidation(GetIndexStatsQuery),
},
},
async (context, request, response) => {
const resp = buildResponse(response);
try {
const { client } = (await context.core).elasticsearch;
const esClient = client.asCurrentUser;
const decodedIndexName = decodeURIComponent(request.params.pattern);
const stats = await fetchStats(client, decodedIndexName);
const { isILMAvailable, startDate, endDate } = request.query;
return response.ok({
body: stats.indices,
});
if (isILMAvailable === true) {
return response.ok({
body: stats.indices,
});
}
/**
* If ILM is not available, we need to fetch the available indices with the given date range.
* `fetchAvailableIndices` returns indices that have data in the given date range.
*/
if (startDate && endDate) {
const decodedStartDate = decodeURIComponent(startDate);
const decodedEndDate = decodeURIComponent(endDate);
const indices = await fetchAvailableIndices(esClient, {
indexPattern: decodedIndexName,
startDate: decodedStartDate,
endDate: decodedEndDate,
});
const availableIndices = indices?.aggregations?.index?.buckets?.reduce(
(acc: Record<string, IndicesStatsIndicesStats>, { key }: { key: string }) => {
if (stats.indices?.[key]) {
acc[key] = stats.indices?.[key];
}
return acc;
},
{}
);
return response.ok({
body: availableIndices,
});
} else {
return resp.error({
body: i18n.translate(
'xpack.ecsDataQualityDashboard.getIndexStats.dateRangeRequiredErrorMessage',
{
defaultMessage: 'startDate and endDate are required',
}
),
statusCode: 400,
});
}
} catch (err) {
const error = transformError(err);
return resp.error({
body: error.message,
statusCode: error.statusCode,

View file

@ -6,7 +6,14 @@
*/
import * as t from 'io-ts';
import { DefaultStringBooleanFalse } from '@kbn/securitysolution-io-ts-types';
export const GetIndexStatsParams = t.type({
pattern: t.string,
});
export const GetIndexStatsQuery = t.type({
isILMAvailable: DefaultStringBooleanFalse,
startDate: t.union([t.string, t.null, t.undefined]),
endDate: t.union([t.string, t.null, t.undefined]),
});

View file

@ -18,6 +18,8 @@
"@kbn/core-http-request-handler-context-server",
"@kbn/securitysolution-es-utils",
"@kbn/securitysolution-io-ts-utils",
"@kbn/securitysolution-io-ts-types",
"@kbn/i18n",
],
"exclude": [
"target/**/*",

View file

@ -22,6 +22,7 @@ const startMock = (): PluginStart => ({
getNavLinks$: jest.fn(() => new BehaviorSubject<NavigationLink[]>([])),
setIsSidebarEnabled: jest.fn(),
setGetStartedPage: jest.fn(),
setIsILMAvailable: jest.fn(),
getBreadcrumbsNav$: jest.fn(
() => new BehaviorSubject<BreadcrumbsNav>({ leading: [], trailing: [] })
),

View file

@ -8,6 +8,7 @@
import { render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { MemoryRouter } from 'react-router-dom';
import { of } from 'rxjs';
import { useKibana as mockUseKibana } from '../../common/lib/kibana/__mocks__';
import { TestProviders } from '../../common/mock';
@ -15,6 +16,7 @@ import { DataQuality } from './data_quality';
import { HOT, WARM, UNMANAGED } from './translations';
const mockedUseKibana = mockUseKibana();
const mockIsILMAvailable = of(true);
jest.mock('../../common/components/landing_page');
jest.mock('../../common/lib/kibana', () => {
@ -49,6 +51,7 @@ jest.mock('../../common/lib/kibana', () => {
useCasesAddToNewCaseFlyout: jest.fn(),
},
},
isILMAvailable$: mockIsILMAvailable,
},
}),
useUiSetting$: () => ['0,0.[000]'],

View file

@ -15,7 +15,7 @@ import {
INDEX_LIFECYCLE_MANAGEMENT_PHASES,
SELECT_ONE_OR_MORE_ILM_PHASES,
} from '@kbn/ecs-data-quality-dashboard';
import type { EuiComboBoxOptionOption } from '@elastic/eui';
import type { EuiComboBoxOptionOption, OnTimeChangeProps } from '@elastic/eui';
import {
EuiComboBox,
EuiFormControlLayout,
@ -25,9 +25,11 @@ import {
EuiText,
EuiToolTip,
useGeneratedHtmlId,
EuiSuperDatePicker,
} from '@elastic/eui';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import styled from 'styled-components';
import useObservable from 'react-use/lib/useObservable';
import { useAssistantAvailability } from '../../assistant/use_assistant_availability';
import { SecurityPageName } from '../../app/types';
@ -120,6 +122,9 @@ const defaultOptions: EuiComboBoxOptionOption[] = [
},
];
const DEFAULT_START_TIME = 'now-7d';
const DEFAULT_END_TIME = 'now';
const renderOption = (
option: EuiComboBoxOptionOption<string | number | string[] | undefined>
): React.ReactNode => (
@ -152,6 +157,26 @@ const DataQualityComponent: React.FC = () => {
const [selectedOptions, setSelectedOptions] = useState<EuiComboBoxOptionOption[]>(defaultOptions);
const { indicesExist, loading: isSourcererLoading, selectedPatterns } = useSourcererDataView();
const { signalIndexName, loading: isSignalIndexNameLoading } = useSignalIndex();
const { isILMAvailable$, cases } = useKibana().services;
const isILMAvailable = useObservable(isILMAvailable$);
const [startDate, setStartTime] = useState<string>();
const [endDate, setEndTime] = useState<string>();
const onTimeChange = ({ start, end, isInvalid }: OnTimeChangeProps) => {
if (isInvalid) {
return;
}
setStartTime(start);
setEndTime(end);
};
useEffect(() => {
if (isILMAvailable != null && isILMAvailable === false) {
setStartTime(DEFAULT_START_TIME);
setEndTime(DEFAULT_END_TIME);
}
}, [isILMAvailable]);
const alertsAndSelectedPatterns = useMemo(
() =>
@ -192,7 +217,6 @@ const DataQualityComponent: React.FC = () => {
[userCasesPermissions.create, userCasesPermissions.read]
);
const { cases } = useKibana().services;
const createCaseFlyout = cases.hooks.useCasesAddToNewCaseFlyout({
toastContent: i18n.ADD_TO_CASE_SUCCESS,
});
@ -231,41 +255,57 @@ const DataQualityComponent: React.FC = () => {
return (
<>
{indicesExist ? (
{indicesExist && isILMAvailable != null ? (
<SecuritySolutionPageWrapper data-test-subj="ecsDataQualityDashboardPage">
<HeaderPage subtitle={subtitle} title={i18n.DATA_QUALITY_TITLE}>
<EuiToolTip content={INDEX_LIFECYCLE_MANAGEMENT_PHASES}>
<FormControlLayout prepend={ilmFormLabel}>
<EuiComboBox
id={labelInputId}
data-test-subj="selectIlmPhases"
placeholder={SELECT_ONE_OR_MORE_ILM_PHASES}
renderOption={renderOption}
selectedOptions={selectedOptions}
style={comboBoxStyle}
options={options}
onChange={setSelectedOptions}
{isILMAvailable && (
<EuiToolTip content={INDEX_LIFECYCLE_MANAGEMENT_PHASES}>
<FormControlLayout prepend={ilmFormLabel}>
<EuiComboBox
id={labelInputId}
data-test-subj="selectIlmPhases"
placeholder={SELECT_ONE_OR_MORE_ILM_PHASES}
renderOption={renderOption}
selectedOptions={selectedOptions}
style={comboBoxStyle}
options={options}
onChange={setSelectedOptions}
/>
</FormControlLayout>
</EuiToolTip>
)}
{!isILMAvailable && startDate && endDate && (
<EuiToolTip content={i18n.DATE_PICKER_TOOLTIP}>
<EuiSuperDatePicker
start={startDate}
end={endDate}
onTimeChange={onTimeChange}
showUpdateButton={false}
isDisabled={true}
/>
</FormControlLayout>
</EuiToolTip>
</EuiToolTip>
)}
</HeaderPage>
<DataQualityPanel
addSuccessToast={addSuccessToast}
baseTheme={baseTheme}
canUserCreateAndReadCases={canUserCreateAndReadCases}
defaultBytesFormat={defaultBytesFormat}
defaultNumberFormat={defaultNumberFormat}
endDate={endDate}
getGroupByFieldsOnClick={getGroupByFieldsOnClick}
reportDataQualityCheckAllCompleted={reportDataQualityCheckAllCompleted}
reportDataQualityIndexChecked={reportDataQualityIndexChecked}
httpFetch={httpFetch}
ilmPhases={ilmPhases}
isAssistantEnabled={hasAssistantPrivilege}
isILMAvailable={isILMAvailable}
lastChecked={lastChecked}
openCreateCaseFlyout={openCreateCaseFlyout}
patterns={alertsAndSelectedPatterns}
setLastChecked={setLastChecked}
baseTheme={baseTheme}
startDate={startDate}
theme={theme}
/>
</SecuritySolutionPageWrapper>

View file

@ -29,6 +29,13 @@ export const DATA_QUALITY_TITLE = i18n.translate(
}
);
export const DATE_PICKER_TOOLTIP = i18n.translate(
'xpack.securitySolution.dataQualityDashboard.datePicker.tooltip',
{
defaultMessage: `Conducting data quality checks is possible solely for the data present within the Ingest and Search Boost window (last 7 days).`,
}
);
export const ELASTIC_COMMON_SCHEMA = i18n.translate(
'xpack.securitySolution.dataQualityDashboard.elasticCommonSchemaReferenceLink',
{

View file

@ -14,6 +14,7 @@ import { navLinks$ } from './common/links/nav_links';
import { breadcrumbsNav$ } from './common/breadcrumbs';
export class PluginContract {
public isILMAvailable$: BehaviorSubject<boolean>;
public isSidebarEnabled$: BehaviorSubject<boolean>;
public getStartedComponent$: BehaviorSubject<React.ComponentType | null>;
public upsellingService: UpsellingService;
@ -22,6 +23,7 @@ export class PluginContract {
constructor() {
this.extraRoutes$ = new BehaviorSubject<RouteProps[]>([]);
this.isILMAvailable$ = new BehaviorSubject<boolean>(true);
this.isSidebarEnabled$ = new BehaviorSubject<boolean>(true);
this.getStartedComponent$ = new BehaviorSubject<React.ComponentType | null>(null);
this.upsellingService = new UpsellingService();
@ -31,6 +33,7 @@ export class PluginContract {
public getStartServices(): ContractStartServices {
return {
extraRoutes$: this.extraRoutes$.asObservable(),
isILMAvailable$: this.isILMAvailable$.asObservable(),
isSidebarEnabled$: this.isSidebarEnabled$.asObservable(),
getStartedComponent$: this.getStartedComponent$.asObservable(),
upselling: this.upsellingService,
@ -52,6 +55,7 @@ export class PluginContract {
setExtraRoutes: (extraRoutes) => this.extraRoutes$.next(extraRoutes),
setIsSidebarEnabled: (isSidebarEnabled: boolean) =>
this.isSidebarEnabled$.next(isSidebarEnabled),
setIsILMAvailable: (isILMAvailable: boolean) => this.isILMAvailable$.next(isILMAvailable),
setGetStartedPage: (getStartedComponent) => {
this.getStartedComponent$.next(getStartedComponent);
},

View file

@ -139,6 +139,7 @@ export interface StartPluginsDependencies extends StartPlugins {
export interface ContractStartServices {
extraRoutes$: Observable<RouteProps[]>;
isILMAvailable$: Observable<boolean>;
isSidebarEnabled$: Observable<boolean>;
getStartedComponent$: Observable<React.ComponentType | null>;
upselling: UpsellingService;
@ -175,6 +176,7 @@ export interface PluginSetup {
export interface PluginStart {
getNavLinks$: () => Observable<NavigationLink[]>;
setExtraRoutes: (extraRoutes: RouteProps[]) => void;
setIsILMAvailable: (isILMAvailable: boolean) => void;
setIsSidebarEnabled: (isSidebarEnabled: boolean) => void;
setGetStartedPage: (getStartedComponent: React.ComponentType) => void;
getBreadcrumbsNav$: () => Observable<BreadcrumbsNav>;

View file

@ -45,6 +45,8 @@ export class SecuritySolutionEssPlugin
});
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services));
securitySolution.setIsILMAvailable(true);
subscribeBreadcrumbs(services);
return {};

View file

@ -57,6 +57,7 @@ export class SecuritySolutionServerlessPlugin
registerUpsellings(securitySolution.getUpselling(), this.config.productTypes, services);
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services, productTypes));
securitySolution.setIsILMAvailable(false);
configureNavigation(services, this.config);
setRoutes(services);