Datasource view: Show a not-found message (#110417)

This commit is contained in:
Andres Martinez Gotor 2025-09-02 14:37:51 +02:00 committed by GitHub
parent 0c44a0c14a
commit 13baef080c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 58 additions and 18 deletions

View File

@ -52,6 +52,18 @@ export function useDataSourceSettingsNav(pageIdParam?: string) {
pageNav = getNavModel(navIndex, navIndexId, getDataSourceLoadingNav('settings')); pageNav = getNavModel(navIndex, navIndexId, getDataSourceLoadingNav('settings'));
} }
if (!datasource.uid) {
const node: NavModelItem = {
text: t('connections.use-data-source-settings-nav.node.subTitle.data-source-error', 'Data Source Error'),
icon: 'exclamation-triangle',
};
pageNav = {
node: node,
main: node,
};
}
if (plugin) { if (plugin) {
pageNav = getNavModel( pageNav = getNavModel(
navIndex, navIndex,
@ -64,8 +76,8 @@ export function useDataSourceSettingsNav(pageIdParam?: string) {
...pageNav.main, ...pageNav.main,
dataSourcePluginName: datasourcePlugin?.name || plugin?.meta.name || '', dataSourcePluginName: datasourcePlugin?.name || plugin?.meta.name || '',
active: true, active: true,
text: datasource.name, text: datasource.name || '',
subTitle: `Type: ${dataSourceMeta.name}`, subTitle: dataSourceMeta.name ? `Type: ${dataSourceMeta.name}` : '',
children: (pageNav.main.children || []).map((navModelItem) => ({ children: (pageNav.main.children || []).map((navModelItem) => ({
...navModelItem, ...navModelItem,
url: navModelItem.url?.replace('datasources/edit/', '/connections/datasources/edit/'), url: navModelItem.url?.replace('datasources/edit/', '/connections/datasources/edit/'),

View File

@ -1,5 +1,5 @@
import { Trans } from '@grafana/i18n'; import { t, Trans } from '@grafana/i18n';
import { Button } from '@grafana/ui'; import { Button, EmptyState } from '@grafana/ui';
import { DataSourceRights } from '../types'; import { DataSourceRights } from '../types';
@ -8,9 +8,10 @@ import { DataSourceReadOnlyMessage } from './DataSourceReadOnlyMessage';
export type Props = { export type Props = {
dataSourceRights: DataSourceRights; dataSourceRights: DataSourceRights;
onDelete: () => void; onDelete: () => void;
notFound: boolean;
}; };
export function DataSourceLoadError({ dataSourceRights, onDelete }: Props) { export function DataSourceLoadError({ dataSourceRights, onDelete, notFound }: Props) {
const { readOnly, hasDeleteRights } = dataSourceRights; const { readOnly, hasDeleteRights } = dataSourceRights;
const canDelete = !readOnly && hasDeleteRights; const canDelete = !readOnly && hasDeleteRights;
const navigateBack = () => window.history.back(); const navigateBack = () => window.history.back();
@ -20,6 +21,12 @@ export function DataSourceLoadError({ dataSourceRights, onDelete }: Props) {
{readOnly && <DataSourceReadOnlyMessage />} {readOnly && <DataSourceReadOnlyMessage />}
<div className="gf-form-button-row"> <div className="gf-form-button-row">
{notFound && (
<EmptyState
variant="not-found"
message={t('datasources.data-source-load-error.not-found', 'Data source not found')}
/>
)}
{canDelete && ( {canDelete && (
<Button type="submit" variant="destructive" onClick={onDelete}> <Button type="submit" variant="destructive" onClick={onDelete}>
<Trans i18nKey="datasources.data-source-load-error.delete">Delete</Trans> <Trans i18nKey="datasources.data-source-load-error.delete">Delete</Trans>

View File

@ -108,6 +108,14 @@ describe('<EditDataSource>', () => {
expect(screen.queryByText(readOnlyMessage)).toBeVisible(); expect(screen.queryByText(readOnlyMessage)).toBeVisible();
}); });
it('should render a message if the datasource is not found', () => {
setup({
dataSource: getMockDataSource({ uid: undefined, id: 0 }),
});
expect(screen.queryByText('Data source not found')).toBeVisible();
});
}); });
describe('On loading', () => { describe('On loading', () => {

View File

@ -114,7 +114,7 @@ export function EditDataSourceView({
}: ViewProps) { }: ViewProps) {
const { plugin, loadError, testingStatus, loading } = dataSourceSettings; const { plugin, loadError, testingStatus, loading } = dataSourceSettings;
const { readOnly, hasWriteRights, hasDeleteRights } = dataSourceRights; const { readOnly, hasWriteRights, hasDeleteRights } = dataSourceRights;
const hasDataSource = dataSource.id > 0; const hasDataSource = dataSource.id > 0 && dataSource.uid;
const { components, isLoading } = useDataSourceConfigPluginExtensions(); const { components, isLoading } = useDataSourceConfigPluginExtensions();
// This is a workaround to avoid race-conditions between the `setSecureJsonData()` and `setJsonData()` calls instantiated by the extension components. // This is a workaround to avoid race-conditions between the `setSecureJsonData()` and `setJsonData()` calls instantiated by the extension components.
// Both those exposed functions are calling `onOptionsChange()` with the new jsonData and secureJsonData, and if they are called in the same tick, the Redux store // Both those exposed functions are calling `onOptionsChange()` with the new jsonData and secureJsonData, and if they are called in the same tick, the Redux store
@ -150,9 +150,14 @@ export function EditDataSourceView({
onTest(); onTest();
}; };
if (loadError) { if (loading || isLoading) {
return <PageLoader />;
}
if (loadError || !hasDataSource || !dsi) {
return ( return (
<DataSourceLoadError <DataSourceLoadError
notFound={!hasDataSource || !dsi}
dataSourceRights={dataSourceRights} dataSourceRights={dataSourceRights}
onDelete={() => { onDelete={() => {
trackDsConfigClicked('delete'); trackDsConfigClicked('delete');
@ -162,15 +167,6 @@ export function EditDataSourceView({
); );
} }
if (loading || isLoading) {
return <PageLoader />;
}
// TODO - is this needed?
if (!hasDataSource || !dsi) {
return null;
}
if (pageId) { if (pageId) {
return ( return (
<DataSourcePluginContextProvider instanceSettings={dsi}> <DataSourcePluginContextProvider instanceSettings={dsi}>

View File

@ -90,7 +90,7 @@ const mockDataSource = getMockDataSource({
// Mock useDataSource hook // Mock useDataSource hook
jest.mock('../state/hooks', () => ({ jest.mock('../state/hooks', () => ({
useDataSource: () => mockDataSource, useDataSource: (uid: string) => (uid === 'not-found' ? {} : mockDataSource),
})); }));
describe('EditDataSourceActions', () => { describe('EditDataSourceActions', () => {
@ -374,6 +374,14 @@ describe('EditDataSourceActions', () => {
}); });
}); });
describe('DataSource Not Found', () => {
it('should not render actions when data source is not found', () => {
render(<EditDataSourceActions uid="not-found" />);
expect(screen.queryByText('Explore data')).not.toBeInTheDocument();
expect(screen.queryByText('Build a dashboard')).not.toBeInTheDocument();
});
});
describe('Favorite Actions', () => { describe('Favorite Actions', () => {
it('should not render favorite button when feature toggle is disabled', () => { it('should not render favorite button when feature toggle is disabled', () => {
config.featureToggles.favoriteDatasources = false; config.featureToggles.favoriteDatasources = false;

View File

@ -90,6 +90,10 @@ export function EditDataSourceActions({ uid }: Props) {
</Menu> </Menu>
); );
if (!dataSource.uid) {
return null;
}
return ( return (
<> <>
<FavoriteButton uid={uid} /> <FavoriteButton uid={uid} />

View File

@ -11,6 +11,10 @@ export const useDataSourceInfo = (dataSourceInfo: DataSourceInfo): PageInfoItem[
const info: PageInfoItem[] = []; const info: PageInfoItem[] = [];
const alertingEnabled = dataSourceInfo.alertingSupported; const alertingEnabled = dataSourceInfo.alertingSupported;
if (!dataSourceInfo.dataSourcePluginName) {
return info;
}
info.push({ info.push({
label: t('datasources.use-data-source-info.label.type', 'Type'), label: t('datasources.use-data-source-info.label.type', 'Type'),
value: dataSourceInfo.dataSourcePluginName, value: dataSourceInfo.dataSourcePluginName,

View File

@ -6543,7 +6543,8 @@
}, },
"data-source-load-error": { "data-source-load-error": {
"back": "Back", "back": "Back",
"delete": "Delete" "delete": "Delete",
"not-found": "Data source not found"
}, },
"data-source-missing-rights-message": { "data-source-missing-rights-message": {
"title-missing-rights": "Missing rights" "title-missing-rights": "Missing rights"