Skip to content

Commit

Permalink
[Dashboard Navigation] Add link editing + reordering (#161568)
Browse files Browse the repository at this point in the history
Closes #154361
Closes #161274
Closes #161693

## Summary

This PR adds editing capabilities to the navigation embeddable,
including deleting/editing existing links and reordering the list of
links. It also fixes the delay in opening the editing flyout from the
async import in `getExplicitInput` (from the navigation embeddable
factory) by moving it to the constructor of the factory.



https://github.com/elastic/kibana/assets/8698078/ace9dcd4-0607-40de-959e-94348a5fa4fa



### Checklist

- [x] Any text added follows [EUI's writing
guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses
sentence case text and includes [i18n
support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md)
- [ ] ~[Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios~ Will be
addressed in #161287
- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)


### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)

---------

Co-authored-by: kibanamachine <[email protected]>
  • Loading branch information
Heenawter and kibanamachine authored Jul 17, 2023
1 parent 70813b7 commit c7485e6
Show file tree
Hide file tree
Showing 19 changed files with 757 additions and 282 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,15 @@ export class CustomizePanelAction implements Action<CustomizePanelActionContext>
(embeddable as VisualizeEmbeddable).getOutput().visTypeName === 'markdown';

const isImage = embeddable.type === 'image';
const isNavigation = embeddable.type === 'navigation';

return Boolean(
embeddable && hasTimeRange(embeddable) && !isInputControl && !isMarkdown && !isImage
embeddable &&
hasTimeRange(embeddable) &&
!isInputControl &&
!isMarkdown &&
!isImage &&
!isNavigation
);
}

Expand Down
21 changes: 19 additions & 2 deletions src/plugins/navigation_embeddable/public/_mixins.scss
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
@import '../../../core/public/mixins';

@keyframes euiFlyoutAnimation {
@keyframes euiFlyoutOpenAnimation {
0% {
opacity: 0;
transform: translateX(100%);
Expand All @@ -12,14 +12,31 @@
}
}

@keyframes euiFlyoutCloseAnimation {
0% {
opacity: 1;
transform: translateX(0%);
}

100% {
opacity: 0;
transform: translateX(100%);
}
}

@mixin euiFlyout {
@include kibanaFullBodyHeight();
border-left: $euiBorderThin;
position: fixed;
width: 50%;
z-index: $euiZFlyout;
background: $euiColorEmptyShade;
display: flex;
flex-direction: column;
align-items: stretch;
inline-size: 50vw;

@media only screen and (max-width: 767px) {
inline-size: $euiSizeXL * 13; // 424px
max-inline-size: 90vw;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,13 @@ export const DashboardLinkComponent = ({ link }: { link: NavigationEmbeddableLin

const { loading: loadingDestinationDashboard, value: destinationDashboard } =
useAsync(async () => {
return await fetchDashboard(link.destination);
if (!link.label && link.id !== parentDashboardId) {
/**
* only fetch the dashboard if **absolutely** necessary; i.e. only if the dashboard link doesn't have
* some custom label, and if it's not the current dashboard (if it is, use `dashboardContainer` instead)
*/
return await fetchDashboard(link.destination);
}
}, [link, parentDashboardId]);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,61 +8,68 @@

import { debounce } from 'lodash';
import useAsync from 'react-use/lib/useAsync';
import React, { useEffect, useMemo, useState } from 'react';
import useMount from 'react-use/lib/useMount';
import React, { useCallback, useMemo, useState } from 'react';

import {
EuiBadge,
EuiSpacer,
EuiComboBox,
EuiFlexItem,
EuiHighlight,
EuiSelectable,
EuiFieldSearch,
EuiSelectableOption,
EuiFlexGroup,
EuiComboBoxOptionOption,
} from '@elastic/eui';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';

import { DashboardItem } from '../../embeddable/types';
import { memoizedFetchDashboards } from './dashboard_link_tools';
import { memoizedFetchDashboard, memoizedFetchDashboards } from './dashboard_link_tools';
import { DashboardLinkEmbeddableStrings } from './dashboard_link_strings';

type DashboardComboBoxOption = EuiComboBoxOptionOption<DashboardItem>;

export const DashboardLinkDestinationPicker = ({
setDestination,
setPlaceholder,
currentDestination,
onDestinationPicked,
initialSelection,
parentDashboard,
...other
}: {
setDestination: (destination?: string) => void;
setPlaceholder: (placeholder?: string) => void;
currentDestination?: string;
onDestinationPicked: (selectedDashboard?: DashboardItem) => void;
parentDashboard?: DashboardContainer;
initialSelection?: string;
}) => {
const [searchString, setSearchString] = useState<string>('');
const [selectedDashboard, setSelectedDashboard] = useState<DashboardItem | undefined>();
const [dashboardListOptions, setDashboardListOptions] = useState<EuiSelectableOption[]>([]);
const [selectedOption, setSelectedOption] = useState<DashboardComboBoxOption[]>([]);

const parentDashboardId = parentDashboard?.select((state) => state.componentState.lastSavedId);

const { loading: loadingDashboardList, value: dashboardList } = useAsync(async () => {
return await memoizedFetchDashboards(searchString, undefined, parentDashboardId);
}, [searchString, parentDashboardId]);
const getDashboardItem = useCallback((dashboard: DashboardItem) => {
return {
key: dashboard.id,
value: dashboard,
label: dashboard.attributes.title,
className: 'navEmbeddableDashboardItem',
};
}, []);

useEffect(() => {
const dashboardOptions =
(dashboardList ?? []).map((dashboard: DashboardItem) => {
return {
data: dashboard,
label: dashboard.attributes.title,
...(dashboard.id === parentDashboardId
? {
prepend: (
<EuiBadge>{DashboardLinkEmbeddableStrings.getCurrentDashboardLabel()}</EuiBadge>
),
}
: {}),
} as EuiSelectableOption;
}) ?? [];
useMount(async () => {
if (initialSelection) {
const dashboard = await memoizedFetchDashboard(initialSelection);
onDestinationPicked(dashboard);
setSelectedOption([getDashboardItem(dashboard)]);
}
});

setDashboardListOptions(dashboardOptions);
}, [dashboardList, parentDashboardId, searchString]);
const { loading: loadingDashboardList, value: dashboardList } = useAsync(async () => {
const dashboards = await memoizedFetchDashboards({
search: searchString,
parentDashboardId,
selectedDashboardId: initialSelection,
});
const dashboardOptions = (dashboards ?? []).map((dashboard: DashboardItem) => {
return getDashboardItem(dashboard);
});
return dashboardOptions;
}, [searchString, parentDashboardId, getDashboardItem]);

const debouncedSetSearch = useMemo(
() =>
Expand All @@ -72,47 +79,53 @@ export const DashboardLinkDestinationPicker = ({
[setSearchString]
);

useEffect(() => {
if (selectedDashboard) {
setDestination(selectedDashboard.id);
setPlaceholder(selectedDashboard.attributes.title);
} else {
setDestination(undefined);
setPlaceholder(undefined);
}
}, [selectedDashboard, setDestination, setPlaceholder]);
const renderOption = useCallback(
(option, searchValue, contentClassName) => {
const { label, key: dashboardId } = option;
return (
<EuiFlexGroup gutterSize="s" alignItems="center" className={contentClassName}>
{dashboardId === parentDashboardId && (
<EuiFlexItem grow={false}>
<EuiBadge>{DashboardLinkEmbeddableStrings.getCurrentDashboardLabel()}</EuiBadge>
</EuiFlexItem>
)}
<EuiFlexItem className={'navEmbeddableLinkText'}>
<EuiHighlight search={searchValue} className={'wrapText'}>
{label}
</EuiHighlight>
</EuiFlexItem>
</EuiFlexGroup>
);
},
[parentDashboardId]
);

/* {...other} is needed so all inner elements are treated as part of the form */
/* {...other} is needed so the EuiComboBox is 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>
<EuiComboBox
{...other}
async
fullWidth
className={'navEmbeddableDashboardPicker'}
isLoading={loadingDashboardList}
aria-label={DashboardLinkEmbeddableStrings.getDashboardPickerAriaLabel()}
placeholder={DashboardLinkEmbeddableStrings.getDashboardPickerPlaceholder()}
singleSelection={{ asPlainText: true }}
options={dashboardList}
onSearchChange={(searchValue) => {
debouncedSetSearch(searchValue);
}}
renderOption={renderOption}
selectedOptions={selectedOption}
onChange={(option) => {
setSelectedOption(option);
if (option.length > 0) {
// single select is `true`, so there is only ever one item in the array
onDestinationPicked(option[0].value);
} else {
onDestinationPicked(undefined);
}
}}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export const DashboardLinkEmbeddableStrings = {
i18n.translate('navigationEmbeddable.dsahboardLink.description', {
defaultMessage: 'Go to dashboard',
}),
getSearchPlaceholder: () =>
i18n.translate('navigationEmbeddable.dashboardLink.editor.searchPlaceholder', {
getDashboardPickerPlaceholder: () =>
i18n.translate('navigationEmbeddable.dashboardLink.editor.dashboardComboBoxPlaceholder', {
defaultMessage: 'Search for a dashboard',
}),
getDashboardPickerAriaLabel: () =>
Expand Down
Loading

0 comments on commit c7485e6

Please sign in to comment.