Skip to content

Commit 9d3f206

Browse files
nickofthymeNicholasPeretti
authored andcommitted
[Visualize] Improve list error handling (elastic#238355)
Fixes an error in the **Visualize Listing** page in which an error in the vis could cause the entire page to error. This improves the error handling to make it easier to identity which visualization is causing the problem in order to address it.
1 parent 59cce68 commit 9d3f206

File tree

5 files changed

+212
-84
lines changed

5 files changed

+212
-84
lines changed

src/platform/plugins/shared/visualizations/public/vis_types/vis_type_alias_registry.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ export interface VisualizationListItem {
2424
error?: string;
2525
icon: string;
2626
id: string;
27-
stage: VisualizationStage;
27+
stage?: VisualizationStage;
2828
savedObjectType: string;
2929
title: string;
3030
description?: string;
3131
getSupportedTriggers?: () => string[];
3232
typeTitle: string;
3333
image?: string;
3434
type?: BaseVisType | string;
35-
editor:
35+
editor?:
3636
| { editUrl: string; editApp?: string }
3737
| { onEdit: (savedObjectId: string) => Promise<void> };
3838
}

src/platform/plugins/shared/visualizations/public/visualize_app/components/visualize_listing.tsx

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ import type { VisualizationListItem } from '../..';
4343
import type { VisualizeServices } from '../types';
4444
import { VisualizeConstants } from '../../../common/constants';
4545
import { getNoItemsMessage, getCustomColumn, getCustomSortingOptions } from '../utils';
46-
import { getVisualizeListItemLink } from '../utils/get_visualize_list_item_link';
46+
import { getVisualizeListItemLinkFn } from '../utils/get_visualize_list_item_link';
4747
import type { VisualizationStage } from '../../vis_types/vis_type_alias_registry';
4848

4949
const visualizeListingStyles = {
@@ -72,7 +72,7 @@ const visualizeListingStyles = {
7272
`,
7373
};
7474

75-
type VisualizeUserContent = VisualizationListItem &
75+
export type VisualizeUserContent = VisualizationListItem &
7676
UserContentCommonSchema & {
7777
type: string;
7878
attributes: {
@@ -142,7 +142,7 @@ const useTableListViewProps = (
142142
}, [closeNewVisModal]);
143143

144144
const editItem = useCallback(
145-
async ({ attributes: { id }, editor }: VisualizeUserContent) => {
145+
async ({ attributes: { id }, editor = { editUrl: '' } }: VisualizeUserContent) => {
146146
if (!('editApp' in editor || 'editUrl' in editor)) {
147147
await editor.onEdit(id);
148148
return;
@@ -372,6 +372,11 @@ export const VisualizeListing = () => {
372372
});
373373
useUnmount(() => closeNewVisModal.current());
374374

375+
const getVisualizeListItemLink = useMemo(
376+
() => getVisualizeListItemLinkFn(application, kbnUrlStateStorage),
377+
[application, kbnUrlStateStorage]
378+
);
379+
375380
const listingLimit = uiSettings.get(SAVED_OBJECTS_LIMIT_SETTING);
376381
const initialPageSize = uiSettings.get(SAVED_OBJECTS_PER_PAGE_SETTING);
377382

@@ -434,19 +439,11 @@ export const VisualizeListing = () => {
434439
defaultMessage: 'visualizations',
435440
})}
436441
getOnClickTitle={(item) =>
437-
item.attributes.readOnly ? undefined : () => tableViewProps.editItem?.(item)
438-
}
439-
getDetailViewLink={({ editor, attributes: { error, readOnly } }) =>
440-
readOnly || (editor && 'onEdit' in editor)
442+
item.attributes.readOnly || item.error
441443
? undefined
442-
: getVisualizeListItemLink(
443-
application,
444-
kbnUrlStateStorage,
445-
editor.editApp,
446-
editor.editUrl,
447-
error
448-
)
444+
: () => tableViewProps.editItem?.(item)
449445
}
446+
getDetailViewLink={getVisualizeListItemLink}
450447
tableCaption={visualizeLibraryTitle}
451448
{...tableViewProps}
452449
{...propsFromParent}
@@ -456,13 +453,14 @@ export const VisualizeListing = () => {
456453
),
457454
};
458455
}, [
456+
styles.calloutLink,
457+
styles.table,
459458
application,
460459
dashboardCapabilities.createNew,
461460
initialPageSize,
462-
kbnUrlStateStorage,
463-
tableViewProps,
464461
visualizeLibraryTitle,
465-
styles,
462+
tableViewProps,
463+
getVisualizeListItemLink,
466464
]);
467465

468466
const tabs = useMemo(

src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_table_columns.tsx

Lines changed: 54 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
*/
99

1010
import React from 'react';
11-
import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiBadge } from '@elastic/eui';
11+
import { EuiBetaBadge, EuiButton, EuiEmptyPrompt, EuiIcon, EuiToolTip } from '@elastic/eui';
1212
import { i18n } from '@kbn/i18n';
1313
import { FormattedMessage } from '@kbn/i18n-react';
1414
import type { CustomSortingOptions } from '@kbn/content-management-table-list-view-table';
@@ -49,23 +49,20 @@ const getBadge = (item: VisualizationListItem) => {
4949
};
5050

5151
const renderItemTypeIcon = (item: VisualizationListItem) => {
52-
let icon;
5352
if (item.image) {
54-
icon = (
53+
return (
5554
<img className="visListingTable__typeImage" aria-hidden="true" alt="" src={item.image} />
5655
);
57-
} else {
58-
icon = (
59-
<EuiIcon
60-
className="visListingTable__typeIcon"
61-
aria-hidden="true"
62-
type={item.icon || 'empty'}
63-
size="m"
64-
/>
65-
);
6656
}
6757

68-
return icon;
58+
return (
59+
<EuiIcon
60+
className="visListingTable__typeIcon"
61+
aria-hidden="true"
62+
type={item.icon || 'empty'}
63+
size="m"
64+
/>
65+
);
6966
};
7067

7168
export const getCustomColumn = () => {
@@ -76,18 +73,50 @@ export const getCustomColumn = () => {
7673
}),
7774
sortable: true,
7875
width: '150px',
79-
render: (field: string, record: VisualizationListItem) =>
80-
!record.error ? (
81-
<span>
82-
{renderItemTypeIcon(record)}
83-
{record.typeTitle}
84-
{getBadge(record)}
85-
</span>
86-
) : (
87-
<EuiBadge iconType="warning" color="warning">
88-
{record.error}
89-
</EuiBadge>
90-
),
76+
render: (field: string, record: VisualizationListItem) => {
77+
if (!record.error) {
78+
return (
79+
<span>
80+
{renderItemTypeIcon(record)}
81+
{record.typeTitle}
82+
{getBadge(record)}
83+
</span>
84+
);
85+
}
86+
87+
if (!record.typeTitle) {
88+
return (
89+
<EuiToolTip position="left" content={record.error}>
90+
<span>
91+
<EuiIcon
92+
className="visListingTable__typeIcon"
93+
aria-hidden="true"
94+
color="warning"
95+
type="warning"
96+
size="m"
97+
/>
98+
<FormattedMessage id="visualizations.listing.type.unknown" defaultMessage="Unknown" />
99+
</span>
100+
</EuiToolTip>
101+
);
102+
}
103+
104+
// We should have a way to display generic item errors from TableListViewTable
105+
return (
106+
<EuiToolTip position="left" content={record.error}>
107+
<span>
108+
<EuiIcon
109+
className="visListingTable__typeIcon"
110+
aria-hidden="true"
111+
color="danger"
112+
type="error"
113+
size="m"
114+
/>
115+
<FormattedMessage id="visualizations.listing.type.error" defaultMessage="Error" />
116+
</span>
117+
</EuiToolTip>
118+
);
119+
},
91120
};
92121
};
93122

src/platform/plugins/shared/visualizations/public/visualize_app/utils/get_visualize_list_item_link.test.ts

Lines changed: 114 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,35 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import { getVisualizeListItemLink } from './get_visualize_list_item_link';
10+
import { getVisualizeListItemLinkFn } from './get_visualize_list_item_link';
1111
import type { ApplicationStart } from '@kbn/core/public';
1212
import { createHashHistory } from 'history';
1313
import { FilterStateStore } from '@kbn/es-query';
1414
import { createKbnUrlStateStorage } from '@kbn/kibana-utils-plugin/public';
1515
import { GLOBAL_STATE_STORAGE_KEY } from '../../../common/constants';
16+
import type { VisualizeUserContent } from '../components/visualize_listing';
17+
18+
const mockItem: VisualizeUserContent = {
19+
id: '9886b410-4c8b-11e8-b3d7-01146121b73d',
20+
updatedAt: '2025-10-09T20:35:02.807Z',
21+
managed: false,
22+
references: [],
23+
type: 'visualization',
24+
icon: 'visBarVertical',
25+
savedObjectType: 'visualization',
26+
typeTitle: 'Vertical bar',
27+
title: '[Flights] Delay Buckets',
28+
error: '',
29+
editor: {
30+
editUrl: '/edit/9886b410-4c8b-11e8-b3d7-01146121b73d',
31+
},
32+
attributes: {
33+
id: '9886b410-4c8b-11e8-b3d7-01146121b73d',
34+
title: '[Flights] Delay Buckets',
35+
description: '',
36+
readOnly: false,
37+
},
38+
};
1639

1740
jest.mock('../../services', () => {
1841
return {
@@ -36,17 +59,74 @@ const kbnUrlStateStorage = createKbnUrlStateStorage({
3659
kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { time: { from: 'now-7d', to: 'now' } });
3760

3861
describe('listing item link is correct for each app', () => {
62+
const getVisualizeListItemLink = getVisualizeListItemLinkFn(application, kbnUrlStateStorage);
63+
64+
test('returns undefined if readOnly', async () => {
65+
const testItem: VisualizeUserContent = {
66+
...mockItem,
67+
attributes: {
68+
...mockItem.attributes,
69+
readOnly: true,
70+
},
71+
};
72+
const url = getVisualizeListItemLink(testItem);
73+
expect(url).toBe(undefined);
74+
});
75+
76+
test('returns undefined if has error', async () => {
77+
const testItem: VisualizeUserContent = {
78+
...mockItem,
79+
attributes: {
80+
...mockItem.attributes,
81+
error: 'error here',
82+
},
83+
};
84+
const url = getVisualizeListItemLink(testItem);
85+
expect(url).toBe(undefined);
86+
});
87+
88+
test('returns undefined if onEdit is in editor', async () => {
89+
const testItem: VisualizeUserContent = {
90+
...mockItem,
91+
editor: { onEdit: async () => {} },
92+
};
93+
const url = getVisualizeListItemLink(testItem);
94+
expect(url).toBe(undefined);
95+
});
96+
97+
test('returns undefined if no editor', async () => {
98+
const testItem: VisualizeUserContent = {
99+
...mockItem,
100+
editor: undefined,
101+
};
102+
const url = getVisualizeListItemLink(testItem);
103+
expect(url).toBe(undefined);
104+
});
105+
39106
test('creates a link to classic visualization if editApp is not defined', async () => {
40107
const editUrl = 'edit/id';
41-
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, undefined, editUrl);
42-
expect(url).toMatchInlineSnapshot(`"/app/visualize#${editUrl}?_g=(time:(from:now-7d,to:now))"`);
108+
const testItem: VisualizeUserContent = {
109+
...mockItem,
110+
editor: {
111+
editUrl,
112+
},
113+
};
114+
const url = getVisualizeListItemLink(testItem);
115+
expect(url).toBe(`/app/visualize#${editUrl}?_g=(time:(from:now-7d,to:now))`);
43116
});
44117

45118
test('creates a link for the app given if editApp is defined', async () => {
46119
const editUrl = '#/edit/id';
47120
const editApp = 'lens';
48-
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl);
49-
expect(url).toMatchInlineSnapshot(`"/app/${editApp}${editUrl}?_g=(time:(from:now-7d,to:now))"`);
121+
const testItem: VisualizeUserContent = {
122+
...mockItem,
123+
editor: {
124+
editUrl,
125+
editApp,
126+
},
127+
};
128+
const url = getVisualizeListItemLink(testItem);
129+
expect(url).toBe(`/app/${editApp}${editUrl}?_g=(time:(from:now-7d,to:now))`);
50130
});
51131

