Skip to content

Commit

Permalink
[Dashboard Navigation] Make links panel available under technical pre…
Browse files Browse the repository at this point in the history
…view (#166896)

## Summary
This PR wraps up the work the @elastic/kibana-presentation team has done
to finish the MVP of [Phase
1](#154354) of the `Link`
embeddable, which enables users to add panels to their dashboard that
contain links to other dashboards + external links - with respect to
dashboard links, we give the author control over which pieces of context
should be kept across dashboards so that things like filter pills,
queries, and time ranges are not lost. This marks a huge improvement in
dashboard navigation overall, which was previously only available via a
variety of different workarounds including (but not limited to):
- Creating (essentially) a `noop` dashboard-to-dashboard drilldown 
- Using markdown panels with hard Dashboard links, which are prone to
break across updates
- Avoiding navigation all together, which resulted in large,
slow-to-load dashboards.

As an added benefit, because these panels contain **references** to each
dashboard rather than hard links, (1) unlike markdown links, they should
not break after updates and (2) if a links panel is exported and
imported into another space or instance, all of the dashboards it links
to will also be imported.



https://github.com/elastic/kibana/assets/8698078/1a86b713-47e7-4db9-8a04-29d41b13681a

> **Note**
> 🔉 The above video has audio! Turn on your sound for the best
experience.

### Note about this PR
- A majority of this work was done on a feature branch, with thorough
reviews from @andreadelrio on behalf of @elastic/kibana-design along the
way. Therefore, while feedback on the design is encouraged, any large
concerns brought up in this PR should be filed as separate issues and
addressed in follow-up PRs.
- This PR contains work for giving embeddables control over their own
panel size / default positioning on the dashboard. This was especially
important for the links panel, since we assume that (a) most links
panels would be located somewhere near the top of the dashboard and (b)
the horizontal links panel should have a different default "shape"
(longer than it is tall) than the vertical panel (taller than it is
long).
- This PR also contains work for caching dashboard saved objects, which
makes navigation much more seamless.

### Flaky Test Runner
-
https://buildkite.com/elastic/kibana-flaky-test-suite-runner/builds/3251


![image](https://github.com/elastic/kibana/assets/8698078/7616443e-0cb0-43ce-a1d0-41f8bee6cbfc)


### 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)
- [ ]
~[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials~ This will
be addressed in a follow up:
#166750
- [x] [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 - ~Units tests
are added, functional tests are forthcoming~ Edit: All tests are in.
- [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: Nick Peihl <[email protected]>
Co-authored-by: kibanamachine <[email protected]>
Co-authored-by: Andrea Del Rio <[email protected]>
Co-authored-by: Devon Thomson <[email protected]>
Co-authored-by: Nick Peihl <[email protected]>
Co-authored-by: Gerard Soldevila <[email protected]>
  • Loading branch information
7 people authored Sep 29, 2023
1 parent 9d3213e commit 9e8312f
Show file tree
Hide file tree
Showing 187 changed files with 7,203 additions and 758 deletions.
1 change: 1 addition & 0 deletions .buildkite/ftr_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ enabled:
- test/functional/apps/dashboard_elements/controls/options_list/config.ts
- test/functional/apps/dashboard_elements/image_embeddable/config.ts
- test/functional/apps/dashboard_elements/input_control_vis/config.ts
- test/functional/apps/dashboard_elements/links/config.ts
- test/functional/apps/dashboard_elements/markdown/config.ts
- test/functional/apps/dashboard/group1/config.ts
- test/functional/apps/dashboard/group2/config.ts
Expand Down
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,7 @@ x-pack/plugins/lens @elastic/kibana-visualizations
x-pack/plugins/license_api_guard @elastic/platform-deployment-management
x-pack/plugins/license_management @elastic/platform-deployment-management
x-pack/plugins/licensing @elastic/kibana-core
src/plugins/links @elastic/kibana-presentation
packages/kbn-lint-packages-cli @elastic/kibana-operations
packages/kbn-lint-ts-projects-cli @elastic/kibana-operations
x-pack/plugins/lists @elastic/security-detection-engine
Expand Down
1 change: 1 addition & 0 deletions .i18nrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
],
"monaco": "packages/kbn-monaco/src",
"navigation": "src/plugins/navigation",
"links": "src/plugins/links",
"newsfeed": "src/plugins/newsfeed",
"presentationUtil": "src/plugins/presentation_util",
"randomSampling": "x-pack/packages/kbn-random-sampling",
Expand Down
4 changes: 4 additions & 0 deletions docs/developer/plugin-list.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,10 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel.
|Utilities for building Kibana plugins.
|{kib-repo}blob/{branch}/src/plugins/links/README.md[links]
|This plugin adds the Links panel which allows authors to create hard links to navigate on click and bring all context from the source dashboard to the destination dashboard.
|{kib-repo}blob/{branch}/src/plugins/management/README.md[management]
|This plugins contains the "Stack Management" page framework. It offers navigation and an API
to link individual management section into it. This plugin does not contain any individual
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@
"@kbn/license-api-guard-plugin": "link:x-pack/plugins/license_api_guard",
"@kbn/license-management-plugin": "link:x-pack/plugins/license_management",
"@kbn/licensing-plugin": "link:x-pack/plugins/licensing",
"@kbn/links-plugin": "link:src/plugins/links",
"@kbn/lists-plugin": "link:x-pack/plugins/lists",
"@kbn/locator-examples-plugin": "link:examples/locator_examples",
"@kbn/locator-explorer-plugin": "link:examples/locator_explorer",
Expand Down
18 changes: 18 additions & 0 deletions packages/kbn-check-mappings-update-cli/current_mappings.json
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,24 @@
}
}
},
"links": {
"dynamic": false,
"properties": {
"id": {
"type": "text"
},
"title": {
"type": "text"
},
"description": {
"type": "text"
},
"links": {
"dynamic": false,
"properties": {}
}
}
},
"lens": {
"properties": {
"title": {
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-optimizer/limits.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ pageLoadAssetSize:
lens: 38000
licenseManagement: 41817
licensing: 29004
links: 44490
lists: 22900
logExplorer: 39045
logsShared: 281060
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ const STANDARD_LIST_TYPES = [
'dashboard',
'search',
'lens',
'links',
'map',
'cases',
// synthetics based objects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
"legacy-url-alias": "9b8cca3fbb2da46fd12823d3cd38fdf1c9f24bc8",
"lens": "5cfa2c52b979b4f8df56dd13c477e152183468b9",
"lens-ui-telemetry": "8c47a9e393861f76e268345ecbadfc8a5fb1e0bd",
"links": "39117a08966e9082d0f47b0b2e7e508499fc1e6d",
"maintenance-window": "d893544460abad56ff7a0e25b78f78776dfe10d1",
"map": "76c71023bd198fb6b1163b31bafd926fe2ceb9da",
"metrics-data-source": "81b69dc9830699d9ead5ac8dcb9264612e2a3c89",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ const previouslyRegisteredTypes = [
'legacy-url-alias',
'lens',
'lens-ui-telemetry',
'links',
'maintenance-window',
'map',
'maps-telemetry',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,7 @@ describe('split .kibana index into multiple system indices', () => {
"legacy-url-alias",
"lens",
"lens-ui-telemetry",
"links",
"maintenance-window",
"map",
"metrics-data-source",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ export const EditControlFlyout = ({
}

closeFlyout();
await controlGroup.replaceEmbeddable(embeddable.id, inputToReturn, type);
if (panel.type === type) {
controlGroup.updateInputForChild(embeddable.id, inputToReturn);
} else {
await controlGroup.replaceEmbeddable(embeddable.id, inputToReturn, type);
}
};

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,22 +222,22 @@ export class ControlGroupContainer extends Container<

public async addDataControlFromField(controlProps: AddDataControlProps) {
const panelState = await getDataControlPanelState(this.getInput(), controlProps);
return this.createAndSaveEmbeddable(panelState.type, panelState);
return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels);
}

public addOptionsListControl(controlProps: AddOptionsListControlProps) {
const panelState = getOptionsListPanelState(this.getInput(), controlProps);
return this.createAndSaveEmbeddable(panelState.type, panelState);
return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels);
}

public addRangeSliderControl(controlProps: AddRangeSliderControlProps) {
const panelState = getRangeSliderPanelState(this.getInput(), controlProps);
return this.createAndSaveEmbeddable(panelState.type, panelState);
return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels);
}

public addTimeSliderControl() {
const panelState = getTimeSliderPanelState(this.getInput());
return this.createAndSaveEmbeddable(panelState.type, panelState);
return this.createAndSaveEmbeddable(panelState.type, panelState, this.getInput().panels);
}

public openAddDataControlFlyout = openAddDataControlFlyout;
Expand Down Expand Up @@ -283,15 +283,19 @@ export class ControlGroupContainer extends Container<

protected createNewPanelState<TEmbeddableInput extends ControlInput = ControlInput>(
factory: EmbeddableFactory<ControlInput, ControlOutput, ControlEmbeddable>,
partial: Partial<TEmbeddableInput> = {}
): ControlPanelState<TEmbeddableInput> {
const panelState = super.createNewPanelState(factory, partial);
partial: Partial<TEmbeddableInput> = {},
otherPanels: ControlGroupInput['panels']
) {
const { newPanel } = super.createNewPanelState(factory, partial);
return {
order: getNextPanelOrder(this.getInput().panels),
width: this.getInput().defaultControlWidth,
grow: this.getInput().defaultControlGrow,
...panelState,
} as ControlPanelState<TEmbeddableInput>;
newPanel: {
order: getNextPanelOrder(this.getInput().panels),
width: this.getInput().defaultControlWidth,
grow: this.getInput().defaultControlGrow,
...newPanel,
} as ControlPanelState<TEmbeddableInput>,
otherPanels,
};
}

protected onRemoveEmbeddable(idToRemove: string) {
Expand Down
12 changes: 9 additions & 3 deletions src/plugins/dashboard/jest_setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@
* Side Public License, v 1.
*/

import { pluginServices } from './public/services/plugin_services';
import { registry } from './public/services/plugin_services.stub';
import { setStubDashboardServices } from './public/services/mocks';

pluginServices.setRegistry(registry.start({}));
/**
* CAUTION: Be very mindful of the things you import in to this `jest_setup` file - anything that is imported
* here (either directly or implicitly through dependencies) will be **unable** to be mocked elsewhere!
*
* Refer to the "Caution" section here:
* https://jestjs.io/docs/jest-object#jestmockmodulename-factory-options
*/
setStubDashboardServices();
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,13 @@ beforeEach(async () => {
.fn()
.mockReturnValue(mockEmbeddableFactory);
container = buildMockDashboard({
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Kibanana', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
}),
overrides: {
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Kibanana', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
}),
},
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,9 @@ import { Action, IncompatibleActionError } from '@kbn/ui-actions-plugin/public';

import { type DashboardPanelState } from '../../common';
import { pluginServices } from '../services/plugin_services';
import { createPanelState } from '../dashboard_container/component/panel';
import { dashboardClonePanelActionStrings } from './_dashboard_actions_strings';
import { placeClonePanel } from '../dashboard_container/component/panel_placement';
import { DASHBOARD_CONTAINER_TYPE, type DashboardContainer } from '../dashboard_container';
import { placePanelBeside } from '../dashboard_container/component/panel/dashboard_panel_placement';

export const ACTION_CLONE_PANEL = 'clonePanel';

Expand Down Expand Up @@ -82,6 +81,7 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
throw new PanelNotFoundError();
}

// Clone panel input
const clonedPanelState: PanelState<EmbeddableInput> = await (async () => {
const newTitle = await this.getCloneTitle(embeddable, embeddable.getTitle() || '');
const id = uuidv4();
Expand Down Expand Up @@ -110,18 +110,20 @@ export class ClonePanelAction implements Action<ClonePanelActionContext> {
'data-test-subj': 'addObjectToContainerSuccess',
});

const { otherPanels, newPanel } = createPanelState(
clonedPanelState,
dashboard.getInput().panels,
placePanelBeside,
{
width: panelToClone.gridData.w,
height: panelToClone.gridData.h,
currentPanels: dashboard.getInput().panels,
placeBesideId: panelToClone.explicitInput.id,
scrollToPanel: true,
}
);
const { newPanelPlacement, otherPanels } = placeClonePanel({
width: panelToClone.gridData.w,
height: panelToClone.gridData.h,
currentPanels: dashboard.getInput().panels,
placeBesideId: panelToClone.explicitInput.id,
});

const newPanel = {
...clonedPanelState,
gridData: {
...newPanelPlacement,
i: clonedPanelState.explicitInput.id,
},
};

dashboard.updateInput({
panels: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ pluginServices.getServices().embeddable.getEmbeddableFactory = jest

beforeEach(async () => {
container = buildMockDashboard({
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Sam', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
}),
overrides: {
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Sam', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
}),
},
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ describe('Export CSV action', () => {
};

container = buildMockDashboard({
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Kibanana', id: '123' },
type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE,
}),
overrides: {
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Kibanana', id: '123' },
type: CONTACT_CARD_EXPORTABLE_EMBEDDABLE,
}),
},
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,13 @@ let container: DashboardContainer;
let embeddable: ContactCardEmbeddable;
beforeEach(async () => {
container = buildMockDashboard({
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Sam', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
}),
overrides: {
panels: {
'123': getSampleDashboardPanel<ContactCardEmbeddableInput>({
explicitInput: { firstName: 'Sam', id: '123' },
type: CONTACT_CARD_EMBEDDABLE,
}),
},
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
} from '@kbn/embeddable-plugin/public';
import { Toast } from '@kbn/core/public';

import { DashboardPanelState } from '../../common';
import { pluginServices } from '../services/plugin_services';
import { dashboardReplacePanelActionStrings } from './_dashboard_actions_strings';
import { DashboardContainer } from '../dashboard_container';
Expand Down Expand Up @@ -58,30 +57,15 @@ export class ReplacePanelFlyout extends React.Component<Props> {

public onReplacePanel = async (savedObjectId: string, type: string, name: string) => {
const { panelToRemove, container } = this.props;
const { w, h, x, y } = (container.getInput().panels[panelToRemove.id] as DashboardPanelState)
.gridData;

const { id } = await container.addNewEmbeddable<SavedObjectEmbeddableInput>(type, {
savedObjectId,
});

const { [panelToRemove.id]: omit, ...panels } = container.getInput().panels;

container.updateInput({
panels: {
...panels,
[id]: {
...panels[id],
gridData: {
...(panels[id] as DashboardPanelState).gridData,
w,
h,
x,
y,
},
} as DashboardPanelState,
const id = await container.replaceEmbeddable<SavedObjectEmbeddableInput>(
panelToRemove.id,
{
savedObjectId,
},
});
type,
true
);

(container as DashboardContainer).setHighlightPanelId(id);
this.showToast(name);
Expand Down
Loading

0 comments on commit 9e8312f

Please sign in to comment.