Skip to content

Commit

Permalink
[ML] AiOps: Action for adding Log Rate analysis embeddable to a dashb…
Browse files Browse the repository at this point in the history
…oard (elastic#200557)

## Summary

Part of: [elastic#197247](elastic#197247)

- Added the ability to add a Log Rate Analysis embeddable to a dashboard



https://github.com/user-attachments/assets/37efd83e-9196-434d-a80d-9249623f3222



### Checklist

Check the PR satisfies following conditions. 

Reviewers should verify this PR satisfies this list as well.

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios
  • Loading branch information
rbrtj authored Nov 20, 2024
1 parent 158a828 commit 50a2626
Show file tree
Hide file tree
Showing 7 changed files with 282 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ export interface DocumentCountContentProps {
barStyleAccessor?: BarStyleAccessor;
baselineAnnotationStyle?: RectAnnotationSpec['style'];
deviationAnnotationStyle?: RectAnnotationSpec['style'];
attachmentsMenu?: React.ReactNode;
}

export const DocumentCountContent: FC<DocumentCountContentProps> = ({
barColorOverride,
barHighlightColorOverride,
attachmentsMenu,
...docCountChartProps
}) => {
const { data, uiSettings, fieldFormats, charts, embeddingOrigin } = useAiopsAppContext();
Expand Down Expand Up @@ -64,7 +66,12 @@ export const DocumentCountContent: FC<DocumentCountContentProps> = ({
return (
<EuiFlexGroup gutterSize="m" direction="column">
<EuiFlexItem>
<TotalCountHeader totalCount={totalCount} sampleProbability={sampleProbability} />
<EuiFlexGroup alignItems="center" justifyContent="spaceBetween">
<EuiFlexItem grow={false}>
<TotalCountHeader totalCount={totalCount} sampleProbability={sampleProbability} />
</EuiFlexItem>
{attachmentsMenu && <EuiFlexItem grow={false}>{attachmentsMenu}</EuiFlexItem>}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<DocumentCountChartRedux
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export const AttachmentsMenu = ({
]
);

const panels = useMemo<EuiContextMenuProps['panels']>(() => {
const panels = useMemo<Exclude<EuiContextMenuProps['panels'], undefined>>(() => {
return [
{
id: 'attachMainPanel',
Expand Down Expand Up @@ -205,26 +205,33 @@ export const AttachmentsMenu = ({
]);

return (
<EuiFlexItem>
<EuiPopover
button={
<EuiButtonIcon
data-test-subj="aiopsLogPatternAnalysisAttachmentsMenuButton"
aria-label={i18n.translate('xpack.aiops.logCategorization.attachmentsMenuAriaLabel', {
defaultMessage: 'Attachments',
})}
iconType="boxesHorizontal"
color="text"
onClick={() => setIsActionMenuOpen(!isActionMenuOpen)}
/>
}
isOpen={isActionMenuOpen}
closePopover={() => setIsActionMenuOpen(false)}
panelPaddingSize="none"
anchorPosition="downRight"
>
<EuiContextMenu panels={panels} initialPanelId="attachMainPanel" />
</EuiPopover>
<>
{!!panels[0]?.items?.length && (
<EuiFlexItem>
<EuiPopover
button={
<EuiButtonIcon
data-test-subj="aiopsLogPatternAnalysisAttachmentsMenuButton"
aria-label={i18n.translate(
'xpack.aiops.logCategorization.attachmentsMenuAriaLabel',
{
defaultMessage: 'Attachments',
}
)}
iconType="boxesHorizontal"
color="text"
onClick={() => setIsActionMenuOpen(!isActionMenuOpen)}
/>
}
isOpen={isActionMenuOpen}
closePopover={() => setIsActionMenuOpen(false)}
panelPaddingSize="none"
anchorPosition="downRight"
>
<EuiContextMenu panels={panels} initialPanelId="attachMainPanel" />
</EuiPopover>
</EuiFlexItem>
)}
{dashboardAttachmentReady ? (
<SavedObjectSaveModalDashboard
canSaveByReference={false}
Expand All @@ -238,6 +245,6 @@ export const AttachmentsMenu = ({
onSave={onSave}
/>
) : null}
</EuiFlexItem>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* 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 { SaveModalDashboardProps } from '@kbn/presentation-util-plugin/public';
import { LazySavedObjectSaveModalDashboard } from '@kbn/presentation-util-plugin/public';
import { withSuspense } from '@kbn/shared-ux-utility';
import React, { useState, useCallback, useMemo } from 'react';
import { useTimeRangeUpdates } from '@kbn/ml-date-picker';
import { EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE } from '@kbn/aiops-log-rate-analysis/constants';
import { FormattedMessage } from '@kbn/i18n-react';
import { i18n } from '@kbn/i18n';
import type { EuiContextMenuProps } from '@elastic/eui';
import {
EuiButton,
EuiButtonIcon,
EuiContextMenu,
EuiFlexItem,
EuiForm,
EuiFormRow,
EuiPanel,
EuiPopover,
EuiSpacer,
EuiSwitch,
} from '@elastic/eui';
import { useDataSource } from '../../../hooks/use_data_source';
import type { LogRateAnalysisEmbeddableState } from '../../../embeddables/log_rate_analysis/types';
import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context';

const SavedObjectSaveModalDashboard = withSuspense(LazySavedObjectSaveModalDashboard);

export const LogRateAnalysisAttachmentsMenu = () => {
const {
application: { capabilities },
embeddable,
} = useAiopsAppContext();
const { dataView } = useDataSource();

const [applyTimeRange, setApplyTimeRange] = useState(false);
const [isActionMenuOpen, setIsActionMenuOpen] = useState(false);
const [dashboardAttachmentReady, setDashboardAttachmentReady] = useState(false);

const timeRange = useTimeRangeUpdates();

const canEditDashboards = capabilities.dashboard.createNew;

const onSave: SaveModalDashboardProps['onSave'] = useCallback(
({ dashboardId, newTitle, newDescription }) => {
const stateTransfer = embeddable!.getStateTransfer();

const embeddableInput: Partial<LogRateAnalysisEmbeddableState> = {
title: newTitle,
description: newDescription,
dataViewId: dataView.id,
hidePanelTitles: false,
...(applyTimeRange && { timeRange }),
};

const state = {
input: embeddableInput,
type: EMBEDDABLE_LOG_RATE_ANALYSIS_TYPE,
};

const path = dashboardId === 'new' ? '#/create' : `#/view/${dashboardId}`;

stateTransfer.navigateToWithEmbeddablePackage('dashboards', { state, path });
},
[dataView.id, embeddable, applyTimeRange, timeRange]
);

const panels = useMemo<Exclude<EuiContextMenuProps['panels'], undefined>>(() => {
return [
{
id: 'attachMainPanel',
size: 's',
items: [
...(canEditDashboards
? [
{
name: i18n.translate('xpack.aiops.logRateAnalysis.addToDashboardTitle', {
defaultMessage: 'Add to dashboard',
}),
panel: 'attachToDashboardPanel',
'data-test-subj': 'aiopsLogRateAnalysisAttachToDashboardButton',
},
]
: []),
],
},
{
id: 'attachToDashboardPanel',
size: 's',
title: i18n.translate('xpack.aiops.logRateAnalysis.attachToDashboardTitle', {
defaultMessage: 'Add to dashboard',
}),
content: (
<EuiPanel paddingSize="s">
<EuiSpacer size="s" />
<EuiForm>
<EuiFormRow>
<EuiSwitch
label={i18n.translate('xpack.aiops.logRateAnalysis.applyTimeRangeLabel', {
defaultMessage: 'Apply time range',
})}
checked={applyTimeRange}
onChange={(e) => setApplyTimeRange(e.target.checked)}
/>
</EuiFormRow>
<EuiSpacer size="s" />
<EuiButton
size="s"
data-test-subj="aiopsLogRateAnalysisAttachToDashboardSubmitButton"
fill
fullWidth
type={'submit'}
onClick={() => {
setIsActionMenuOpen(false);
setDashboardAttachmentReady(true);
}}
>
<FormattedMessage
id="xpack.aiops.logRateAnalysis.attachToDashboardSubmitButtonLabel"
defaultMessage="Add to dashboard"
/>
</EuiButton>
</EuiForm>
</EuiPanel>
),
},
];
}, [canEditDashboards, applyTimeRange]);

return (
<>
{!!panels[0]?.items?.length && (
<EuiFlexItem>
<EuiPopover
button={
<EuiButtonIcon
data-test-subj="aiopsLogRateAnalysisAttachmentsMenuButton"
aria-label={i18n.translate('xpack.aiops.logRateAnalysis.attachmentsMenuAriaLabel', {
defaultMessage: 'Attachments',
})}
iconType="boxesHorizontal"
color="text"
onClick={() => setIsActionMenuOpen(!isActionMenuOpen)}
/>
}
isOpen={isActionMenuOpen}
closePopover={() => setIsActionMenuOpen(false)}
panelPaddingSize="none"
anchorPosition="downRight"
>
<EuiContextMenu panels={panels} initialPanelId="attachMainPanel" />
</EuiPopover>
</EuiFlexItem>
)}
{dashboardAttachmentReady ? (
<SavedObjectSaveModalDashboard
canSaveByReference={false}
objectType={i18n.translate('xpack.aiops.logRateAnalysis.objectTypeLabel', {
defaultMessage: 'Log rate analysis',
})}
documentInfo={{
title: 'Log rate analysis',
}}
onClose={() => setDashboardAttachmentReady(false)}
onSave={onSave}
/>
) : null}
</>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
type LogRateAnalysisResultsData,
} from '../log_rate_analysis_results';
import { useAiopsAppContext } from '../../../hooks/use_aiops_app_context';
import { LogRateAnalysisAttachmentsMenu } from './log_rate_analysis_attachments_menu';

export const DEFAULT_SEARCH_QUERY: estypes.QueryDslQueryContainer = { match_all: {} };
const DEFAULT_SEARCH_BAR_QUERY: estypes.QueryDslQueryContainer = {
Expand Down Expand Up @@ -216,6 +217,7 @@ export const LogRateAnalysisContent: FC<LogRateAnalysisContentProps> = ({
barColorOverride={barColorOverride}
barHighlightColorOverride={barHighlightColorOverride}
barStyleAccessor={barStyleAccessor}
attachmentsMenu={<LogRateAnalysisAttachmentsMenu />}
/>
)}
<EuiSpacer size="m" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export const LogRateAnalysisPage: FC = () => {
'uiSettings',
'unifiedSearch',
'observabilityAIAssistant',
'embeddable',
]),
}}
/>
Expand Down
18 changes: 18 additions & 0 deletions x-pack/test/functional/apps/aiops/log_rate_analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,24 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) {
await elasticChart.setNewChartUiDebugFlag(true);
});

it(`${testData.suiteTitle} attaches log rate analysis to a dashboard`, async () => {
await aiops.logRateAnalysisPage.navigateToDataViewSelection();

await ml.testExecution.logTestStep(
`${testData.suiteTitle} loads the log rate analysis page with selected data source`
);
await ml.jobSourceSelection.selectSourceForLogRateAnalysis(
testData.sourceIndexOrSavedSearch
);

await ml.testExecution.logTestStep(
`${testData.suiteTitle} starting dashboard attachment process`
);
await aiops.logRateAnalysisPage.attachToDashboard();

await ml.navigation.navigateToMl();
});

runTests(testData);
});
}
Expand Down
48 changes: 48 additions & 0 deletions x-pack/test/functional/services/aiops/log_rate_analysis_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr
const testSubjects = getService('testSubjects');
const retry = getService('retry');
const header = getPageObject('header');
const dashboardPage = getPageObject('dashboard');

return {
async assertTimeRangeSelectorSectionExists() {
Expand Down Expand Up @@ -387,5 +388,52 @@ export function LogRateAnalysisPageProvider({ getService, getPageObject }: FtrPr
{ location: handle, offset: { x: dragAndDropOffsetPx, y: 0 } }
);
},

async openAttachmentsMenu() {
await testSubjects.click('aiopsLogRateAnalysisAttachmentsMenuButton');
},

async clickAttachToDashboard() {
await testSubjects.click('aiopsLogRateAnalysisAttachToDashboardButton');
},

async confirmAttachToDashboard() {
await testSubjects.click('aiopsLogRateAnalysisAttachToDashboardSubmitButton');
},

async completeSaveToDashboardForm(createNew?: boolean) {
const dashboardSelector = await testSubjects.find('add-to-dashboard-options');
if (createNew) {
const label = await dashboardSelector.findByCssSelector(
`label[for="new-dashboard-option"]`
);
await label.click();
}

await testSubjects.click('confirmSaveSavedObjectButton');
await retry.waitForWithTimeout('Save modal to disappear', 1000, () =>
testSubjects
.missingOrFail('confirmSaveSavedObjectButton')
.then(() => true)
.catch(() => false)
);

// make sure the dashboard page actually loaded
const dashboardItemCount = await dashboardPage.getSharedItemsCount();
expect(dashboardItemCount).to.not.eql(undefined);

const embeddable = await testSubjects.find('aiopsEmbeddableLogRateAnalysis', 30 * 1000);
expect(await embeddable.isDisplayed()).to.eql(
true,
'Log rate analysis chart should be displayed in dashboard'
);
},

async attachToDashboard() {
await this.openAttachmentsMenu();
await this.clickAttachToDashboard();
await this.confirmAttachToDashboard();
await this.completeSaveToDashboardForm(true);
},
};
}

0 comments on commit 50a2626

Please sign in to comment.