Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Cases] Handle lens actions in Serverless #163581

Merged
merged 23 commits into from
Aug 14, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
8c2a2e2
hide solution picker for serverless
js-jankisalvi Aug 7, 2023
b803cd8
unit test added
js-jankisalvi Aug 8, 2023
00abb4a
Merge remote-tracking branch 'upstream/main' into hide-lens-actions-s…
js-jankisalvi Aug 8, 2023
967d6f0
update post case to have default owner in case of no owners available
js-jankisalvi Aug 8, 2023
ce954dc
Merge remote-tracking branch 'upstream/main' into hide-lens-actions-s…
js-jankisalvi Aug 9, 2023
fdbd3aa
Merge remote-tracking branch 'upstream/main' into hide-lens-actions-s…
js-jankisalvi Aug 10, 2023
5b18d96
add functional tests to add lens to case
js-jankisalvi Aug 10, 2023
de5e9b6
check that solution picker is not visible
js-jankisalvi Aug 10, 2023
36b78b2
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Aug 10, 2023
0d33bca
[CI] Auto-commit changed files from 'node scripts/eslint --no-cache -…
kibanamachine Aug 10, 2023
72ed819
fix serverless tests
js-jankisalvi Aug 10, 2023
9972f69
remove skiploading flag
js-jankisalvi Aug 10, 2023
6ec2f43
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Aug 10, 2023
e5393ab
removed unnecessary changes
js-jankisalvi Aug 10, 2023
521b5f1
add retry to handle loading indicator timeout
js-jankisalvi Aug 11, 2023
271f5d8
Merge branch 'main' into hide-lens-actions-serverless
js-jankisalvi Aug 11, 2023
a68df43
Merge remote-tracking branch 'upstream/main' into hide-lens-actions-s…
js-jankisalvi Aug 11, 2023
0b9f944
update config to interact with saved objects
js-jankisalvi Aug 11, 2023
83da2f6
Merge remote-tracking branch 'upstream/main' into hide-lens-actions-s…
js-jankisalvi Aug 14, 2023
fb9a42c
remove retry and use lens page object to add lens to dashboard
js-jankisalvi Aug 14, 2023
5bae788
add encryption key to shared config
js-jankisalvi Aug 14, 2023
20516f1
update cases helper api for serverless
js-jankisalvi Aug 14, 2023
437e01c
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Aug 14, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 17 additions & 5 deletions test/functional/page_objects/dashboard_page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,8 @@ export class DashboardPageObject extends FtrService {
*/
public async saveDashboard(
dashboardName: string,
saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true, exitFromEditMode: true }
saveOptions: SaveDashboardOptions = { waitDialogIsClosed: true, exitFromEditMode: true },
skipLoadingIndicatorHiddenCheck?: boolean,
) {
await this.retry.try(async () => {
await this.enterDashboardTitleAndClickSave(dashboardName, saveOptions);
Expand All @@ -468,14 +469,21 @@ export class DashboardPageObject extends FtrService {
await this.testSubjects.existOrFail('saveDashboardSuccess');
});
const message = await this.common.closeToast();
await this.header.waitUntilLoadingHasFinished();

if(!skipLoadingIndicatorHiddenCheck) {
await this.header.waitUntilLoadingHasFinished();
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
}

await this.common.waitForSaveModalToClose();

const isInViewMode = await this.testSubjects.exists('dashboardEditMode');
if (saveOptions.exitFromEditMode && !isInViewMode) {
await this.clickCancelOutOfEditMode();
}
await this.header.waitUntilLoadingHasFinished();

if(!skipLoadingIndicatorHiddenCheck) {
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
await this.header.waitUntilLoadingHasFinished();
}

return message;
}
Expand Down Expand Up @@ -557,15 +565,19 @@ export class DashboardPageObject extends FtrService {

// use the search filter box to narrow the results down to a single
// entry, or at least to a single page of results
public async loadSavedDashboard(dashboardName: string) {
public async loadSavedDashboard(dashboardName: string, skipLoadingIndicatorHiddenCheck?: boolean) {
this.log.debug(`Load Saved Dashboard ${dashboardName}`);

await this.gotoDashboardLandingPage();

await this.listingTable.searchForItemWithName(dashboardName);
await this.retry.try(async () => {
await this.listingTable.clickItemLink('dashboard', dashboardName);
await this.header.waitUntilLoadingHasFinished();

if(!skipLoadingIndicatorHiddenCheck) {
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
await this.header.waitUntilLoadingHasFinished();
}

// check Dashboard landing page is not present
await this.testSubjects.missingOrFail('dashboardLandingPage', { timeout: 10000 });
});
Expand Down
6 changes: 4 additions & 2 deletions test/functional/page_objects/time_picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class TimePickerPageObject extends FtrService {
* @param {String} toTime MMM D, YYYY @ HH:mm:ss.SSS
* @param {Boolean} force time picker force update, default is false
*/
public async setAbsoluteRange(fromTime: string, toTime: string, force = false) {
public async setAbsoluteRange(fromTime: string, toTime: string, force = false, skipLoadingIndicatorHiddenCheck = false) {
if (!force) {
const currentUrl = decodeURI(await this.browser.getCurrentUrl());
const DEFAULT_DATE_FORMAT = 'MMM D, YYYY @ HH:mm:ss.SSS';
Expand Down Expand Up @@ -192,7 +192,9 @@ export class TimePickerPageObject extends FtrService {
await this.testSubjects.click('querySubmitButton');
}

await this.header.awaitGlobalLoadingIndicatorHidden();
if(!skipLoadingIndicatorHiddenCheck) {
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
await this.header.awaitGlobalLoadingIndicatorHidden();
}
}

public async isOff() {
Expand Down
8 changes: 6 additions & 2 deletions test/functional/services/dashboard/add_panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,18 @@ export class DashboardAddPanelService extends FtrService {
await this.common.sleep(500);
}

async clickCreateNewLink() {
async clickCreateNewLink(skipLoadingIndicatorHiddenCheck?: boolean) {
this.log.debug('DashboardAddPanel.clickAddNewPanelButton');
await this.retry.try(async () => {
// prevent query bar auto suggest from blocking button
await this.browser.pressKeys(this.browser.keys.ESCAPE);
await this.testSubjects.click('dashboardAddNewPanelButton');
await this.testSubjects.waitForDeleted('dashboardAddNewPanelButton');
await this.header.waitUntilLoadingHasFinished();

if(!skipLoadingIndicatorHiddenCheck) {
js-jankisalvi marked this conversation as resolved.
Show resolved Hide resolved
await this.header.waitUntilLoadingHasFinished();
}

await this.testSubjects.existOrFail('lnsApp', {
timeout: 5000,
});
Expand Down
19 changes: 16 additions & 3 deletions x-pack/plugins/cases/public/components/create/form.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,18 @@ import { useCaseConfigureResponse } from '../configure_cases/__mock__';
import { TestProviders } from '../../common/mock';
import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors';
import { useGetTags } from '../../containers/use_get_tags';
import { useAvailableCasesOwners } from '../app/use_available_owners';

jest.mock('../../containers/use_get_tags');
jest.mock('../../containers/configure/use_get_supported_action_connectors');
jest.mock('../../containers/configure/use_configure');
jest.mock('../markdown_editor/plugins/lens/use_lens_draft_comment');
jest.mock('../app/use_available_owners', () => ({
useAvailableCasesOwners: () => ['securitySolution', 'observability'],
}));
jest.mock('../app/use_available_owners');

const useGetTagsMock = useGetTags as jest.Mock;
const useGetConnectorsMock = useGetSupportedActionConnectors as jest.Mock;
const useCaseConfigureMock = useCaseConfigure as jest.Mock;
const useAvailableOwnersMock = useAvailableCasesOwners as jest.Mock;

const initialCaseValue: FormProps = {
description: '',
Expand Down Expand Up @@ -77,6 +77,7 @@ describe('CreateCaseForm', () => {

beforeEach(() => {
jest.clearAllMocks();
useAvailableOwnersMock.mockReturnValue(['securitySolution', 'observability']);
useGetTagsMock.mockReturnValue({ data: ['test'] });
useGetConnectorsMock.mockReturnValue({ isLoading: false, data: connectorsMock });
useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse);
Expand Down Expand Up @@ -138,6 +139,18 @@ describe('CreateCaseForm', () => {
expect(wrapper.find(`[data-test-subj="caseOwnerSelector"]`).exists()).toBeTruthy();
});

it('does not render solution picker when only one owner is available', async () => {
useAvailableOwnersMock.mockReturnValue(['securitySolution']);

const wrapper = mount(
<MockHookWrapperComponent>
<CreateCaseForm {...casesFormProps} />
</MockHookWrapperComponent>
);

expect(wrapper.find(`[data-test-subj="caseOwnerSelector"]`).exists()).toBeFalsy();
});

it('hides the sync alerts toggle', () => {
const { queryByText } = render(
<MockHookWrapperComponent testProviderProps={{ features: { alerts: { sync: false } } }}>
Expand Down
3 changes: 1 addition & 2 deletions x-pack/plugins/cases/public/components/create/form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,8 @@ export const CreateCaseFormFields: React.FC<CreateCaseFormFieldsProps> = React.m
({ connectors, isLoadingConnectors, withSteps, owner, draftStorageKey }) => {
const { isSubmitting } = useFormContext();
const { isSyncAlertsEnabled, caseAssignmentAuthorized } = useCasesFeatures();

const availableOwners = useAvailableCasesOwners();
const canShowCaseSolutionSelection = !owner.length && availableOwners.length;
const canShowCaseSolutionSelection = !owner.length && availableOwners.length > 1;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will slightly change the behavior of the form when going through Analytics in Classic Kibana.

There there is no owner and the availableOwners can be 1(depending on the role's permissions). This will make it not show the owner picker(your PR) vs showing the owner picker with all options disabled except one(main).

It is a specific scenario so maybe it is not a problem.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm..The only way to handle this could be to add another config variable which tells weather it is serverless or classic kibana and then put the check accordingly.

However I think it's okay to not show owner selector picker when only one owner because we don't show it anyway when it's a security solution dashboard and user has permission for cases in security in classic kibana.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah but there we don't show because we are in Security Solution. The owner is defined and it is safe to assume the created case will end up inside Security Solution.

In the scenario I mentioned, we start from Analytics. The user has no indication of where the case will end up.

(Although he can assume if he only has access to like, Observability 😄 )

I think this is minor though. Maybe double-check with @mdefazio ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm good with hiding this if there is only one solution available. Thanks for double checking on this. @shanisagiv1 Let me know if you're ok with hiding this solution selector (and if you would like some more context to this, happy to zoom 😉 )


const firstStep = useMemo(
() => ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
getConnectorsFormDeserializer,
getConnectorsFormSerializer,
} from '../utils';
import { useAvailableCasesOwners } from '../app/use_available_owners';
import type { CaseAttachmentsWithoutOwner } from '../../types';
import { useGetSupportedActionConnectors } from '../../containers/configure/use_get_supported_action_connectors';
import { useCreateCaseWithAttachmentsTransaction } from '../../common/apm/use_cases_transactions';
Expand Down Expand Up @@ -68,6 +69,7 @@ export const FormContext: React.FC<Props> = ({
const { mutateAsync: createAttachments } = useCreateAttachments();
const { mutateAsync: pushCaseToExternalService } = usePostPushToService();
const { startTransaction } = useCreateCaseWithAttachmentsTransaction();
const availableOwners = useAvailableCasesOwners();

const submitCase = useCallback(
async (
Expand All @@ -82,6 +84,7 @@ export const FormContext: React.FC<Props> = ({
if (isValid) {
const { selectedOwner, ...userFormData } = dataWithoutConnectorId;
const caseConnector = getConnectorById(dataConnectorId, connectors);
const defaultOwner = owner[0] ?? availableOwners[0];

startTransaction({ appId, attachments });

Expand All @@ -94,7 +97,7 @@ export const FormContext: React.FC<Props> = ({
...userFormData,
connector: connectorToUpdate,
settings: { syncAlerts },
owner: selectedOwner ?? owner[0],
owner: selectedOwner ?? defaultOwner,
},
});

Expand Down Expand Up @@ -131,6 +134,7 @@ export const FormContext: React.FC<Props> = ({
attachments,
postCase,
owner,
availableOwners,
afterCaseCreated,
onSuccess,
createAttachments,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/*
* 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 { expect } from 'expect';
import { v4 as uuidv4 } from 'uuid';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { createCase } from './helper/api';
import { ConnectorTypes } from '@kbn/cases-plugin/common/types/domain';

export default ({ getPageObject, getService }: FtrProviderContext) => {
const common = getPageObject('common');
const dashboard = getPageObject('dashboard');
const lens = getPageObject('lens');
const timePicker = getPageObject('timePicker');
const svlCommonNavigation = getPageObject('svlCommonNavigation');
const svlObltNavigation = getService('svlObltNavigation');
const testSubjects = getService('testSubjects');
const esArchiver = getService('esArchiver');
const kibanaServer = getService('kibanaServer');
const dashboardAddPanel = getService('dashboardAddPanel');
const cases = getService('cases');
const find = getService('find');
const listingTable = getService('listingTable');
const supertest = getService('supertest');

// duplicated from x-pack/test/functional/page_objects/lens_page.ts to convert args into object for better readability
const goToTimeRange = async ({ fromTime, toTime, skipLoadingIndicatorHiddenCheck }: { fromTime?: string, toTime?: string, skipLoadingIndicatorHiddenCheck?: boolean }) => {
await timePicker.ensureHiddenNoDataPopover();
fromTime = fromTime || timePicker.defaultStartTime;
toTime = toTime || timePicker.defaultEndTime;
await timePicker.setAbsoluteRange(fromTime, toTime, false, skipLoadingIndicatorHiddenCheck);
await common.sleep(500);
};

describe('persistable attachment', () => {
describe('lens visualization', () => {
const skipLoadingIndicatorHiddenCheck = true;
const myDashboardName = `My-dashboard-${uuidv4()}`;

before(async () => {
await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/logstash_functional');
await kibanaServer.importExport.load(
'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json'
);

await svlObltNavigation.navigateToLandingPage();

await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' });

await dashboard.clickNewDashboard();
adcoelho marked this conversation as resolved.
Show resolved Hide resolved

await dashboardAddPanel.clickCreateNewLink(skipLoadingIndicatorHiddenCheck);

await goToTimeRange({ skipLoadingIndicatorHiddenCheck });

await lens.configureDimension({
dimension: 'lnsXY_xDimensionPanel > lns-empty-dimension',
operation: 'date_histogram',
field: '@timestamp',
});

await lens.configureDimension({
dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension',
operation: 'average',
field: 'bytes',
});

await lens.configureDimension({
dimension: 'lnsXY_splitDimensionPanel > lns-empty-dimension',
operation: 'terms',
field: 'ip',
});

await lens.saveAndReturn();
await dashboard.waitForRenderComplete();
await dashboard.saveDashboard(myDashboardName, {}, skipLoadingIndicatorHiddenCheck);
});

after(async () => {
await cases.api.deleteAllCases();

await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' });

await testSubjects.click('~breadcrumb-deepLinkId-dashboards');

await listingTable.checkListingSelectAllCheckbox();
await listingTable.clickDeleteSelected();

await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional');
await kibanaServer.importExport.unload(
'x-pack/test/functional/fixtures/kbn_archiver/lens/lens_basic.json'
);
});

it('adds lens visualization to a new case', async () => {
const caseTitle = 'case created in observability from my dashboard with lens visualization';

await testSubjects.click('embeddablePanelToggleMenuIcon');
await testSubjects.click('embeddablePanelMore-mainMenu');
await testSubjects.click('embeddablePanelAction-embeddable_addToNewCase');

await cases.create.createCase({
title: caseTitle,
description: 'test description',
});

// verify that solution picker is not visible
await testSubjects.missingOrFail('caseOwnerSelector');

await testSubjects.click('create-case-submit');

await cases.common.expectToasterToContain(`${caseTitle} has been updated`);

await testSubjects.click('toaster-content-case-view-link');

const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]');
expect(await title.getVisibleText()).toEqual(caseTitle);

await testSubjects.existOrFail('comment-persistableState-.lens');
});

it('adds lens visualization to an existing case from dashboard', async () => {
const theCaseTitle = 'case already exists in observability!!';
const postCaseReq = {
description: 'This is a test case to verify existing action scenario!!',
title: theCaseTitle,
tags: ['defacement'],
connector: {
id: 'none',
name: 'none',
type: ConnectorTypes.none,
fields: null,
},
settings: {
syncAlerts: true,
},
owner: 'observability',
assignees: [],
}
const theCase = await createCase(supertest, postCaseReq);

await svlCommonNavigation.sidenav.clickLink({ deepLinkId: 'dashboards' });

await testSubjects.click('embeddablePanelToggleMenuIcon');
await testSubjects.click('embeddablePanelMore-mainMenu');
await testSubjects.click('embeddablePanelAction-embeddable_addToExistingCase');

// verify that solution filter is not visible
await testSubjects.missingOrFail('solution-filter-popover-button');

await testSubjects.click(`cases-table-row-select-${theCase.id}`);

await cases.common.expectToasterToContain(`${theCaseTitle} has been updated`);
await testSubjects.click('toaster-content-case-view-link');

const title = await find.byCssSelector('[data-test-subj="editable-title-header-value"]');
expect(await title.getVisibleText()).toEqual(theCaseTitle);

await testSubjects.existOrFail('comment-persistableState-.lens');
});
});
});
};
Loading