mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[SecuritySolution] Data Quality dashboard in serverless (#163733)
This commit is contained in:
parent
e8127e275f
commit
8da14b7c4c
67 changed files with 1107 additions and 132 deletions
|
@ -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",
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -42,6 +42,7 @@ const patternRollups: Record<string, PatternRollup> = {
|
|||
|
||||
const flattenedBuckets = getFlattenedBuckets({
|
||||
ilmPhases,
|
||||
isILMAvailable: true,
|
||||
patternRollups,
|
||||
});
|
||||
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -48,6 +48,7 @@ const pattern = 'auditbeat-*';
|
|||
const items = getSummaryTableItems({
|
||||
ilmExplain: mockIlmExplain,
|
||||
indexNames: indexNames ?? [],
|
||||
isILMAvailable: true,
|
||||
pattern,
|
||||
patternDocsCount: auditbeatWithAllResults?.docsCount ?? 0,
|
||||
results: auditbeatWithAllResults?.results,
|
||||
|
|
|
@ -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, []);
|
||||
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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([]);
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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={[]}
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(() => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -24,5 +24,6 @@
|
|||
"@kbn/core-http-browser-mocks",
|
||||
"@kbn/elastic-assistant",
|
||||
"@kbn/triggers-actions-ui-plugin",
|
||||
"@kbn/core",
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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: [],
|
||||
},
|
||||
},
|
||||
});
|
|
@ -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));
|
|
@ -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> =>
|
||||
|
|
|
@ -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>> =>
|
||||
|
|
|
@ -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> =>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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]),
|
||||
});
|
||||
|
|
|
@ -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/**/*",
|
||||
|
|
|
@ -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: [] })
|
||||
),
|
||||
|
|
|
@ -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]'],
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
{
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -45,6 +45,8 @@ export class SecuritySolutionEssPlugin
|
|||
});
|
||||
|
||||
securitySolution.setGetStartedPage(getSecurityGetStartedComponent(services));
|
||||
securitySolution.setIsILMAvailable(true);
|
||||
|
||||
subscribeBreadcrumbs(services);
|
||||
|
||||
return {};
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue