207 lines
6.4 KiB
TypeScript
207 lines
6.4 KiB
TypeScript
import { css } from '@emotion/css';
|
|
|
|
import {
|
|
CoreApp,
|
|
FieldType,
|
|
getPanelDataSummary,
|
|
GrafanaTheme2,
|
|
PanelData,
|
|
PanelDataSummary,
|
|
PanelPluginVisualizationSuggestion,
|
|
} from '@grafana/data';
|
|
import { selectors } from '@grafana/e2e-selectors';
|
|
import { t, Trans } from '@grafana/i18n';
|
|
import { PanelDataErrorViewProps, locationService, config } from '@grafana/runtime';
|
|
import { VizPanel } from '@grafana/scenes';
|
|
import { Icon, usePanelContext, useStyles2 } from '@grafana/ui';
|
|
import { CardButton } from 'app/core/components/CardButton';
|
|
import { LS_VISUALIZATION_SELECT_TAB_KEY } from 'app/core/constants';
|
|
import store from 'app/core/store';
|
|
import { toggleVizPicker } from 'app/features/dashboard/components/PanelEditor/state/reducers';
|
|
import { VisualizationSelectPaneTab } from 'app/features/dashboard/components/PanelEditor/types';
|
|
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
|
import { DashboardScene } from 'app/features/dashboard-scene/scene/DashboardScene';
|
|
import { findVizPanelByKey, getVizPanelKeyForPanelId } from 'app/features/dashboard-scene/utils/utils';
|
|
import { useDispatch } from 'app/types/store';
|
|
|
|
import { changePanelPlugin } from '../state/actions';
|
|
import { hasData } from '../suggestions/utils';
|
|
|
|
function hasNoQueryConfigured(data: PanelData): boolean {
|
|
return !data.request?.targets || data.request.targets.length === 0;
|
|
}
|
|
|
|
export function PanelDataErrorView(props: PanelDataErrorViewProps) {
|
|
const styles = useStyles2(getStyles);
|
|
const context = usePanelContext();
|
|
const dataSummary = getPanelDataSummary(props.data.series);
|
|
const message = getMessageFor(props, dataSummary);
|
|
const dispatch = useDispatch();
|
|
|
|
const dashboardScene: DashboardScene | undefined = window.__grafanaSceneContext;
|
|
let panel;
|
|
if (dashboardScene instanceof DashboardScene) {
|
|
panel = findVizPanelByKey(dashboardScene, getVizPanelKeyForPanelId(props.panelId));
|
|
} else {
|
|
panel = getDashboardSrv().getCurrent()?.getPanelById(props.panelId);
|
|
}
|
|
|
|
const openVizPicker = () => {
|
|
store.setObject(LS_VISUALIZATION_SELECT_TAB_KEY, VisualizationSelectPaneTab.Suggestions);
|
|
if (dashboardScene) {
|
|
dashboardScene.state.editPanel?.state.optionsPane?.onToggleVizPicker();
|
|
|
|
return;
|
|
}
|
|
|
|
dispatch(toggleVizPicker(true));
|
|
};
|
|
|
|
const switchToTable = () => {
|
|
if (!panel) {
|
|
return;
|
|
}
|
|
|
|
if (panel instanceof VizPanel) {
|
|
panel.changePluginType('table');
|
|
|
|
return;
|
|
}
|
|
|
|
dispatch(
|
|
changePanelPlugin({
|
|
panel,
|
|
pluginId: 'table',
|
|
})
|
|
);
|
|
};
|
|
|
|
const loadSuggestion = (s: PanelPluginVisualizationSuggestion) => {
|
|
if (!panel) {
|
|
return;
|
|
}
|
|
|
|
if (panel instanceof VizPanel) {
|
|
panel.changePluginType(s.pluginId, s.options, s.fieldConfig);
|
|
} else {
|
|
dispatch(
|
|
changePanelPlugin({
|
|
...s, // includes panelId, config, etc
|
|
panel,
|
|
})
|
|
);
|
|
}
|
|
|
|
if (s.transformations) {
|
|
setTimeout(() => {
|
|
locationService.partial({ tab: 'transform' });
|
|
}, 100);
|
|
}
|
|
};
|
|
|
|
const noData = !hasData(props.data);
|
|
const noQueryConfigured = hasNoQueryConfigured(props.data);
|
|
const showEmptyState =
|
|
config.featureToggles.newVizSuggestions && context.app === CoreApp.PanelEditor && noQueryConfigured && noData;
|
|
|
|
return (
|
|
<div className={styles.wrapper}>
|
|
{showEmptyState && <Icon name="chart-line" size="xxxl" className={styles.emptyStateIcon} />}
|
|
<div className={styles.message} data-testid={selectors.components.Panels.Panel.PanelDataErrorMessage}>
|
|
{message}
|
|
</div>
|
|
{context.app === CoreApp.PanelEditor && dataSummary.hasData && panel && (
|
|
<div className={styles.actions}>
|
|
{props.suggestions && (
|
|
<>
|
|
{props.suggestions.map((v) => (
|
|
<CardButton key={v.name} icon="process" onClick={() => loadSuggestion(v)}>
|
|
{v.name}
|
|
</CardButton>
|
|
))}
|
|
</>
|
|
)}
|
|
<CardButton icon="table" onClick={switchToTable}>
|
|
<Trans i18nKey="panel.panel-data-error-view.switch-to-table">Switch to table</Trans>
|
|
</CardButton>
|
|
<CardButton icon="chart-line" onClick={openVizPicker}>
|
|
<Trans i18nKey="panel.panel-data-error-view.open-visualization-suggestions">
|
|
Open visualization suggestions
|
|
</Trans>
|
|
</CardButton>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function getMessageFor(
|
|
{ data, fieldConfig, message, needsNumberField, needsTimeField, needsStringField }: PanelDataErrorViewProps,
|
|
dataSummary: PanelDataSummary
|
|
): string {
|
|
if (message) {
|
|
return message;
|
|
}
|
|
|
|
const noData = !hasData(data);
|
|
const noQueryConfigured = hasNoQueryConfigured(data);
|
|
|
|
if (config.featureToggles.newVizSuggestions && noQueryConfigured && noData) {
|
|
return t(
|
|
'dashboard.new-panel.empty-state-message',
|
|
'Run a query to visualize it here or go to all visualizations to add other panel types'
|
|
);
|
|
}
|
|
|
|
if (noData) {
|
|
return fieldConfig?.defaults.noValue ?? t('panel.panel-data-error-view.no-value.default', 'No data');
|
|
}
|
|
|
|
if (needsStringField && !dataSummary.hasFieldType(FieldType.string)) {
|
|
return t('panel.panel-data-error-view.missing-value.string', 'Data is missing a string field');
|
|
}
|
|
|
|
if (needsNumberField && !dataSummary.hasFieldType(FieldType.number)) {
|
|
return t('panel.panel-data-error-view.missing-value.number', 'Data is missing a number field');
|
|
}
|
|
|
|
if (needsTimeField && !dataSummary.hasFieldType(FieldType.time)) {
|
|
return t('panel.panel-data-error-view.missing-value.time', 'Data is missing a time field');
|
|
}
|
|
|
|
return t('panel.panel-data-error-view.missing-value.unknown', 'Cannot visualize data');
|
|
}
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => {
|
|
return {
|
|
wrapper: css({
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
height: '100%',
|
|
width: '100%',
|
|
}),
|
|
message: css({
|
|
textAlign: 'center',
|
|
color: theme.colors.text.secondary,
|
|
fontSize: theme.typography.size.lg,
|
|
width: '100%',
|
|
}),
|
|
actions: css({
|
|
marginTop: theme.spacing(2),
|
|
display: 'flex',
|
|
height: '50%',
|
|
maxHeight: '150px',
|
|
columnGap: theme.spacing(1),
|
|
rowGap: theme.spacing(1),
|
|
width: '100%',
|
|
maxWidth: '600px',
|
|
}),
|
|
emptyStateIcon: css({
|
|
color: theme.colors.text.secondary,
|
|
marginBottom: theme.spacing(2),
|
|
}),
|
|
};
|
|
};
|