[Links] Fix link settings not persisting (#211041)

Closes https://github.com/elastic/kibana/issues/211022

## Summary

This PR fixes a bug with persisting Link options where, because we
weren't providing the "initial" state to the options editor, it was
always starting with the default state - therefore, editing something
**other** than the options would reset the link options back to the
default.

I tested this in `8.14` and the bug was present there, too - based on
[the file history / git
blame](https://github.com/elastic/kibana/blame/main/src/platform/plugins/private/links/public/components/editor/link_editor.tsx#L60),
this bug has been around from the very beginning 🙈

**Before:**


https://github.com/user-attachments/assets/360af02e-ae0f-470c-91a3-a52fc9b3d8c6


**After:**


https://github.com/user-attachments/assets/d1e93bfa-566a-4506-99e3-47f92c922d49



### Checklist

- [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
- [x] The PR description includes the appropriate Release Notes section,
and the correct `release_note:*` label is applied per the
[guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Hannah Mudge 2025-02-13 14:03:31 -07:00 committed by GitHub
parent 96b4f8442e
commit c6e6a77c54
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 109 additions and 4 deletions

View file

@ -0,0 +1,105 @@
/*
* 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { LinkEditor } from './link_editor';
jest.mock('./link_destination', () => {
// mock this component to prevent handleDestinationPicked from being called on mount
return { LinkDestination: () => <>LinkDestinationMock</> };
});
describe('LinksEditor', () => {
const nonDefaultOptions = {
openInNewTab: true,
useCurrentDateRange: false,
useCurrentFilters: false,
};
const defaultProps = {
link: {
id: 'foo',
type: 'dashboardLink' as const,
destination: '123',
title: 'dashboard 01',
},
parentDashboardId: 'test',
onSave: jest.fn(),
onClose: jest.fn(),
};
afterEach(() => {
jest.clearAllMocks();
});
const getOptionAriaChecked = (option: string): string | null => {
return screen
.getByTestId(`dashboardDrillDownOptions--${option}--checkbox`)
.getAttribute('aria-checked');
};
describe('dashboard link options', () => {
test('starts with default when options not provided', async () => {
render(<LinkEditor {...defaultProps} />);
await waitFor(() => {
expect(screen.queryByTestId('dashboardDrillDownOptions')).not.toBeNull(); // wait for lazy load
});
expect(getOptionAriaChecked('useCurrentFilters')).toBe('true');
expect(getOptionAriaChecked('useCurrentDateRange')).toBe('true');
expect(getOptionAriaChecked('openInNewTab')).toBe('false');
});
test('properly overrides default values when provided', async () => {
render(
<LinkEditor
{...defaultProps}
link={{
...defaultProps.link,
options: nonDefaultOptions,
}}
/>
);
await waitFor(() => {
expect(screen.queryByTestId('dashboardDrillDownOptions')).not.toBeNull(); // wait for lazy load
});
expect(getOptionAriaChecked('useCurrentFilters')).toBe('false');
expect(getOptionAriaChecked('useCurrentDateRange')).toBe('false');
expect(getOptionAriaChecked('openInNewTab')).toBe('true');
});
test('options are persisted on edit', async () => {
render(
<LinkEditor
{...defaultProps}
link={{
...defaultProps.link,
options: nonDefaultOptions,
}}
/>
);
await waitFor(() => {
expect(screen.queryByTestId('dashboardDrillDownOptions')).not.toBeNull(); // wait for lazy load
});
await userEvent.click(screen.getByTestId('links--linkEditor--linkLabel--input'));
await userEvent.keyboard('test label');
await userEvent.click(screen.getByTestId('links--linkEditor--saveBtn'));
expect(defaultProps.onSave).toBeCalledWith({
...defaultProps.link,
label: 'test label',
options: nonDefaultOptions,
});
});
});
});

View file

@ -54,10 +54,10 @@ export const LinkEditor = ({
const [selectedLinkType, setSelectedLinkType] = useState<LinkType>(
link?.type ?? DASHBOARD_LINK_TYPE
);
const [defaultLinkLabel, setDefaultLinkLabel] = useState<string | undefined>();
const [defaultLinkLabel, setDefaultLinkLabel] = useState<string | undefined>(link?.title);
const [currentLinkLabel, setCurrentLinkLabel] = useState<string>(link?.label ?? '');
const [linkDescription, setLinkDescription] = useState<string | undefined>();
const [linkOptions, setLinkOptions] = useState<LinkOptions | undefined>();
const [linkDescription, setLinkDescription] = useState<string | undefined>(link?.description);
const [linkOptions, setLinkOptions] = useState<LinkOptions | undefined>(link?.options);
const [linkDestination, setLinkDestination] = useState<string | undefined>(link?.destination);
const linkTypes: EuiRadioGroupOption[] = useMemo(() => {

View file

@ -24,7 +24,7 @@ export const DashboardDrilldownOptionsComponent = ({
}: DashboardDrilldownOptionsProps) => {
return (
<>
<EuiFormRow>
<EuiFormRow data-test-subj="dashboardDrillDownOptions">
<div>
<EuiSwitch
compressed