PanelDataErrorView.tsx 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. import { css } from '@emotion/css';
  2. import React from 'react';
  3. import { useDispatch } from 'react-redux';
  4. import { CoreApp, GrafanaTheme2, PanelDataSummary, VisualizationSuggestionsBuilder } from '@grafana/data';
  5. import { PanelDataErrorViewProps } from '@grafana/runtime';
  6. import { usePanelContext, useStyles2 } from '@grafana/ui';
  7. import { CardButton } from 'app/core/components/CardButton';
  8. import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants';
  9. import store from 'app/core/store';
  10. import { toggleVizPicker } from 'app/features/dashboard/components/PanelEditor/state/reducers';
  11. import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types';
  12. import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
  13. import { changePanelPlugin } from '../state/actions';
  14. export function PanelDataErrorView(props: PanelDataErrorViewProps) {
  15. const styles = useStyles2(getStyles);
  16. const context = usePanelContext();
  17. const builder = new VisualizationSuggestionsBuilder(props.data);
  18. const { dataSummary } = builder;
  19. const message = getMessageFor(props, dataSummary);
  20. const dispatch = useDispatch();
  21. const openVizPicker = () => {
  22. store.setObject(LS_VISUALIZATION_SELECT_TAB_KEY, VisualizationSelectPaneTab.Suggestions);
  23. dispatch(toggleVizPicker(true));
  24. };
  25. const switchToTable = () => {
  26. const panel = getDashboardSrv().getCurrent()?.getPanelById(props.panelId);
  27. if (!panel) {
  28. return;
  29. }
  30. dispatch(
  31. changePanelPlugin({
  32. panel,
  33. pluginId: 'table',
  34. })
  35. );
  36. };
  37. return (
  38. <div className={styles.wrapper}>
  39. <div className={styles.message}>{message}</div>
  40. {context.app === CoreApp.PanelEditor && dataSummary.hasData && (
  41. <div className={styles.actions}>
  42. <CardButton icon="table" onClick={switchToTable}>
  43. Switch to table
  44. </CardButton>
  45. <CardButton icon="chart-line" onClick={openVizPicker}>
  46. Open visualization suggestions
  47. </CardButton>
  48. </div>
  49. )}
  50. </div>
  51. );
  52. }
  53. function getMessageFor(
  54. { data, fieldConfig, message, needsNumberField, needsTimeField, needsStringField }: PanelDataErrorViewProps,
  55. dataSummary: PanelDataSummary
  56. ): string {
  57. if (message) {
  58. return message;
  59. }
  60. // In some cases there is a data frame but with no fields
  61. if (!data.series || data.series.length === 0 || (data.series.length === 1 && data.series[0].length === 0)) {
  62. return fieldConfig?.defaults.noValue ?? 'No data';
  63. }
  64. if (needsStringField && !dataSummary.hasStringField) {
  65. return 'Data is missing a string field';
  66. }
  67. if (needsNumberField && !dataSummary.hasNumberField) {
  68. return 'Data is missing a number field';
  69. }
  70. if (needsTimeField && !dataSummary.hasTimeField) {
  71. return 'Data is missing a time field';
  72. }
  73. return 'Cannot visualize data';
  74. }
  75. const getStyles = (theme: GrafanaTheme2) => {
  76. return {
  77. wrapper: css({
  78. display: 'flex',
  79. flexDirection: 'column',
  80. justifyContent: 'center',
  81. alignItems: 'center',
  82. height: '100%',
  83. width: '100%',
  84. }),
  85. message: css({
  86. textAlign: 'center',
  87. color: theme.colors.text.secondary,
  88. fontSize: theme.typography.size.lg,
  89. width: '100%',
  90. }),
  91. actions: css({
  92. marginTop: theme.spacing(2),
  93. display: 'flex',
  94. height: '50%',
  95. maxHeight: '150px',
  96. columnGap: theme.spacing(1),
  97. rowGap: theme.spacing(1),
  98. width: '100%',
  99. maxWidth: '600px',
  100. }),
  101. };
  102. };