Skip to content

Commit

Permalink
First attempt at allowing external links
Browse files Browse the repository at this point in the history
  • Loading branch information
Heenawter committed Jun 26, 2023
1 parent 6dadf88 commit 5768166
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 73 deletions.
2 changes: 2 additions & 0 deletions src/plugins/image_embeddable/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
import { PluginInitializerContext } from '@kbn/core/public';
import { ImageEmbeddablePlugin } from './plugin';

export { createValidateUrl } from './utils/validate_url';

export { type ImageClickContext, IMAGE_CLICK_TRIGGER } from './actions';

export function plugin(context: PluginInitializerContext) {
Expand Down
12 changes: 9 additions & 3 deletions src/plugins/navigation_embeddable/kibana.jsonc
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
{
"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", "dashboard", "presentationUtil", "kibanaReact"],
"optionalPlugins": [],
"requiredPlugins": [
"dashboard",
"embeddable",
"kibanaReact",
"presentationUtil",
"imageEmbeddable"
],
"optionalPlugins": ["triggersActionsUi"],
"requiredBundles": []
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,16 @@ import {
EuiPopover,
} from '@elastic/eui';

import { isDashboardLink } from '../types';
import { useNavigationEmbeddable } from '../embeddable/navigation_embeddable';
import { NavigationEmbeddableLinkEditor } from './navigation_embeddable_link_editor';

import './navigation_embeddable.scss';
import { NavigationEmbeddableLinkEditor } from './navigation_embeddable_link_editor';

export const NavigationEmbeddableComponent = () => {
const navEmbeddable = useNavigationEmbeddable();

const selectedDashboards = navEmbeddable.select((state) => state.componentState.dashboardLinks);
const links = navEmbeddable.select((state) => state.componentState.links);
const currentDashboardId = navEmbeddable.select(
(state) => state.componentState.currentDashboardId
);
Expand All @@ -36,22 +37,30 @@ export const NavigationEmbeddableComponent = () => {

useEffect(() => {
setDashboardListGroupItems(
(selectedDashboards ?? []).map((dashboard) => {
(links ?? []).map((link) => {
if (isDashboardLink(link)) {
return {
label: link.label || link.title,
iconType: 'dashboardApp',
...(link.id === currentDashboardId
? {
color: 'text',
}
: {
color: 'primary',
onClick: () => {}, // TODO: connect to drilldown
}),
};
}
return {
label: dashboard.label || dashboard.title,
iconType: 'dashboardApp',
...(dashboard.id === currentDashboardId
? {
color: 'text',
}
: {
color: 'primary',
onClick: () => {}, // TODO: connect to drilldown
}),
label: link.label || link.url,
iconType: 'link',
color: 'primary',
onClick: () => {}, // TODO: connect to drilldown
};
})
);
}, [selectedDashboards, currentDashboardId]);
}, [links, currentDashboardId]);

const onButtonClick = useCallback(() => setIsEditPopoverOpen((isOpen) => !isOpen), []);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,23 +10,30 @@ import classNames from 'classnames';
import useAsync from 'react-use/lib/useAsync';
import React, { useEffect, useState } from 'react';

import { DashboardItem } from '@kbn/dashboard-plugin/common/content_management';
// import { isValidUrl } from '@kbn/triggers-actions-ui-plugin/public';
import {
EuiSpacer,
EuiHighlight,
EuiSelectable,
EuiLoadingSpinner,
EuiSelectableOption,
EuiFieldSearch,
EuiHighlight,
EuiSelectableOption,
} from '@elastic/eui';
import { createValidateUrl } from '@kbn/image-embeddable-plugin/public';

import { useNavigationEmbeddable } from '../embeddable/navigation_embeddable';
import { DashboardItem } from '../types';
import { coreServices } from '../services/navigation_embeddable_services';

interface Props {
onUrlSelected: (url: string) => void;
onDashboardSelected: (selectedDashboard: DashboardItem) => void;
}

export const NavigationEmbeddableDashboardList = ({ onDashboardSelected, ...other }: Props) => {
export const NavigationEmbeddableDashboardList = ({
onUrlSelected,
onDashboardSelected,
...other
}: Props) => {
const navEmbeddable = useNavigationEmbeddable();
const currentDashboardId = navEmbeddable.select(
(state) => state.componentState.currentDashboardId
Expand All @@ -42,12 +49,13 @@ export const NavigationEmbeddableDashboardList = ({ onDashboardSelected, ...othe
useEffect(() => {
const dashboardOptions =
dashboardList?.map((dashboard: DashboardItem) => {
const isCurrentDashboard = dashboard.id === currentDashboardId;
return {
data: dashboard, // just store the ID here - that's all that is necessary
data: dashboard, // TODO: just store the ID here - that's all that is necessary
className: classNames({
'navEmbeddable-currentDashboard': dashboard.id === currentDashboardId,
'navEmbeddable-currentDashboard': isCurrentDashboard,
}),
label: dashboard.attributes.title,
label: isCurrentDashboard ? 'Current' : dashboard.attributes.title,
} as EuiSelectableOption;
}) ?? [];
setDashboardListOptions(dashboardOptions);
Expand All @@ -60,7 +68,13 @@ export const NavigationEmbeddableDashboardList = ({ onDashboardSelected, ...othe
isClearable={true}
placeholder={'Search for a dashboard or enter external URL'}
onSearch={(value) => {
setSearchString(value);
const validateUrl = createValidateUrl(coreServices.http.externalUrl);
if (validateUrl(value).isValid) {
setSearchString('');
onUrlSelected(value);
} else {
setSearchString(value);
}
}}
/>
<EuiSpacer size="s" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ import {
EuiFlexGroup,
EuiFlexItem,
} from '@elastic/eui';
import { DashboardItem } from '@kbn/dashboard-plugin/common/content_management';

import { useNavigationEmbeddable } from '../embeddable/navigation_embeddable';
import { NavigationEmbeddableDashboardList } from './navigation_embeddable_dashboard_list';
import { DashboardItem } from '../types';

export const NavigationEmbeddableLinkEditor = ({
setIsPopoverOpen,
Expand All @@ -29,23 +29,27 @@ export const NavigationEmbeddableLinkEditor = ({
}) => {
const navEmbeddable = useNavigationEmbeddable();

const [linkLabel, setLinkLabel] = useState<string>('');
const [selectedUrl, setSelectedUrl] = useState<string>();
const [selectedDashboard, setSelectedDashboard] = useState<DashboardItem | undefined>();
const [dashboardLabel, setDashboardLabel] = useState<string>('');

return (
<>
<EuiForm component="form">
<EuiFormRow label="Choose destination">
<NavigationEmbeddableDashboardList onDashboardSelected={setSelectedDashboard} />
<NavigationEmbeddableDashboardList
onUrlSelected={setSelectedUrl}
onDashboardSelected={setSelectedDashboard}
/>
</EuiFormRow>
<EuiFormRow label="Text">
<EuiFieldText
placeholder={
selectedDashboard ? selectedDashboard.attributes.title : 'Enter text for link'
}
value={dashboardLabel}
value={linkLabel}
onChange={(e) => {
setDashboardLabel(e.target.value);
setLinkLabel(e.target.value);
}}
aria-label="Use aria labels when no actual label is in use"
/>
Expand All @@ -58,14 +62,16 @@ export const NavigationEmbeddableLinkEditor = ({
size="s"
onClick={() => {
if (selectedDashboard) {
navEmbeddable.dispatch.addLink({
label: dashboardLabel,
navEmbeddable.dispatch.addDashboardLink({
label: linkLabel,
id: selectedDashboard.id,
title: selectedDashboard.attributes.title,
description: selectedDashboard.attributes.description,
});
} else if (selectedUrl) {
navEmbeddable.dispatch.addExternalLink({ url: selectedUrl, label: linkLabel });
}
setDashboardLabel('');
setLinkLabel('');
setIsPopoverOpen(false);
}}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@ import { distinctUntilChanged, Subscription } from 'rxjs';
import { Embeddable } from '@kbn/embeddable-plugin/public';
import type { IContainer } from '@kbn/embeddable-plugin/public';
import { KibanaThemeProvider } from '@kbn/kibana-react-plugin/public';
import { DashboardItem } from '@kbn/dashboard-plugin/common/content_management';
import { DashboardAttributes } from '@kbn/dashboard-plugin/common/content_management';
import { DashboardContainer } from '@kbn/dashboard-plugin/public/dashboard_container';
import { ReduxEmbeddableTools, ReduxToolsPackage } from '@kbn/presentation-util-plugin/public';

import { navigationEmbeddableReducers } from '../navigation_embeddable_reducers';
import { NavigationEmbeddableInput, NavigationEmbeddableReduxState } from '../types';
import {
DashboardItem,
isDashboardLink,
NavigationEmbeddableInput,
NavigationEmbeddableReduxState,
} from '../types';
import { coreServices, dashboardServices } from '../services/navigation_embeddable_services';
import { NavigationEmbeddableComponent } from '../components/navigation_embeddable_component';

Expand Down Expand Up @@ -103,39 +108,68 @@ export class NavigationEmbeddable extends Embeddable<NavigationEmbeddableInput>
**/
this.subscriptions.add(
this.getInput$()
.pipe(distinctUntilChanged((a, b) => isEqual(a.dashboardLinks, b.dashboardLinks)))
.pipe(distinctUntilChanged((a, b) => isEqual(a.links, b.links)))
.subscribe(async () => {
await this.updateDashboardLinks();
})
);
}

private async updateDashboardLinks() {
const { dashboardLinks } = this.getInput();
const { links } = this.getInput();

if (dashboardLinks && !isEmpty(dashboardLinks)) {
if (links) {
this.dispatch.setLoading(true);
const findDashboardsService = await dashboardServices.findDashboardsService();
const responses = await findDashboardsService.findByIds(
dashboardLinks.map((link) => link.id)
);
const updatedDashboardLinks = responses.map((response, i) => {
if (response.status === 'error') {
throw new Error('failure');

// Get all of the dashboard IDs that are referenced so we can fetch their saved objects
const uniqueDashboardIds = new Set<string>();
Object.keys(links).forEach((linkId) => {
const link = links[linkId];
if (isDashboardLink(link)) {
uniqueDashboardIds.add(link.id);
}
return {
id: response.id,
label: dashboardLinks[i].label,
title: response.attributes.title,
description: response.attributes.description,
};
});

// Fetch the dashboard saved objects from their IDs and store the attributes
const dashboardAttributes: { [dashboardId: string]: DashboardAttributes } = {};
if (!isEmpty(uniqueDashboardIds)) {
const findDashboardsService = await dashboardServices.findDashboardsService();
const responses = await findDashboardsService.findByIds(Array.from(uniqueDashboardIds));
responses.forEach((response) => {
if (response.status === 'error') {
throw new Error('failure'); // TODO: better error handling
}
dashboardAttributes[response.id] = response.attributes;
});
}

// Convert the explicit input `links` object to a sorted array for component state
const sortedLinks = Object.keys(links)
.sort(function (a, b) {
return links[a].order - links[b].order;
})
.map((linkId) => {
const link = links[linkId];
if (isDashboardLink(link)) {
const dashboardId = link.id;
return {
id: dashboardId,
label: link.label,
order: link.order,
title: dashboardAttributes[dashboardId].title,
description: dashboardAttributes[dashboardId].description,
};
}
return link;
});

// Update component state to keep in sync with changes to explicit input
batch(() => {
this.dispatch.setDashboardLinks(updatedDashboardLinks);
this.dispatch.setLinks(sortedLinks);
this.dispatch.setLoading(false);
});
} else {
this.dispatch.setDashboardLinks([]);
this.dispatch.setLinks([]);
}
}

Expand All @@ -150,16 +184,32 @@ export class NavigationEmbeddable extends Embeddable<NavigationEmbeddableInput>
});

const { currentDashboardId } = this.getState().componentState;
const sortedDashboards = responses.hits.sort((hit) => {
return hit.id === currentDashboardId ? -1 : 1; // force the current dashboard to the top of the list - we might not actually want this ¯\_(ツ)_/¯
});
const sortedDashboards = responses.hits
.sort((hit) => {
return hit.id === currentDashboardId ? -1 : 1; // force the current dashboard to the top of the list - we might not actually want this ¯\_(ツ)_/¯
})
.map((hit) => {
return { id: hit.id, attributes: hit.attributes };
});

// TODO: make this nicer
if (isEmpty(search) && currentDashboardId && sortedDashboards[0].id !== currentDashboardId) {
const currentDashboard = (await findDashboardsService.findByIds([currentDashboardId]))[0];
if (currentDashboard.status === 'success') {
sortedDashboards.pop();
sortedDashboards.unshift({
id: currentDashboardId,
attributes: currentDashboard.attributes,
});
}
}

batch(() => {
this.dispatch.setDashboardList(sortedDashboards);
this.dispatch.setDashboardCount(responses.total);
});

return responses.hits;
return sortedDashboards;
}

public async reload() {
Expand Down
Loading

0 comments on commit 5768166

Please sign in to comment.