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

[SecuritySolution] Setup dashboard view page #153040

Merged
merged 34 commits into from
Apr 11, 2023
Merged
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
1cc4ff5
init dashboards
angorayc Mar 20, 2023
8b1e905
rm unused types
angorayc Mar 9, 2023
ec52aa2
unit tests
angorayc Mar 10, 2023
3c78f16
rm unused i18n keys
angorayc Mar 10, 2023
6ccc33f
show dashboard name in breadcrumbs
angorayc Mar 10, 2023
53e2aa0
search bar
angorayc Mar 20, 2023
4ac3d81
add page title
angorayc Mar 22, 2023
adb3e98
fix url state
angorayc Mar 22, 2023
82a67cc
Unified Search Integration
angorayc Mar 22, 2023
65b564e
sync url state
angorayc Mar 22, 2023
3a22755
update query and filter
angorayc Mar 22, 2023
7aaf4c7
bread crumb
angorayc Mar 23, 2023
f575159
edit dashboard in dashboard app
angorayc Mar 24, 2023
a13fbaf
fix update timerange by brushing on charts
angorayc Mar 24, 2023
fb6a2db
edit button
angorayc Mar 24, 2023
09e38f3
unit tests
angorayc Mar 24, 2023
1047fd5
types
angorayc Mar 24, 2023
658cb9c
update reducer on query changed
angorayc Mar 27, 2023
42afcb0
pass onTimeRangeChange to search bar
angorayc Mar 27, 2023
910a843
update unit test
angorayc Mar 28, 2023
cd3c99d
remove configs for dashboard view
angorayc Mar 28, 2023
901660f
rename dashboardLanding to dashboards
angorayc Mar 28, 2023
e1f7f51
revert unused changes
angorayc Mar 28, 2023
ef8be93
add comments
angorayc Mar 29, 2023
362d97a
revert change
angorayc Mar 29, 2023
a5afb37
clean up
angorayc Mar 29, 2023
1d8f408
rm kbnUrlStateStorage
angorayc Mar 29, 2023
b7f15c4
Update src/plugins/unified_search/public/search_bar/search_bar.tsx
angorayc Mar 30, 2023
9be878c
[CI] Auto-commit changed files from 'node scripts/precommit_hook.js -…
kibanamachine Mar 30, 2023
4d2a572
add dashboard view links' entries
angorayc Apr 3, 2023
7c70a98
update edit link
angorayc Apr 4, 2023
7e48271
Merge branch 'main' of github.com:elastic/kibana into dashboard-setup
angorayc Apr 11, 2023
d92fd7f
review
angorayc Apr 11, 2023
54472ed
Merge branch 'dashboard-setup' of github.com:angorayc/kibana into das…
angorayc Apr 11, 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
15 changes: 15 additions & 0 deletions src/plugins/unified_search/public/search_bar/search_bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ export interface SearchBarOwnProps<QT extends AggregateQuery | Query = Query> {
onSaved?: (savedQuery: SavedQuery) => void;
// User has modified the saved query, your app should persist the update
onSavedQueryUpdated?: (savedQuery: SavedQuery) => void;
// Execute whenever time range is updated.
onTimeRangeChange?: (payload: { dateRange: TimeRange }) => void;
// User has cleared the active query, your app should clear the entire query bar
onClearSavedQuery?: () => void;

Expand Down Expand Up @@ -185,6 +187,19 @@ class SearchBarUI<QT extends (Query | AggregateQuery) | Query = Query> extends C
if (nextDateRange) {
nextState.dateRangeFrom = nextDateRange.dateRangeFrom;
nextState.dateRangeTo = nextDateRange.dateRangeTo;

/**
* Some applications do not rely on the _g url parameter to update the time. The onTimeRangeChange
* callback can be used in these cases to notify the consumer for the time change.
*/
if (nextDateRange.dateRangeFrom && nextDateRange.dateRangeTo) {
nextProps?.onTimeRangeChange?.({
dateRange: {
from: nextDateRange.dateRangeFrom,
to: nextDateRange.dateRangeTo,
},
});
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Adding this because the time range of the Visualization wouldn't update after brushing on a time-based histogram / area chart.

brushing_on_chart.mov

After:

Screen.Recording.2023-03-27.at.11.35.47.mov

return nextState;
}
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/security_solution/common/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export enum SecurityPageName {
* All cloud defend page names must match `CloudDefendPageId` in x-pack/plugins/cloud_defend/public/common/navigation/types.ts
*/
cloudDefendPolicies = 'cloud_defend-policies',
dashboardsLanding = 'dashboards',
dashboards = 'dashboards',
dataQuality = 'data_quality',
detections = 'detections',
detectionAndResponse = 'detection_response',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export const securitySolutionsDeepLinks: SecuritySolutionDeepLink[] = [
],
},
{
id: SecurityPageName.dashboardsLanding,
id: SecurityPageName.dashboards,
title: DASHBOARDS,
path: OVERVIEW_PATH,
navLinkStatus: AppNavLinkStatus.visible,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { TestProviders } from '../../mock';
import { DashboardRenderer } from './dashboard_renderer';

jest.mock('@kbn/dashboard-plugin/public', () => {
const actual = jest.requireActual('@kbn/dashboard-plugin/public');
return {
...actual,
LazyDashboardContainerRenderer: jest
.fn()
.mockImplementation(() => <div data-test-subj="dashboardRenderer" />),
};
});

jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useParams: jest.fn().mockReturnValue({
detailName: '2d50f100-be6f-11ed-964a-ffa67304840e',
}),
};
});

