-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
[Dashboard Navigation] Add creation UI #160179
Changes from 63 commits
b1d9f30
e0c4bd2
7a0ac38
6faccad
73ad237
2198908
50638fc
6982698
bc9c863
6e54b30
499b001
d58c714
fa69298
6dadf88
5768166
e70ed3c
8523c77
2720ebe
21d35ce
338abbb
3c760a6
c8029e3
0215127
f6c59f9
0642e60
a35e341
a013db2
2bf27bb
38f87ed
a1c5127
acf7867
1a62f9c
94974fd
7725e52
7687f36
32a6b85
6bfef0a
6a76e0e
ef92a99
80eac65
304e3b7
977d135
bde1630
1b095df
fcbbd04
4da5749
5f07ba0
e55947d
f4ad6a2
049608c
173edd0
31b4d8c
9b41ed8
2b0ae54
2cbe2c9
1c71b1c
7d1864b
47e09f5
3644406
c0c81e2
d71df9e
9b10044
852e990
b0358d1
efbfddf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,6 +18,7 @@ import { DASHBOARD_CONTENT_ID } from '../../../dashboard_constants'; | |
|
||
export interface SearchDashboardsArgs { | ||
contentManagement: DashboardStartDependencies['contentManagement']; | ||
options?: DashboardCrudTypes['SearchIn']['options']; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I added search options so that the navigation embeddable dashboard search could match only on title and not description. |
||
hasNoReference?: SavedObjectsFindOptionsReference[]; | ||
hasReference?: SavedObjectsFindOptionsReference[]; | ||
search: string; | ||
|
@@ -33,6 +34,7 @@ export async function searchDashboards({ | |
contentManagement, | ||
hasNoReference, | ||
hasReference, | ||
options, | ||
search, | ||
size, | ||
}: SearchDashboardsArgs): Promise<SearchDashboardsResponse> { | ||
|
@@ -52,6 +54,7 @@ export async function searchDashboards({ | |
excluded: (hasNoReference ?? []).map(({ id }) => id), | ||
}, | ||
}, | ||
options, | ||
}); | ||
return { | ||
total, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -301,7 +301,7 @@ export abstract class Container< | |
|
||
public async getExplicitInputIsEqual(lastInput: TContainerInput) { | ||
const { panels: lastPanels, ...restOfLastInput } = lastInput; | ||
const { panels: currentPanels, ...restOfCurrentInput } = this.getInput(); | ||
const { panels: currentPanels, ...restOfCurrentInput } = this.getExplicitInput(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This was a bug that was never caught because we've never had a container embeddable as a panel in Dashboard before - basically, because we were calling |
||
const otherInputIsEqual = isEqual(restOfLastInput, restOfCurrentInput); | ||
if (!otherInputIsEqual) return false; | ||
|
||
|
@@ -371,7 +371,6 @@ export abstract class Container< | |
initializeSettings?: EmbeddableContainerSettings | ||
) { | ||
let initializeOrder = Object.keys(initialInput.panels); | ||
|
||
if (initializeSettings?.childIdInitializeOrder) { | ||
const initializeOrderSet = new Set<string>(); | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -430,27 +430,28 @@ export class EmbeddablePanel extends React.Component<Props, State> { | |
}; | ||
|
||
private getActionContextMenuPanel = async () => { | ||
let regularActions = | ||
const regularActions = | ||
(await this.props.getActions?.(CONTEXT_MENU_TRIGGER, { | ||
embeddable: this.props.embeddable, | ||
})) ?? []; | ||
|
||
const { disabledActions } = this.props.embeddable.getInput(); | ||
|
||
let allActions = regularActions.concat( | ||
Object.values(this.state.universalActions ?? {}) as Array<Action<object>> | ||
); | ||
if (disabledActions) { | ||
const removeDisabledActions = removeById(disabledActions); | ||
regularActions = regularActions.filter(removeDisabledActions); | ||
allActions = allActions.filter(removeDisabledActions); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because drilldowns are considered a "universal action," I had to do the filtering of disabled actions after the universal actions were added to the action list so that I could filter the drilldown action out of the link panel. With this change, the drilldown UI action is now disabled for the navigation embeddable: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
} | ||
|
||
let sortedActions = regularActions | ||
.concat(Object.values(this.state.universalActions || {}) as Array<Action<object>>) | ||
.sort(sortByOrderField); | ||
|
||
if (this.props.actionPredicate) { | ||
sortedActions = sortedActions.filter(({ id }) => this.props.actionPredicate!(id)); | ||
allActions = allActions.filter(({ id }) => this.props.actionPredicate!(id)); | ||
} | ||
allActions.sort(sortByOrderField); | ||
|
||
const panels = await buildContextMenuForActions({ | ||
actions: sortedActions.map((action) => ({ | ||
actions: allActions.map((action) => ({ | ||
action, | ||
context: { embeddable: this.props.embeddable }, | ||
trigger: contextMenuTrigger, | ||
|
@@ -460,7 +461,7 @@ export class EmbeddablePanel extends React.Component<Props, State> { | |
|
||
return { | ||
panels, | ||
actions: sortedActions, | ||
actions: allActions, | ||
}; | ||
}; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,17 +1,14 @@ | ||
{ | ||
"type": "plugin", | ||
"id": "@kbn/navigation-embeddable-plugin", | ||
"owner": "@elastic/kibana-presentation", | ||
"id": "@kbn/navigation-embeddable-plugin", | ||
"description": "An embeddable for quickly navigating between dashboards.", | ||
"plugin": { | ||
"id": "navigationEmbeddable", | ||
"server": false, | ||
"browser": true, | ||
"requiredPlugins": [ | ||
"embeddable" | ||
], | ||
"optionalPlugins": [], | ||
"requiredBundles": [ | ||
] | ||
"requiredPlugins": ["dashboard", "embeddable", "kibanaReact", "presentationUtil"], | ||
"optionalPlugins": ["triggersActionsUi"], | ||
"requiredBundles": [] | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
@import '../../../core/public/mixins'; | ||
|
||
@keyframes euiFlyoutAnimation { | ||
0% { | ||
opacity: 0; | ||
transform: translateX(100%); | ||
} | ||
|
||
100% { | ||
opacity: 1; | ||
transform: translateX(0%); | ||
} | ||
} | ||
|
||
@mixin euiFlyout { | ||
@include kibanaFullBodyHeight(); | ||
border-left: $euiBorderThin; | ||
position: fixed; | ||
width: 50%; | ||
z-index: $euiZFlyout; | ||
background: $euiColorEmptyShade; | ||
display: flex; | ||
flex-direction: column; | ||
align-items: stretch; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import React from 'react'; | ||
import useAsync from 'react-use/lib/useAsync'; | ||
|
||
import { EuiButtonEmpty } from '@elastic/eui'; | ||
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; | ||
|
||
import { | ||
DASHBOARD_LINK_TYPE, | ||
NavigationEmbeddableLink, | ||
NavigationLinkInfo, | ||
} from '../../embeddable/types'; | ||
import { fetchDashboard } from './dashboard_link_tools'; | ||
import { useNavigationEmbeddable } from '../../embeddable/navigation_embeddable'; | ||
|
||
export const DashboardLinkComponent = ({ link }: { link: NavigationEmbeddableLink }) => { | ||
const navEmbeddable = useNavigationEmbeddable(); | ||
|
||
const dashboardContainer = navEmbeddable.parent as DashboardContainer; | ||
const parentDashboardTitle = dashboardContainer.select((state) => state.explicitInput.title); | ||
const parentDashboardId = dashboardContainer.select((state) => state.componentState.lastSavedId); | ||
|
||
const { loading: loadingDestinationDashboard, value: destinationDashboard } = | ||
useAsync(async () => { | ||
return await fetchDashboard(link.destination); | ||
}, [link, parentDashboardId]); | ||
Comment on lines
+31
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider the following scenario:
That is why I added |
||
|
||
return ( | ||
<EuiButtonEmpty | ||
isLoading={loadingDestinationDashboard} | ||
iconType={NavigationLinkInfo[DASHBOARD_LINK_TYPE].icon} | ||
{...(link.destination === parentDashboardId | ||
? { | ||
color: 'text', | ||
} | ||
: { | ||
color: 'primary', | ||
onClick: () => {}, // TODO: As part of https://github.com/elastic/kibana/issues/154381, connect to drilldown | ||
})} | ||
> | ||
{link.label || | ||
(link.destination === parentDashboardId | ||
? parentDashboardTitle | ||
: destinationDashboard?.attributes.title)} | ||
</EuiButtonEmpty> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { debounce } from 'lodash'; | ||
import useAsync from 'react-use/lib/useAsync'; | ||
import React, { useEffect, useMemo, useState } from 'react'; | ||
|
||
import { | ||
EuiBadge, | ||
EuiSpacer, | ||
EuiHighlight, | ||
EuiSelectable, | ||
EuiFieldSearch, | ||
EuiSelectableOption, | ||
} from '@elastic/eui'; | ||
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container'; | ||
import { DashboardItem } from '../../embeddable/types'; | ||
import { memoizedFetchDashboards } from './dashboard_link_tools'; | ||
import { DashboardLinkEmbeddableStrings } from './dashboard_link_strings'; | ||
|
||
export const DashboardLinkDestinationPicker = ({ | ||
setDestination, | ||
setPlaceholder, | ||
currentDestination, | ||
parentDashboard, | ||
...other | ||
}: { | ||
setDestination: (destination?: string) => void; | ||
setPlaceholder: (placeholder?: string) => void; | ||
currentDestination?: string; | ||
parentDashboard?: DashboardContainer; | ||
}) => { | ||
const [searchString, setSearchString] = useState<string>(''); | ||
const [selectedDashboard, setSelectedDashboard] = useState<DashboardItem | undefined>(); | ||
const [dashboardListOptions, setDashboardListOptions] = useState<EuiSelectableOption[]>([]); | ||
|
||
const parentDashboardId = parentDashboard?.select((state) => state.componentState.lastSavedId); | ||
|
||
const { loading: loadingDashboardList, value: dashboardList } = useAsync(async () => { | ||
return await memoizedFetchDashboards(searchString, undefined, parentDashboardId); | ||
}, [searchString, parentDashboardId]); | ||
Comment on lines
+44
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to https://github.com/elastic/kibana/pull/160179/files#r1254915984, we want a |
||
|
||
useEffect(() => { | ||
const dashboardOptions = | ||
(dashboardList ?? []).map((dashboard: DashboardItem) => { | ||
return { | ||
data: dashboard, | ||
label: dashboard.attributes.title, | ||
...(dashboard.id === parentDashboardId | ||
? { | ||
prepend: ( | ||
<EuiBadge>{DashboardLinkEmbeddableStrings.getCurrentDashboardLabel()}</EuiBadge> | ||
), | ||
} | ||
: {}), | ||
} as EuiSelectableOption; | ||
}) ?? []; | ||
|
||
setDashboardListOptions(dashboardOptions); | ||
}, [dashboardList, parentDashboardId, searchString]); | ||
|
||
const debouncedSetSearch = useMemo( | ||
() => | ||
debounce((newSearch: string) => { | ||
setSearchString(newSearch); | ||
}, 250), | ||
[setSearchString] | ||
); | ||
|
||
useEffect(() => { | ||
if (selectedDashboard) { | ||
setDestination(selectedDashboard.id); | ||
setPlaceholder(selectedDashboard.attributes.title); | ||
} else { | ||
setDestination(undefined); | ||
setPlaceholder(undefined); | ||
} | ||
}, [selectedDashboard, setDestination, setPlaceholder]); | ||
|
||
/* {...other} is needed so all inner elements are treated as part of the form */ | ||
return ( | ||
<div {...other}> | ||
<EuiFieldSearch | ||
isClearable={true} | ||
placeholder={DashboardLinkEmbeddableStrings.getSearchPlaceholder()} | ||
onChange={(e) => { | ||
debouncedSetSearch(e.target.value); | ||
}} | ||
/> | ||
<EuiSpacer size="s" /> | ||
<EuiSelectable | ||
singleSelection={true} | ||
options={dashboardListOptions} | ||
isLoading={loadingDashboardList} | ||
listProps={{ onFocusBadge: false, bordered: true, isVirtualized: true }} | ||
aria-label={DashboardLinkEmbeddableStrings.getDashboardPickerAriaLabel()} | ||
onChange={(newOptions, _, selected) => { | ||
if (selected.checked) { | ||
setSelectedDashboard(selected.data as DashboardItem); | ||
} else { | ||
setSelectedDashboard(undefined); | ||
} | ||
setDashboardListOptions(newOptions); | ||
}} | ||
renderOption={(option) => { | ||
return <EuiHighlight search={searchString}>{option.label}</EuiHighlight>; | ||
}} | ||
> | ||
{(list) => list} | ||
</EuiSelectable> | ||
</div> | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
/* | ||
* 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 and the Server Side Public License, v 1; you may not use this file except | ||
* in compliance with, at your election, the Elastic License 2.0 or the Server | ||
* Side Public License, v 1. | ||
*/ | ||
|
||
import { i18n } from '@kbn/i18n'; | ||
|
||
export const DashboardLinkEmbeddableStrings = { | ||
getDisplayName: () => | ||
i18n.translate('navigationEmbeddable.dashboardLink.displayName', { | ||
defaultMessage: 'Dashboard', | ||
}), | ||
getDescription: () => | ||
i18n.translate('navigationEmbeddable.dsahboardLink.description', { | ||
defaultMessage: 'Go to dashboard', | ||
}), | ||
getSearchPlaceholder: () => | ||
i18n.translate('navigationEmbeddable.dashboardLink.editor.searchPlaceholder', { | ||
defaultMessage: 'Search for a dashboard', | ||
}), | ||
getDashboardPickerAriaLabel: () => | ||
i18n.translate('navigationEmbeddable.dashboardLink.editor.dashboardPickerAriaLabel', { | ||
defaultMessage: 'Pick a destination dashboard', | ||
}), | ||
getCurrentDashboardLabel: () => | ||
i18n.translate('navigationEmbeddable.dashboardLink.editor.currentDashboardLabel', { | ||
defaultMessage: 'Current', | ||
}), | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I had to modify the
getExplicitInput
for embeddable factories to accept an optional parent - this is because, when creating the navigation container (i.e. before the container embeddable actually exists), I need to know the ID of the parent dashboard so that I can add thecurrent
tag to the dashboard list even when creating a brand new navigation embeddable: