diff --git a/packages/content-management/table_list/src/components/table.tsx b/packages/content-management/table_list/src/components/table.tsx index 3214e7bf00a72..9104327c7f46a 100644 --- a/packages/content-management/table_list/src/components/table.tsx +++ b/packages/content-management/table_list/src/components/table.tsx @@ -28,7 +28,8 @@ import type { Props as TableListViewProps, UserContentCommonSchema, } from '../table_list_view'; -import type { TableItemsRowActions } from '../types'; +import type { TableItemsRowActions, TagReference } from '../types'; + import { TableSortSelect } from './table_sort_select'; import { TagFilterPanel } from './tag_filter_panel'; import { useTagFilterPanel } from './use_tag_filter_panel'; @@ -46,43 +47,45 @@ type TagManagementProps = Pick< >; interface Props extends State, TagManagementProps { + clearTagSelection: () => void; + deleteItems: TableListViewProps['deleteItems']; dispatch: Dispatch>; entityName: string; entityNamePlural: string; - isFetchingItems: boolean; - tableCaption: string; - tableColumns: Array>; hasUpdatedAtMetadata: boolean; - deleteItems: TableListViewProps['deleteItems']; - tableItemsRowActions: TableItemsRowActions; + isFetchingItems: boolean; onSortChange: (column: SortColumnField, direction: Direction) => void; onTableChange: (criteria: CriteriaWithPagination) => void; onTableSearchChange: (arg: { query: Query | null; queryText: string }) => void; - clearTagSelection: () => void; + tableCaption: string; + tableColumns: Array>; + tableItemsRowActions: TableItemsRowActions; + fixedTagReferences?: TagReference[] | null; } export function Table({ + addOrRemoveExcludeTagFilter, + addOrRemoveIncludeTagFilter, + clearTagSelection, + deleteItems, dispatch, - items, + entityName, + entityNamePlural, + hasUpdatedAtMetadata, isFetchingItems, + items, + onSortChange, + onTableChange, + onTableSearchChange, + pagination, searchQuery, selectedIds, - pagination, + tableCaption, tableColumns, + tableItemsRowActions, tableSort, - hasUpdatedAtMetadata, - entityName, - entityNamePlural, tagsToTableItemMap, - tableItemsRowActions, - deleteItems, - tableCaption, - onTableChange, - onTableSearchChange, - onSortChange, - addOrRemoveExcludeTagFilter, - addOrRemoveIncludeTagFilter, - clearTagSelection, + fixedTagReferences, }: Props) { const { getTagList } = useServices(); @@ -151,6 +154,7 @@ export function Table({ tagsToTableItemMap, addOrRemoveExcludeTagFilter, addOrRemoveIncludeTagFilter, + fixedTagReferences, }); const tableSortSelectFilter = useMemo(() => { @@ -172,6 +176,7 @@ export function Table({ return { type: 'custom_component', component: () => { + const disableActions = fixedTagReferences != null; return ( ({ onFilterButtonClick={onFilterButtonClick} onSelectChange={onSelectChange} clearTagSelection={clearTagSelection} + disableActions={disableActions} /> ); }, }; }, [ + fixedTagReferences, isPopoverOpen, isInUse, closePopover, diff --git a/packages/content-management/table_list/src/components/tag_filter_panel.tsx b/packages/content-management/table_list/src/components/tag_filter_panel.tsx index 03439f9dec161..8fec81aa61658 100644 --- a/packages/content-management/table_list/src/components/tag_filter_panel.tsx +++ b/packages/content-management/table_list/src/components/tag_filter_panel.tsx @@ -46,23 +46,25 @@ const saveBtnWrapperCSS = css` interface Props { clearTagSelection: () => void; closePopover: () => void; - isPopoverOpen: boolean; + disableActions?: boolean; isInUse: boolean; - options: TagOptionItem[]; - totalActiveFilters: number; + isPopoverOpen: boolean; onFilterButtonClick: () => void; onSelectChange: (updatedOptions: TagOptionItem[]) => void; + options: TagOptionItem[]; + totalActiveFilters: number; } export const TagFilterPanel: FC = ({ - isPopoverOpen, + clearTagSelection, + closePopover, + disableActions, isInUse, - options, - totalActiveFilters, + isPopoverOpen, onFilterButtonClick, onSelectChange, - closePopover, - clearTagSelection, + options, + totalActiveFilters, }) => { const { euiTheme } = useEuiTheme(); const { navigateToUrl, currentAppId$, getTagManagementUrl } = useServices(); @@ -122,18 +124,24 @@ export const TagFilterPanel: FC = ({ Tags - - {totalActiveFilters > 0 && ( - - {i18n.translate( - 'contentManagement.tableList.tagFilterPanel.clearSelectionButtonLabelLabel', - { - defaultMessage: 'Clear selection', - } - )} - - )} - + {!disableActions && ( + + {totalActiveFilters > 0 && ( + + {i18n.translate( + 'contentManagement.tableList.tagFilterPanel.clearSelectionButtonLabelLabel', + { + defaultMessage: 'Clear selection', + } + )} + + )} + + )} @@ -156,6 +164,7 @@ export const TagFilterPanel: FC = ({ ); }} + diff --git a/packages/content-management/table_list/src/components/use_tag_filter_panel.tsx b/packages/content-management/table_list/src/components/use_tag_filter_panel.tsx index ca7aab6f8bb08..fc4d6373e6033 100644 --- a/packages/content-management/table_list/src/components/use_tag_filter_panel.tsx +++ b/packages/content-management/table_list/src/components/use_tag_filter_panel.tsx @@ -5,12 +5,12 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useEffect, useState, useCallback } from 'react'; +import React, { useEffect, useState, useCallback, useMemo } from 'react'; import type { MouseEvent } from 'react'; import { Query, EuiFlexGroup, EuiFlexItem, EuiText, EuiHealth, EuiBadge } from '@elastic/eui'; import type { FieldValueOptionType } from '@elastic/eui'; -import type { Tag } from '../types'; +import type { Tag, TagReference } from '../types'; const isMac = navigator.platform.toLowerCase().indexOf('mac') >= 0; @@ -36,14 +36,25 @@ export interface Params { getTagList: () => Tag[]; addOrRemoveIncludeTagFilter: (tag: Tag) => void; addOrRemoveExcludeTagFilter: (tag: Tag) => void; + fixedTagReferences?: TagReference[] | null; } +const getTotalActiveFilters = ( + options: TagOptionItem[], + fixedTagReferences?: TagReference[] | null +) => + Math.max( + fixedTagReferences?.length ?? 0, + options.filter((option) => option.checked === 'on').length ?? 0 + ); + export const useTagFilterPanel = ({ query, tagsToTableItemMap, getTagList, addOrRemoveExcludeTagFilter, addOrRemoveIncludeTagFilter, + fixedTagReferences, }: Params) => { const [isPopoverOpen, setIsPopoverOpen] = useState(false); // When the panel is "in use" it means that it is opened and the user is interacting with it. @@ -53,21 +64,31 @@ export const useTagFilterPanel = ({ const [isInUse, setIsInUse] = useState(false); const [options, setOptions] = useState([]); const [tagSelection, setTagSelection] = useState({}); - const totalActiveFilters = Object.keys(tagSelection).length; + const totalActiveFilters = useMemo( + () => getTotalActiveFilters(options, fixedTagReferences), + [fixedTagReferences, options] + ); const onSelectChange = useCallback( (updatedOptions: TagOptionItem[]) => { // Note: see data flow comment in useEffect() below const diff = updatedOptions.find((item, index) => item.checked !== options[index].checked); + + if (fixedTagReferences?.find((ref) => ref.name === diff?.tag.name)) { + return; + } if (diff) { addOrRemoveIncludeTagFilter(diff.tag); } }, - [options, addOrRemoveIncludeTagFilter] + [fixedTagReferences, options, addOrRemoveIncludeTagFilter] ); const onOptionClick = useCallback( (tag: Tag) => (e: MouseEvent) => { + if (fixedTagReferences?.find((ref) => ref.name === tag.name)) { + return; + } const withModifierKey = (isMac && e.metaKey) || (!isMac && e.ctrlKey); if (withModifierKey) { @@ -76,7 +97,7 @@ export const useTagFilterPanel = ({ addOrRemoveIncludeTagFilter(tag); } }, - [addOrRemoveIncludeTagFilter, addOrRemoveExcludeTagFilter] + [addOrRemoveExcludeTagFilter, addOrRemoveIncludeTagFilter, fixedTagReferences] ); const updateTagList = useCallback(() => { @@ -118,7 +139,7 @@ export const useTagFilterPanel = ({ }); setOptions(tagsToSelectOptions); - }, [getTagList, tagsToTableItemMap, tagSelection, onOptionClick]); + }, [getTagList, tagSelection, onOptionClick, tagsToTableItemMap]); const onFilterButtonClick = useCallback(() => { setIsPopoverOpen((prev) => !prev); diff --git a/packages/content-management/table_list/src/table_list_view.test.tsx b/packages/content-management/table_list/src/table_list_view.test.tsx index 0245af450fb8a..a84b19d815ad0 100644 --- a/packages/content-management/table_list/src/table_list_view.test.tsx +++ b/packages/content-management/table_list/src/table_list_view.test.tsx @@ -610,7 +610,7 @@ describe('TableListView', () => { }, ]; - test('should have an "inpect" button if the content editor is enabled', async () => { + test('should have an "inspect" button if the content editor is enabled', async () => { let testBed: TestBed; await act(async () => { @@ -629,6 +629,63 @@ describe('TableListView', () => { }); }); + describe('render PageTemplateHeader', () => { + const hits: UserContentCommonSchema[] = [ + { + id: '123', + updatedAt: twoDaysAgo.toISOString(), + type: 'dashboard', + attributes: { + title: 'Item 1', + description: 'Item 1 description', + }, + references: [], + }, + { + id: '456', + // This is the latest updated and should come first in the table + updatedAt: yesterday.toISOString(), + type: 'dashboard', + attributes: { + title: 'Item 2', + description: 'Item 2 description', + }, + references: [], + }, + ]; + + test('should render PageTemplateHeader', async () => { + let testBed: TestBed; + + await act(async () => { + testBed = await setup({ + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits }), + }); + }); + + const { exists, component } = testBed!; + component.update(); + + expect(exists('top-nav')).toBeTruthy(); + }); + + test('should not render PageTemplateHeader if withPageTemplateHeader is false', async () => { + let testBed: TestBed; + + await act(async () => { + testBed = await setup({ + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits }), + withPageTemplateHeader: false, + }); + }); + + const { exists, component } = testBed!; + component.update(); + + expect(exists('top-nav')).toBeFalsy(); + }); + }); + describe('tag filtering', () => { const setupTagFiltering = registerTestBed( WithServices(TableListView, { @@ -671,6 +728,59 @@ describe('TableListView', () => { }, ]; + test('should render default query if fixedTagReferences is provided', async () => { + let testBed: TestBed; + + const findItems = jest.fn().mockResolvedValue({ total: hits.length, hits }); + + await act(async () => { + testBed = await setupTagFiltering({ + findItems, + fixedTagReferences: [ + { id: 'id-tag-1', name: 'tag-1', description: '', color: '', type: 'tag' }, + ], + }); + }); + + const { component, find } = testBed!; + component.update(); + + const getSearchBoxValue = () => find('tableListSearchBox').props().defaultValue; + + const expected = 'tag:(tag-1)'; + + expect(getSearchBoxValue()).toBe(expected); + }); + + test('should not able to remove the default selected tag if fixedTagReferences is provided', async () => { + let testBed: TestBed; + + const findItems = jest.fn().mockResolvedValue({ total: hits.length, hits }); + + await act(async () => { + testBed = await setupTagFiltering({ + findItems, + fixedTagReferences: [ + { id: 'id-tag-1', name: 'tag-1', description: '', color: '', type: 'tag' }, + ], + }); + }); + + const { component, find } = testBed!; + component.update(); + + await act(async () => { + find('tag-id-tag-1').simulate('click'); + }); + component.update(); + + const getSearchBoxValue = () => find('tableListSearchBox').props().defaultValue; + + const expected = 'tag:(tag-1)'; + + expect(getSearchBoxValue()).toBe(expected); + }); + test('should filter by tag from the table', async () => { let testBed: TestBed; diff --git a/packages/content-management/table_list/src/table_list_view.tsx b/packages/content-management/table_list/src/table_list_view.tsx index 2191a3c9b7eee..72017c33be9f3 100644 --- a/packages/content-management/table_list/src/table_list_view.tsx +++ b/packages/content-management/table_list/src/table_list_view.tsx @@ -20,6 +20,7 @@ import { CriteriaWithPagination, Query, Ast, + EuiPaddingSize, } from '@elastic/eui'; import { keyBy, uniq, get } from 'lodash'; import { i18n } from '@kbn/i18n'; @@ -28,7 +29,6 @@ import type { IHttpFetchError } from '@kbn/core-http-browser'; import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { useOpenContentEditor } from '@kbn/content-management-content-editor'; import type { OpenContentEditorParams } from '@kbn/content-management-content-editor'; - import { Table, ConfirmDeleteModal, @@ -42,7 +42,7 @@ import { getReducer } from './reducer'; import type { SortColumnField } from './components'; import { useTags } from './use_tags'; import { useInRouterContext, useUrlState } from './use_url_state'; -import { RowActions, TableItemsRowActions } from './types'; +import { RowActions, TableItemsRowActions, TagReference } from './types'; interface ContentEditorConfig extends Pick { @@ -103,7 +103,11 @@ export interface Props { @@ -177,7 +181,7 @@ const urlStateDeserializer = (params: URLQueryParams): URLState => { } }); - // For backward compability with the Dashboard app we will support both "s" and "title" passed + // For backward compatibility with the Dashboard app we will support both "s" and "title" passed // in the query params. We might want to stop supporting both in a future release (v9.0?) stateFromURL.s = sanitizedParams.s ?? sanitizedParams.title; @@ -235,31 +239,65 @@ const tableColumnMetadata = { }, } as const; +const appendQuery = (q: Query, tagName: string) => { + return q.addOrFieldValue('tag', tagName, true, 'eq'); +}; + +const getDefaultQuery = ( + initialQuery: string, + fixedTagReferences: TagReference[] | null | undefined +) => { + const query = new Query(Ast.create([]), undefined, initialQuery); + const uniqueQueryArray = fixedTagReferences?.reduce((acc, { name }) => { + if (name && acc.indexOf(name) === -1) { + acc.push(name); + } + return acc; + }, []); + return ( + uniqueQueryArray?.reduce((q, ref) => { + return appendQuery(q, ref); + }, query) ?? query + ); +}; + +const getFindItemReference = ( + references: SavedObjectsFindOptionsReference[] | undefined, + fixedTagReferences?: TagReference[] | null | undefined +): SavedObjectsFindOptionsReference[] | undefined => { + const fixedTagFindReferences = fixedTagReferences?.map(({ id, type }) => ({ id, type })) ?? []; + return [...(references ?? []), ...fixedTagFindReferences]; +}; + function TableListViewComp({ - tableListTitle, - tableListDescription, + additionalRightSideActions = [], + children, + contentEditor = { enabled: false }, + createItem, + customTableColumn, + deleteItems, + editItem, + emptyPrompt, entityName, entityNamePlural, - initialFilter: initialQuery, + findItems, + getDetailViewLink, headingId, + id: listingId = 'userContent', + initialFilter: initialQuery, initialPageSize, listingLimit, - urlStateEnabled = true, - customTableColumn, - emptyPrompt, - rowItemActions, - findItems, - createItem, - editItem, - deleteItems, - getDetailViewLink, onClickTitle, - id: listingId = 'userContent', - contentEditor = { enabled: false }, - children, + pageSectionPadding = 'm', + restrictPageSectionWidth = true, + rowItemActions, + tableListDescription, + tableListTitle, + fixedTagReferences, titleColumnName, - additionalRightSideActions = [], + urlStateEnabled = true, withoutPageTemplateWrapper, + withPageTemplateHeader = true, }: Props) { if (!getDetailViewLink && !onClickTitle) { throw new Error( @@ -323,7 +361,7 @@ function TableListViewComp({ selectedIds: [], searchQuery: initialQuery !== undefined - ? { text: initialQuery, query: new Query(Ast.create([]), undefined, initialQuery) } + ? { text: initialQuery, query: getDefaultQuery(initialQuery, fixedTagReferences) } : { text: '', query: new Query(Ast.create([]), undefined, '') }, pagination: { pageIndex: 0, @@ -336,7 +374,7 @@ function TableListViewComp({ direction: 'asc', }, }), - [initialPageSize, initialQuery] + [fixedTagReferences, initialPageSize, initialQuery] ); const [state, dispatch] = useReducer(reducer, initialState); @@ -374,9 +412,16 @@ function TableListViewComp({ referencesToExclude, } = searchQueryParser ? await searchQueryParser(searchQuery.text) - : { searchQuery: searchQuery.text, references: undefined, referencesToExclude: undefined }; - - const response = await findItems(searchQueryParsed, { references, referencesToExclude }); + : { + searchQuery: searchQuery.text, + references: undefined, + referencesToExclude: undefined, + }; + + const response = await findItems(searchQueryParsed, { + references: getFindItemReference(references, fixedTagReferences), + referencesToExclude, + }); if (!isMounted.current) { return; @@ -396,7 +441,7 @@ function TableListViewComp({ data: err, }); } - }, [searchQueryParser, findItems, searchQuery.text]); + }, [searchQueryParser, searchQuery.text, findItems, fixedTagReferences]); const updateQuery = useCallback( (query: Query) => { @@ -422,6 +467,7 @@ function TableListViewComp({ query: searchQuery.query, updateQuery, items, + fixedTagReferences, }); const inspectItem = useCallback( @@ -860,7 +906,7 @@ function TableListViewComp({ updateQueryFromURL(urlState.s); updateSortFromURL(urlState.sort); - }, [urlState, searchQueryParser, getTagList, urlStateEnabled]); + }, [urlState, searchQueryParser, urlStateEnabled, getTagList]); useEffect(() => { isMounted.current = true; @@ -911,16 +957,22 @@ function TableListViewComp({ return ( - {tableListTitle}} - description={tableListDescription} - rightSideItems={[ - renderCreateButton() ?? , - ...additionalRightSideActions?.slice(0, 2), - ]} - data-test-subj="top-nav" - /> - + {withPageTemplateHeader && ( + {tableListTitle}} + description={tableListDescription} + rightSideItems={[ + renderCreateButton() ?? , + ...additionalRightSideActions?.slice(0, 2), + ]} + data-test-subj="top-nav" + /> + )} + {/* Any children passed to the component */} {children} @@ -941,27 +993,28 @@ function TableListViewComp({ {/* Table of items */}
+ addOrRemoveExcludeTagFilter={addOrRemoveExcludeTagFilter} + addOrRemoveIncludeTagFilter={addOrRemoveIncludeTagFilter} + clearTagSelection={clearTagSelection} + deleteItems={deleteItems} dispatch={dispatch} - items={items} - isFetchingItems={isFetchingItems} - searchQuery={searchQuery} - tableColumns={tableColumns} + entityName={entityName} + entityNamePlural={entityNamePlural} + fixedTagReferences={fixedTagReferences} hasUpdatedAtMetadata={hasUpdatedAtMetadata} - tableSort={tableSort} + isFetchingItems={isFetchingItems} + items={items} + onSortChange={onSortChange} + onTableChange={onTableChange} + onTableSearchChange={onTableSearchChange} pagination={pagination} + searchQuery={searchQuery} selectedIds={selectedIds} - entityName={entityName} - entityNamePlural={entityNamePlural} - tagsToTableItemMap={tagsToTableItemMap} - deleteItems={deleteItems} tableCaption={tableListTitle} + tableColumns={tableColumns} tableItemsRowActions={tableItemsRowActions} - onTableChange={onTableChange} - onTableSearchChange={onTableSearchChange} - onSortChange={onSortChange} - addOrRemoveIncludeTagFilter={addOrRemoveIncludeTagFilter} - addOrRemoveExcludeTagFilter={addOrRemoveExcludeTagFilter} - clearTagSelection={clearTagSelection} + tableSort={tableSort} + tagsToTableItemMap={tagsToTableItemMap} /> {/* Delete modal */} diff --git a/packages/content-management/table_list/src/types.ts b/packages/content-management/table_list/src/types.ts index c8e734a289451..baad92ff009fb 100644 --- a/packages/content-management/table_list/src/types.ts +++ b/packages/content-management/table_list/src/types.ts @@ -13,6 +13,14 @@ export interface Tag { color: string; } +export interface TagReference { + id: string; + name: string; + description: string; + color: string; + type: string; +} + export type TableRowAction = 'delete'; export type RowActions = { diff --git a/packages/content-management/table_list/src/use_tags.ts b/packages/content-management/table_list/src/use_tags.ts index 345a3484306ff..913d49f71d863 100644 --- a/packages/content-management/table_list/src/use_tags.ts +++ b/packages/content-management/table_list/src/use_tags.ts @@ -8,7 +8,8 @@ import { useCallback, useMemo } from 'react'; import { Query } from '@elastic/eui'; -import type { Tag } from './types'; +import type { Tag, TagReference } from './types'; + import type { UserContentCommonSchema } from './table_list_view'; type QueryUpdater = (query: Query, tag: Tag) => Query; @@ -17,10 +18,12 @@ export function useTags({ query, updateQuery, items, + fixedTagReferences, }: { query: Query; updateQuery: (query: Query) => void; items: UserContentCommonSchema[]; + fixedTagReferences?: TagReference[] | null; }) { // Return a map of tag.id to an array of saved object ids having that tag // { 'abc-123': ['saved_object_id_1', 'saved_object_id_2', ...] } @@ -78,13 +81,23 @@ export function useTags({ ); const removeTagFromIncludeClause = useMemo( - () => updateTagClauseGetter((q, tag) => q.removeOrFieldValue('tag', tag.name)), - [updateTagClauseGetter] + () => + updateTagClauseGetter((q, tag) => + fixedTagReferences?.find((ref) => ref.name === tag.name) + ? q + : q.removeOrFieldValue('tag', tag.name) + ), + [fixedTagReferences, updateTagClauseGetter] ); const addTagToExcludeClause = useMemo( - () => updateTagClauseGetter((q, tag) => q.addOrFieldValue('tag', tag.name, false, 'eq')), - [updateTagClauseGetter] + () => + updateTagClauseGetter((q, tag) => + fixedTagReferences?.find((ref) => ref.name === tag.name) + ? q + : q.addOrFieldValue('tag', tag.name, false, 'eq') + ), + [fixedTagReferences, updateTagClauseGetter] ); const removeTagFromExcludeClause = useMemo( @@ -112,8 +125,8 @@ export function useTags({ [ hasTagInExclude, hasTagInInclude, - removeTagFromExcludeClause, addTagToIncludeClause, + removeTagFromExcludeClause, removeTagFromIncludeClause, ] ); @@ -138,8 +151,8 @@ export function useTags({ [ hasTagInInclude, hasTagInExclude, - removeTagFromIncludeClause, addTagToExcludeClause, + removeTagFromIncludeClause, removeTagFromExcludeClause, ] ); diff --git a/packages/kbn-test-jest-helpers/src/testbed/types.ts b/packages/kbn-test-jest-helpers/src/testbed/types.ts index c5d591b10c4b9..7e722223f4dc1 100644 --- a/packages/kbn-test-jest-helpers/src/testbed/types.ts +++ b/packages/kbn-test-jest-helpers/src/testbed/types.ts @@ -100,7 +100,7 @@ export interface TestBed { * Select or unselect a form checkbox. * * @param dataTestSubject The test subject of the checkbox (can be a nested path. e.g. "myForm.mySelect"). - * @param isChecked Defines if the checkobx is active or not + * @param isChecked Defines if the checkbox is active or not */ selectCheckBox: (checkboxTestSubject: T, isChecked?: boolean) => void; /** diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx index 383b51fbd65fa..419453d73ae7d 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.test.tsx @@ -66,6 +66,88 @@ test('initial filter is passed through', async () => { ); }); +test('fixedTagReferences is passed through', async () => { + pluginServices.getServices().dashboardCapabilities.showWriteControls = false; + + let component: ReactWrapper; + const fixedTagReferences = [ + { id: 'mockTagId', name: 'mockTagName', description: '', color: '', type: 'tag' }, + ]; + + await act(async () => { + ({ component } = mountWith({ props: { fixedTagReferences } })); + }); + component!.update(); + expect(TableListView).toHaveBeenCalledWith( + expect.objectContaining({ fixedTagReferences }), + expect.any(Object) // react context + ); +}); + +test('withPageTemplateHeader is passed through', async () => { + pluginServices.getServices().dashboardCapabilities.showWriteControls = false; + + let component: ReactWrapper; + const withPageTemplateHeader = false; + + await act(async () => { + ({ component } = mountWith({ props: { withPageTemplateHeader } })); + }); + component!.update(); + expect(TableListView).toHaveBeenCalledWith( + expect.objectContaining({ withPageTemplateHeader }), + expect.any(Object) // react context + ); +}); + +test('restrictPageSectionWidth is passed through', async () => { + pluginServices.getServices().dashboardCapabilities.showWriteControls = false; + + let component: ReactWrapper; + const restrictPageSectionWidth = false; + + await act(async () => { + ({ component } = mountWith({ props: { restrictPageSectionWidth } })); + }); + component!.update(); + expect(TableListView).toHaveBeenCalledWith( + expect.objectContaining({ restrictPageSectionWidth }), + expect.any(Object) // react context + ); +}); + +test('pageSectionPadding is passed through', async () => { + pluginServices.getServices().dashboardCapabilities.showWriteControls = false; + + let component: ReactWrapper; + const pageSectionPadding = 'none'; + + await act(async () => { + ({ component } = mountWith({ props: { pageSectionPadding } })); + }); + component!.update(); + expect(TableListView).toHaveBeenCalledWith( + expect.objectContaining({ pageSectionPadding }), + expect.any(Object) // react context + ); +}); + +test('urlStateEnabled is passed through', async () => { + pluginServices.getServices().dashboardCapabilities.showWriteControls = false; + + let component: ReactWrapper; + const urlStateEnabled = false; + + await act(async () => { + ({ component } = mountWith({ props: { urlStateEnabled } })); + }); + component!.update(); + expect(TableListView).toHaveBeenCalledWith( + expect.objectContaining({ urlStateEnabled }), + expect.any(Object) // react context + ); +}); + test('when showWriteControls is true, table list view is passed editing functions', async () => { pluginServices.getServices().dashboardCapabilities.showWriteControls = true; diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx index 8572356687fd6..c545ee159043b 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing.tsx @@ -20,6 +20,7 @@ import { reportPerformanceMetricEvent } from '@kbn/ebt-tools'; import { toMountPoint, useExecutionContext } from '@kbn/kibana-react-plugin/public'; import type { SavedObjectsFindOptionsReference, SimpleSavedObject } from '@kbn/core/public'; +import { EuiPaddingSize } from '@elastic/eui'; import { SAVED_OBJECT_DELETE_TIME, SAVED_OBJECT_LOADED_TIME, @@ -42,6 +43,14 @@ type TableListViewApplicationService = DashboardApplicationService & { capabilities: { advancedSettings: { save: boolean } }; }; +interface TagReference { + id: string; + type: string; + name: string; + color: string; + description: string; +} + const SAVED_OBJECTS_LIMIT_SETTING = 'savedObjects:listingLimit'; const SAVED_OBJECTS_PER_PAGE_SETTING = 'savedObjects:perPage'; @@ -71,18 +80,30 @@ const toTableListViewSavedObject = ( }; export type DashboardListingProps = PropsWithChildren<{ + disableCreateDashboardButton?: boolean; + getDashboardUrl: (dashboardId: string, usesTimeRestore: boolean) => string; + goToDashboard: (dashboardId?: string, viewMode?: ViewMode) => void; initialFilter?: string; + pageSectionPadding?: EuiPaddingSize; + restrictPageSectionWidth?: boolean; + fixedTagReferences?: TagReference[] | null; useSessionStorageIntegration?: boolean; - goToDashboard: (dashboardId?: string, viewMode?: ViewMode) => void; - getDashboardUrl: (dashboardId: string, usesTimeRestore: boolean) => string; + withPageTemplateHeader?: boolean; + urlStateEnabled?: boolean; }>; export const DashboardListing = ({ children, - initialFilter, - goToDashboard, + disableCreateDashboardButton, getDashboardUrl, + goToDashboard, + initialFilter, + pageSectionPadding = 'm', + restrictPageSectionWidth = true, + fixedTagReferences, useSessionStorageIntegration, + withPageTemplateHeader = true, + urlStateEnabled, }: DashboardListingProps) => { const { application, @@ -198,6 +219,7 @@ export const DashboardListing = ({ const emptyPrompt = ( <> {children} diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx index 886d43a1db6d9..2551779752274 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.test.tsx @@ -34,6 +34,7 @@ const makeDefaultProps = (): DashboardListingEmptyPromptProps => ({ goToDashboard: jest.fn(), setUnsavedDashboardIds: jest.fn(), useSessionStorageIntegration: true, + disableCreateDashboardButton: false, }); function mountWith({ @@ -75,6 +76,21 @@ test('renders empty prompt with link when showWriteControls is on', async () => expect(component!.find('EuiLink').length).toBe(1); }); +test('renders disabled action button when disableCreateDashboardButton is true', async () => { + pluginServices.getServices().dashboardCapabilities.showWriteControls = true; + + let component: ReactWrapper; + await act(async () => { + ({ component } = mountWith({ props: { disableCreateDashboardButton: true } })); + }); + + component!.update(); + + expect(component!.find(`[data-test-subj="newItemButton"]`).first().prop('disabled')).toEqual( + true + ); +}); + test('renders continue button when no dashboards exist but one is in progress', async () => { pluginServices.getServices().dashboardCapabilities.showWriteControls = true; let component: ReactWrapper; diff --git a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx index a518c520bcbd8..374b7593ee4ee 100644 --- a/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx +++ b/src/plugins/dashboard/public/dashboard_listing/dashboard_listing_empty_prompt.tsx @@ -29,18 +29,20 @@ import { DASHBOARD_PANELS_UNSAVED_ID } from '../services/dashboard_session_stora export interface DashboardListingEmptyPromptProps { createItem: () => void; - unsavedDashboardIds: string[]; + disableCreateDashboardButton?: boolean; goToDashboard: DashboardListingProps['goToDashboard']; setUnsavedDashboardIds: React.Dispatch>; + unsavedDashboardIds: string[]; useSessionStorageIntegration: DashboardListingProps['useSessionStorageIntegration']; } export const DashboardListingEmptyPrompt = ({ - useSessionStorageIntegration, + createItem, + disableCreateDashboardButton, + goToDashboard, setUnsavedDashboardIds, unsavedDashboardIds, - goToDashboard, - createItem, + useSessionStorageIntegration, }: DashboardListingEmptyPromptProps) => { const { application, @@ -56,7 +58,13 @@ export const DashboardListingEmptyPrompt = ({ const getEmptyAction = useCallback(() => { if (!isEditingFirstDashboard) { return ( - + {noItemsStrings.getCreateNewDashboardText()} ); @@ -94,11 +102,12 @@ export const DashboardListingEmptyPrompt = ({ ); }, [ + createItem, dashboardSessionStorage, + disableCreateDashboardButton, + goToDashboard, isEditingFirstDashboard, setUnsavedDashboardIds, - goToDashboard, - createItem, ]); if (!showWriteControls) { diff --git a/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts b/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts index 6f87ede894dd1..fa08f28035b56 100644 --- a/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts +++ b/x-pack/plugins/security_solution/public/common/containers/tags/__mocks__/api.ts @@ -6,13 +6,16 @@ */ export const MOCK_TAG_ID = 'securityTagId'; +export const MOCK_TAG_NAME = 'test tag'; export const DEFAULT_TAGS_RESPONSE = [ { id: MOCK_TAG_ID, - name: 'test tag', - description: 'test tag description', - color: '#2c7b82', + attributes: { + name: MOCK_TAG_NAME, + description: 'test tag description', + color: '#2c7b82', + }, }, ]; diff --git a/x-pack/plugins/security_solution/public/common/containers/tags/api.ts b/x-pack/plugins/security_solution/public/common/containers/tags/api.ts index 57b27318103fc..4a9dd805e3384 100644 --- a/x-pack/plugins/security_solution/public/common/containers/tags/api.ts +++ b/x-pack/plugins/security_solution/public/common/containers/tags/api.ts @@ -6,10 +6,14 @@ */ import type { HttpSetup } from '@kbn/core/public'; -import type { Tag } from '@kbn/saved-objects-tagging-plugin/public'; import type { TagAttributes } from '@kbn/saved-objects-tagging-plugin/common'; import { INTERNAL_TAGS_URL } from '../../../../common/constants'; +export interface Tag { + id: string; + attributes: TagAttributes; +} + export const getTagsByName = ( { http, tagName }: { http: HttpSetup; tagName: string }, abortSignal?: AbortSignal diff --git a/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.test.ts b/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.test.ts index 6041df888dfcb..bfd4f8fbc9b85 100644 --- a/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.test.ts +++ b/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.test.ts @@ -12,6 +12,7 @@ import { SECURITY_TAG_DESCRIPTION, SECURITY_TAG_NAME, } from '../../../common/constants'; +import { DEFAULT_TAGS_RESPONSE } from '../../common/containers/tags/__mocks__/api'; import { useKibana } from '../../common/lib/kibana'; import { useFetchSecurityTags } from './use_fetch_security_tags'; @@ -66,11 +67,16 @@ describe('useFetchSecurityTags', () => { }); test('should return Security Solution tags', async () => { - const mockFoundTags = [{ id: 'tagId', name: 'Security Solution', description: '', color: '' }]; - mockGet.mockResolvedValue(mockFoundTags); + mockGet.mockResolvedValue(DEFAULT_TAGS_RESPONSE); + + const expected = DEFAULT_TAGS_RESPONSE.map((tag) => ({ + id: tag.id, + type: 'tag', + ...tag.attributes, + })); const { result } = await asyncRenderUseCreateSecurityDashboardLink(); expect(mockPut).not.toHaveBeenCalled(); - expect(result.current.tags).toEqual(expect.objectContaining(mockFoundTags)); + expect(result.current.tags).toEqual(expect.objectContaining(expected)); }); }); diff --git a/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.ts b/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.ts index a91daf7be16eb..6e8cb3eef6a6b 100644 --- a/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.ts +++ b/x-pack/plugins/security_solution/public/dashboards/containers/use_fetch_security_tags.ts @@ -42,9 +42,9 @@ export const useFetchSecurityTags = () => { const tagsResult = useMemo(() => { if (tags?.length) { - return tags; + return tags.map((t) => ({ id: t.id, type: 'tag', ...t.attributes })); } - return tag ? [tag] : undefined; + return tag ? [{ id: tag.id, type: 'tag', ...tag.attributes }] : undefined; }, [tags, tag]); return { diff --git a/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx b/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx index 32aea030e0632..e00b0aee57072 100644 --- a/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/context/dashboard_context.tsx @@ -9,8 +9,14 @@ import React from 'react'; import type { Tag } from '@kbn/saved-objects-tagging-plugin/common'; import { useFetchSecurityTags } from '../containers/use_fetch_security_tags'; +export interface TagReference extends Tag { + type: string; + color: string; + description: string; +} + export interface DashboardContextType { - securityTags: Tag[] | null; + securityTags: TagReference[] | null; } const DashboardContext = React.createContext({ securityTags: null }); diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx index fa12b78af2991..f710fd3ff2bd0 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; import React from 'react'; import { SecurityPageName } from '../../../app/types'; import { TestProviders } from '../../../common/mock'; @@ -13,12 +13,24 @@ import { DashboardsLandingPage } from '.'; import type { NavLinkItem } from '../../../common/components/navigation/types'; import { useCapabilities } from '../../../common/lib/kibana'; import * as telemetry from '../../../common/lib/telemetry'; - +import { DashboardListingTable } from '@kbn/dashboard-plugin/public'; +import { + DEFAULT_TAGS_RESPONSE, + MOCK_TAG_NAME, +} from '../../../common/containers/tags/__mocks__/api'; +import { DashboardContextProvider } from '../../context/dashboard_context'; +import { act } from 'react-dom/test-utils'; + +jest.mock('../../../common/containers/tags/api'); jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/utils/route/spy_routes', () => ({ SpyRoute: () => null })); -jest.mock('../../components/dashboards_table', () => ({ - DashboardsTable: () => , -})); +jest.mock('@kbn/dashboard-plugin/public', () => { + const actual = jest.requireActual('@kbn/dashboard-plugin/public'); + return { + ...actual, + DashboardListingTable: jest.fn().mockReturnValue(), + }; +}); const DEFAULT_DASHBOARD_CAPABILITIES = { show: true, createNew: true }; const mockUseCapabilities = useCapabilities as jest.Mock; @@ -63,97 +75,134 @@ jest.mock('../../hooks/use_create_security_dashboard_link', () => { }; }); -const renderDashboardLanding = () => render(, { wrapper: TestProviders }); +const TestComponent = () => ( + + + + + +); + +const renderDashboardLanding = async () => { + await act(async () => { + render(); + }); +}; describe('Dashboards landing', () => { + beforeEach(() => { + mockUseCapabilities.mockReturnValue(DEFAULT_DASHBOARD_CAPABILITIES); + mockUseCreateSecurityDashboard.mockReturnValue(CREATE_DASHBOARD_LINK); + }); + describe('Dashboards default links', () => { - it('should render items', () => { - const { queryByText } = renderDashboardLanding(); + it('should render items', async () => { + await renderDashboardLanding(); - expect(queryByText(OVERVIEW_ITEM_LABEL)).toBeInTheDocument(); - expect(queryByText(DETECTION_RESPONSE_ITEM_LABEL)).toBeInTheDocument(); + expect(screen.queryByText(OVERVIEW_ITEM_LABEL)).toBeInTheDocument(); + expect(screen.queryByText(DETECTION_RESPONSE_ITEM_LABEL)).toBeInTheDocument(); }); - it('should render items in the same order as defined', () => { + it('should render items in the same order as defined', async () => { mockAppManageLink.mockReturnValueOnce({ ...APP_DASHBOARD_LINKS, }); - const { queryAllByTestId } = renderDashboardLanding(); + await renderDashboardLanding(); - const renderedItems = queryAllByTestId('LandingImageCard-item'); + const renderedItems = screen.queryAllByTestId('LandingImageCard-item'); expect(renderedItems[0]).toHaveTextContent(OVERVIEW_ITEM_LABEL); expect(renderedItems[1]).toHaveTextContent(DETECTION_RESPONSE_ITEM_LABEL); }); - it('should not render items if all items filtered', () => { - mockAppManageLink.mockReturnValueOnce({ + it('should not render items if all items filtered', async () => { + mockAppManageLink.mockReturnValue({ ...APP_DASHBOARD_LINKS, links: [], }); - const { queryByText } = renderDashboardLanding(); + await renderDashboardLanding(); - expect(queryByText(OVERVIEW_ITEM_LABEL)).not.toBeInTheDocument(); - expect(queryByText(DETECTION_RESPONSE_ITEM_LABEL)).not.toBeInTheDocument(); + expect(screen.queryByText(OVERVIEW_ITEM_LABEL)).not.toBeInTheDocument(); + expect(screen.queryByText(DETECTION_RESPONSE_ITEM_LABEL)).not.toBeInTheDocument(); }); }); describe('Security Dashboards', () => { - it('should render dashboards table', () => { - const result = renderDashboardLanding(); + it('should render dashboards table', async () => { + await renderDashboardLanding(); + + expect(screen.getByTestId('dashboardsTable')).toBeInTheDocument(); + }); + + it('should call DashboardListingTable with correct fixedTagReferences', async () => { + await renderDashboardLanding(); + + expect((DashboardListingTable as jest.Mock).mock.calls[0][0].fixedTagReferences).toEqual( + DEFAULT_TAGS_RESPONSE.map((res) => ({ + id: res.id, + type: 'tag', + ...res.attributes, + })) + ); + }); + + it('should call DashboardListingTable with correct initialFilter', async () => { + await renderDashboardLanding(); - expect(result.getByTestId('dashboardsTable')).toBeInTheDocument(); + expect((DashboardListingTable as jest.Mock).mock.calls[0][0].initialFilter).toEqual( + `tag:("${MOCK_TAG_NAME}")` + ); }); - it('should not render dashboards table if no read capability', () => { - mockUseCapabilities.mockReturnValueOnce({ + it('should not render dashboards table if no read capability', async () => { + mockUseCapabilities.mockReturnValue({ ...DEFAULT_DASHBOARD_CAPABILITIES, show: false, }); - const result = renderDashboardLanding(); + await renderDashboardLanding(); - expect(result.queryByTestId('dashboardsTable')).not.toBeInTheDocument(); + expect(screen.queryByTestId('dashboardsTable')).not.toBeInTheDocument(); }); describe('Create Security Dashboard button', () => { - it('should render', () => { - const result = renderDashboardLanding(); + it('should render', async () => { + await renderDashboardLanding(); - expect(result.getByTestId('createDashboardButton')).toBeInTheDocument(); + expect(screen.getByTestId('createDashboardButton')).toBeInTheDocument(); }); - it('should not render if no write capability', () => { - mockUseCapabilities.mockReturnValueOnce({ + it('should not render if no write capability', async () => { + mockUseCapabilities.mockReturnValue({ ...DEFAULT_DASHBOARD_CAPABILITIES, createNew: false, }); - const result = renderDashboardLanding(); + await renderDashboardLanding(); - expect(result.queryByTestId('createDashboardButton')).not.toBeInTheDocument(); + expect(screen.queryByTestId('createDashboardButton')).not.toBeInTheDocument(); }); - it('should be enabled when link loaded', () => { - const result = renderDashboardLanding(); + it('should be enabled when link loaded', async () => { + await renderDashboardLanding(); - expect(result.getByTestId('createDashboardButton')).not.toHaveAttribute('disabled'); + expect(screen.getByTestId('createDashboardButton')).not.toHaveAttribute('disabled'); }); - it('should be disabled when link is not loaded', () => { - mockUseCreateSecurityDashboard.mockReturnValueOnce({ isLoading: true, url: '' }); - const result = renderDashboardLanding(); + it('should be disabled when link is not loaded', async () => { + mockUseCreateSecurityDashboard.mockReturnValue({ isLoading: true, url: '' }); + await renderDashboardLanding(); - expect(result.getByTestId('createDashboardButton')).toHaveAttribute('disabled'); + expect(screen.getByTestId('createDashboardButton')).toHaveAttribute('disabled'); }); - it('should link to correct href', () => { - const result = renderDashboardLanding(); + it('should link to correct href', async () => { + await renderDashboardLanding(); - expect(result.getByTestId('createDashboardButton')).toHaveAttribute('href', URL); + expect(screen.getByTestId('createDashboardButton')).toHaveAttribute('href', URL); }); - it('should send telemetry', () => { - const result = renderDashboardLanding(); - result.getByTestId('createDashboardButton').click(); + it('should send telemetry', async () => { + await renderDashboardLanding(); + screen.getByTestId('createDashboardButton').click(); expect(spyTrack).toHaveBeenCalledWith( telemetry.METRIC_TYPE.CLICK, telemetry.TELEMETRY_EVENT.CREATE_DASHBOARD diff --git a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx index 513ad89c482c2..62efd71a196bf 100644 --- a/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx +++ b/x-pack/plugins/security_solution/public/dashboards/pages/landing_page/index.tsx @@ -5,9 +5,9 @@ * 2.0. */ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiSpacer, EuiTitle } from '@elastic/eui'; -import React from 'react'; +import React, { useCallback, useMemo } from 'react'; import type { DashboardCapabilities } from '@kbn/dashboard-plugin/common/types'; -import { LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; +import { DashboardListingTable, LEGACY_DASHBOARD_APP_ID } from '@kbn/dashboard-plugin/public'; import { SecuritySolutionPageWrapper } from '../../../common/components/page_wrapper'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { LandingImageCards } from '../../../common/components/landing_links/landing_links_images'; @@ -19,11 +19,32 @@ import { LinkButton } from '../../../common/components/links/helpers'; import * as i18n from './translations'; import { METRIC_TYPE, TELEMETRY_EVENT, track } from '../../../common/lib/telemetry'; import { DASHBOARDS_PAGE_TITLE } from '../translations'; +import { useGetSecuritySolutionUrl } from '../../../common/components/link_to'; import { useCreateSecurityDashboardLink } from '../../hooks/use_create_security_dashboard_link'; -import { DashboardsTable } from '../../components/dashboards_table'; +import type { TagReference } from '../../context/dashboard_context'; +import { useSecurityTags } from '../../context/dashboard_context'; -const Header: React.FC<{ canCreateDashboard: boolean }> = ({ canCreateDashboard }) => { - const { isLoading, url } = useCreateSecurityDashboardLink(); +const getInitialFilterString = (securityTags: TagReference[] | null | undefined) => { + if (!securityTags) { + return; + } + const uniqueQueryArray = securityTags?.reduce((acc, { name }) => { + const nameString = `"${name}"`; + if (name && acc.indexOf(nameString) === -1) { + acc.push(nameString); + } + return acc; + }, []); + + const query = [uniqueQueryArray].join(' or'); + return `tag:(${query})`; +}; + +const Header: React.FC<{ canCreateDashboard: boolean; isLoading: boolean; url: string }> = ({ + canCreateDashboard, + isLoading, + url, +}) => { const { navigateTo } = useNavigateTo(); return ( @@ -57,10 +78,42 @@ export const DashboardsLandingPage = () => { const dashboardLinks = useRootNavLink(SecurityPageName.dashboards)?.links ?? []; const { show: canReadDashboard, createNew: canCreateDashboard } = useCapabilities(LEGACY_DASHBOARD_APP_ID); + const { navigateTo } = useNavigateTo(); + const getSecuritySolutionUrl = useGetSecuritySolutionUrl(); + const getSecuritySolutionDashboardUrl = useCallback( + (id: string) => + `${getSecuritySolutionUrl({ + deepLinkId: SecurityPageName.dashboards, + path: id, + })}`, + [getSecuritySolutionUrl] + ); + const { isLoading: loadingCreateDashboardUrl, url: createDashboardUrl } = + useCreateSecurityDashboardLink(); + + const getHref = useCallback( + (id: string | undefined) => (id ? getSecuritySolutionDashboardUrl(id) : createDashboardUrl), + [createDashboardUrl, getSecuritySolutionDashboardUrl] + ); + + const goToDashboard = useCallback( + (dashboardId: string | undefined) => { + track(METRIC_TYPE.CLICK, TELEMETRY_EVENT.DASHBOARD); + navigateTo({ url: getHref(dashboardId) }); + }, + [getHref, navigateTo] + ); + + const securityTags = useSecurityTags(); + const initialFilter = useMemo(() => getInitialFilterString(securityTags), [securityTags]); return ( - -
+ +
@@ -68,16 +121,27 @@ export const DashboardsLandingPage = () => { - + - {canReadDashboard && ( + {canReadDashboard && securityTags && ( <> - -

{i18n.DASHBOARDS_PAGE_SECTION_CUSTOM}

-
- - - + + +

{i18n.DASHBOARDS_PAGE_SECTION_CUSTOM}

+
+ + +
)}