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

[Dashboard Navigation] Add link editing + reordering #161568

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
858432b
Stub edit UI + make opening link editor flyout imperative
Heenawter Jul 10, 2023
e508591
Can delete links
Heenawter Jul 10, 2023
bd6a27d
Small cleanup
Heenawter Jul 10, 2023
69564b2
First attempt at editing external links
Heenawter Jul 10, 2023
98e05fb
Working on editing dashboard links
Heenawter Jul 10, 2023
ca0ba08
Fix validity check for dashboard picker
Heenawter Jul 10, 2023
2ddb6ec
Seperate out link logic to improve dashboard loading
Heenawter Jul 10, 2023
c8474b0
Hide custom time range in panel settings
Heenawter Jul 11, 2023
1973cae
Add animation on unmount
Heenawter Jul 11, 2023
e5c0541
Fix bug with link label
Heenawter Jul 11, 2023
c4fc9a8
Fix bug with dashboard list
Heenawter Jul 11, 2023
96a0cef
Add link reordering
Heenawter Jul 12, 2023
9ade9ab
Clean up
Heenawter Jul 12, 2023
75c3338
Add i18n
Heenawter Jul 12, 2023
07309c8
Improve flyout opening speed
Heenawter Jul 12, 2023
adfcc09
Add tooltips to edit/delete buttons
Heenawter Jul 12, 2023
bd4f5e1
Clean up panel editor
Heenawter Jul 12, 2023
f7baaa9
Make things more efficient
Heenawter Jul 12, 2023
7ec5bc8
Go back to unmemoized version
Heenawter Jul 12, 2023
2f6b188
More code clean up
Heenawter Jul 12, 2023
54f6259
Memoize ordered link list
Heenawter Jul 12, 2023
d6085be
Reduce height of links
Heenawter Jul 13, 2023
ce016b4
Use value instead of placeholder for link label
Heenawter Jul 13, 2023
42c3fed
Address first round of Nick's feedback
Heenawter Jul 13, 2023
e7e3578
Fix linting
Heenawter Jul 13, 2023
c1daec2
Switch to lazy loading part of flyout
Heenawter Jul 13, 2023
44f58ad
[CI] Auto-commit changed files from 'node scripts/lint_ts_projects --…
kibanamachine Jul 13, 2023
b4d73d5
Fix external link
Heenawter Jul 14, 2023
946fcfb
Merge branch 'navigation-embeddable' of github.com:elastic/kibana int…
Heenawter Jul 14, 2023
928243c
Add tooltip to save button when disabled
Heenawter Jul 14, 2023
60cf98b
Remove code I accidentally committed
Heenawter Jul 14, 2023
263d338
Undo changes to dashboard picker
Heenawter Jul 14, 2023
be256b1
Change flyout title when editing link panel
Heenawter Jul 14, 2023
bdf7756
Switch to `EuiComboBox` for dashboard selector
Heenawter Jul 14, 2023
12ecd76
Remove EOL whitespace
Heenawter Jul 14, 2023
36bba2e
Fix bug when clearing dashboard selection
Heenawter Jul 14, 2023
5e81c45
Fix styling of small flyout
Heenawter Jul 14, 2023
6a90379
Change id of droppable area from generic
Heenawter Jul 17, 2023
a2f4d21
Limit width on flyout
Heenawter Jul 17, 2023
401e854
Fix `scss` linting
Heenawter Jul 17, 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
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
Heenawter marked this conversation as resolved.
Show resolved Hide resolved
);
}

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%);
}
}

Heenawter marked this conversation as resolved.
Show resolved Hide resolved
@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