52132
describe('when global time changes', () => {
@@ -62,9 +142,16 @@ describe('listing item link is correct for each app', () => {
62142
test('it propagates the correct time on the query', async () => {
63143
const editUrl = '#/edit/id';
64144
const editApp = 'lens';
65-
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl);
66-
expect(url).toMatchInlineSnapshot(
67-
`"/app/${editApp}${editUrl}?_g=(time:(from:'2021-01-05T11:45:53.375Z',to:'2021-01-21T11:46:00.990Z'))"`
145+
const testItem: VisualizeUserContent = {
146+
...mockItem,
147+
editor: {
148+
editUrl,
149+
editApp,
150+
},
151+
};
152+
const url = getVisualizeListItemLink(testItem);
153+
expect(url).toBe(
154+
`/app/${editApp}${editUrl}?_g=(time:(from:'2021-01-05T11:45:53.375Z',to:'2021-01-21T11:46:00.990Z'))`
68155
);
69156
});
70157
});
@@ -79,10 +166,15 @@ describe('listing item link is correct for each app', () => {
79166
test('it propagates the refreshInterval on the query', async () => {
80167
const editUrl = '#/edit/id';
81168
const editApp = 'lens';
82-
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl);
83-
expect(url).toMatchInlineSnapshot(
84-
`"/app/${editApp}${editUrl}?_g=(refreshInterval:(pause:!f,value:300))"`
85-
);
169+
const testItem: VisualizeUserContent = {
170+
...mockItem,
171+
editor: {
172+
editUrl,
173+
editApp,
174+
},
175+
};
176+
const url = getVisualizeListItemLink(testItem);
177+
expect(url).toBe(`/app/${editApp}${editUrl}?_g=(refreshInterval:(pause:!f,value:300))`);
86178
});
87179
});
88180

@@ -117,9 +209,16 @@ describe('listing item link is correct for each app', () => {
117209
test('propagates the filters on the query', async () => {
118210
const editUrl = '#/edit/id';
119211
const editApp = 'lens';
120-
const url = getVisualizeListItemLink(application, kbnUrlStateStorage, editApp, editUrl);
121-
expect(url).toMatchInlineSnapshot(
122-
`"/app/${editApp}${editUrl}?_g=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1)),('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))))"`
212+
const testItem: VisualizeUserContent = {
213+
...mockItem,
214+
editor: {
215+
editUrl,
216+
editApp,
217+
},
218+
};
219+
const url = getVisualizeListItemLink(testItem);
220+
expect(url).toBe(
221+
`/app/${editApp}${editUrl}?_g=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1)),('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))))`
123222
);
124223
});
125224
});

0 commit comments

Comments
 (0)