describe('DashboardRenderer', () => {
const props = {
canReadDashboard: true,
id: 'dashboard-savedObjectId',
savedObjectId: 'savedObjectId',
timeRange: {
from: '2023-03-10T00:00:00.000Z',
to: '2023-03-10T23:59:59.999Z',
},
};

it('renders', () => {
const { queryByTestId } = render(<DashboardRenderer {...props} />, { wrapper: TestProviders });
expect(queryByTestId(`dashboardRenderer`)).toBeInTheDocument();
});

it('does not render when No Read Permission', () => {
const testProps = {
...props,
canReadDashboard: false,
};
const { queryByTestId } = render(<DashboardRenderer {...testProps} />, {
wrapper: TestProviders,
});
expect(queryByTestId(`dashboardRenderer`)).not.toBeInTheDocument();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import React, { useCallback, useEffect, useState } from 'react';
import type { DashboardContainer } from '@kbn/dashboard-plugin/public';
import { LazyDashboardContainerRenderer } from '@kbn/dashboard-plugin/public';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { Filter, Query } from '@kbn/es-query';

import { useDispatch } from 'react-redux';
import { InputsModelId } from '../../store/inputs/constants';
import { inputsActions } from '../../store/inputs';

const DashboardRendererComponent = ({
canReadDashboard,
filters,
id,
inputId = InputsModelId.global,
onDashboardContainerLoaded,
query,
savedObjectId,
timeRange,
}: {
canReadDashboard: boolean;
filters?: Filter[];
id: string;
inputId?: InputsModelId.global | InputsModelId.timeline;
onDashboardContainerLoaded?: (dashboardContainer: DashboardContainer) => void;
query?: Query;
savedObjectId: string | undefined;
timeRange: {
from: string;
fromStr?: string | undefined;
to: string;
toStr?: string | undefined;
};
}) => {
const dispatch = useDispatch();
const [dashboardContainer, setDashboardContainer] = useState<DashboardContainer>();

const getCreationOptions = useCallback(
() =>
Promise.resolve({
overrideInput: { timeRange, viewMode: ViewMode.VIEW, query, filters },
}),
[filters, query, timeRange]
);

const refetchByForceRefresh = useCallback(() => {
dashboardContainer?.forceRefresh();
}, [dashboardContainer]);

useEffect(() => {
dispatch(
inputsActions.setQuery({
inputId,
id,
refetch: refetchByForceRefresh,
loading: false,
inspect: null,
})
);
return () => {
dispatch(inputsActions.deleteOneQuery({ inputId, id }));
};
}, [dispatch, id, inputId, refetchByForceRefresh]);

useEffect(() => {
dashboardContainer?.updateInput({ timeRange, query, filters });
}, [dashboardContainer, filters, query, timeRange]);

const handleDashboardLoaded = useCallback(
(container: DashboardContainer) => {
setDashboardContainer(container);
onDashboardContainerLoaded?.(container);
},
[onDashboardContainerLoaded]
);
return savedObjectId && canReadDashboard ? (
<LazyDashboardContainerRenderer
savedObjectId={savedObjectId}
getCreationOptions={getCreationOptions}
onDashboardContainerLoaded={handleDashboardLoaded}
/>
) : null;
};
DashboardRendererComponent.displayName = 'DashboardRendererComponent';
export const DashboardRenderer = React.memo(DashboardRendererComponent);
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { getTrailingBreadcrumbs as getCSPBreadcrumbs } from '../../../../cloud_s
import { getTrailingBreadcrumbs as getUsersBreadcrumbs } from '../../../../explore/users/pages/details/utils';
import { getTrailingBreadcrumbs as getKubernetesBreadcrumbs } from '../../../../kubernetes/pages/utils/breadcrumbs';
import { getTrailingBreadcrumbs as getAlertDetailBreadcrumbs } from '../../../../detections/pages/alert_details/utils/breadcrumbs';
import { getTrailingBreadcrumbs as getDashboardBreadcrumbs } from '../../../../dashboards/pages/utils';
import { SecurityPageName } from '../../../../app/types';
import type { RouteSpyState } from '../../../utils/route/types';
import { timelineActions } from '../../../../timelines/store/timeline';
Expand Down Expand Up @@ -134,6 +135,8 @@ const getTrailingBreadcrumbsForRoutes = (
return getAlertDetailBreadcrumbs(spyState, getSecuritySolutionUrl);
case SecurityPageName.cloudSecurityPostureBenchmarks:
return getCSPBreadcrumbs(spyState, getSecuritySolutionUrl);
case SecurityPageName.dashboards:
return getDashboardBreadcrumbs(spyState);
}

return [];
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,23 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(
}, []);

const indexPatterns = useMemo(() => [indexPattern], [indexPattern]);

const onTimeRangeChange = useCallback(
({ query, dateRange }) => {
const isQuickSelection = dateRange.from.includes('now') || dateRange.to.includes('now');
updateSearch({
end: dateRange.to,
filterManager,
id,
isInvalid: false,
isQuickSelection,
query,
setTablesActivePageToZero,
start: dateRange.from,
updateTime: true,
});
},
[filterManager, id, setTablesActivePageToZero, updateSearch]
);
return (
<div data-test-subj={`${id}DatePicker`}>
<SearchBar
Expand All @@ -307,6 +323,7 @@ export const SearchBarComponent = memo<SiemSearchBarProps & PropsFromRedux>(
onQuerySubmit={onQuerySubmit}
onRefresh={onRefresh}
onSaved={onSaved}
onTimeRangeChange={onTimeRangeChange}
onSavedQueryUpdated={onSavedQueryUpdated}
savedQuery={savedQuery}
showFilterBar={!hideFilterBar}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ import {
useSecurityDashboardsTableItems,
} from './use_security_dashboards_table';
import * as telemetry from '../../lib/telemetry';
import { SecurityPageName } from '../../../../common/constants';
import * as linkTo from '../../components/link_to';
import type { DashboardTableItem } from './types';

jest.mock('../../lib/kibana');

const spyUseGetSecuritySolutionUrl = jest.spyOn(linkTo, 'useGetSecuritySolutionUrl');
const spyTrack = jest.spyOn(telemetry, 'track');

const TAG_ID = 'securityTagId';
Expand Down Expand Up @@ -202,4 +206,20 @@ describe('Security Dashboards Table hooks', () => {
telemetry.TELEMETRY_EVENT.DASHBOARD
);
});

it('should land on SecuritySolution dashboard view page when dashboard title clicked', async () => {
const mockGetSecuritySolutionUrl = jest.fn();
spyUseGetSecuritySolutionUrl.mockImplementation(() => mockGetSecuritySolutionUrl);
const { result: itemsResult } = await renderUseSecurityDashboardsTableItems();
const { result: columnsResult } = renderUseDashboardsTableColumns();

render(<EuiBasicTable items={itemsResult.current.items} columns={columnsResult.current} />, {
wrapper: TestProviders,
});

expect(mockGetSecuritySolutionUrl).toHaveBeenCalledWith({
deepLinkId: SecurityPageName.dashboards,
path: 'dashboardId1',
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import { useKibana, useNavigateTo } from '../../lib/kibana';
import * as i18n from './translations';
import { useFetch, REQUEST_NAMES } from '../../hooks/use_fetch';
import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../lib/telemetry';
import { SecurityPageName } from '../../../../common/constants';
import { useGetSecuritySolutionUrl } from '../../components/link_to';

import type { DashboardTableItem } from './types';

const EMPTY_DESCRIPTION = '-' as const;
Expand Down Expand Up @@ -49,8 +52,9 @@ export const useSecurityDashboardsTableItems = () => {
export const useSecurityDashboardsTableColumns = (): Array<
EuiBasicTableColumn<DashboardTableItem>
> => {
const { savedObjectsTagging, dashboard } = useKibana().services;
const { savedObjectsTagging } = useKibana().services;
const { navigateTo } = useNavigateTo();
const getSecuritySolutionUrl = useGetSecuritySolutionUrl();

const getNavigationHandler = useCallback(
(href: string): MouseEventHandler =>
Expand All @@ -69,7 +73,10 @@ export const useSecurityDashboardsTableColumns = (): Array<
name: i18n.DASHBOARD_TITLE,
sortable: true,
render: (title: string, { id }) => {
const href = dashboard?.locator?.getRedirectUrl({ dashboardId: id });
const href = `${getSecuritySolutionUrl({
deepLinkId: SecurityPageName.dashboards,
path: id,
})}`;
return href ? (
<LinkAnchor href={href} onClick={getNavigationHandler(href)}>
{title}
Expand All @@ -90,7 +97,7 @@ export const useSecurityDashboardsTableColumns = (): Array<
// adds the tags table column based on the saved object items
...(savedObjectsTagging ? [savedObjectsTagging.ui.getTableColumnDefinition()] : []),
],
[getNavigationHandler, dashboard, savedObjectsTagging]
[savedObjectsTagging, getSecuritySolutionUrl, getNavigationHandler]
);

return columns;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,11 @@ import { links as detectionLinks } from '../../detections/links';
import { links as timelinesLinks } from '../../timelines/links';
import { getCasesLinkItems } from '../../cases/links';
import { links as managementLinks, getManagementFilteredLinks } from '../../management/links';
import { dashboardsLandingLinks, threatHuntingLandingLinks } from '../../landing_pages/links';
import { threatHuntingLandingLinks } from '../../landing_pages/links';
import { gettingStartedLinks } from '../../overview/links';
import { rootLinks as cloudSecurityPostureRootLinks } from '../../cloud_security_posture/links';
import type { StartPlugins } from '../../types';
import { dashboardsLandingLinks } from '../../dashboards/links';

const casesLinks = getCasesLinkItems();

Expand Down
Loading