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'));
}
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) {
pageNav = getNavModel(
navIndex,
@ -64,8 +76,8 @@ export function useDataSourceSettingsNav(pageIdParam?: string) {
...pageNav.main,
dataSourcePluginName: datasourcePlugin?.name || plugin?.meta.name || '',
active: true,
text: datasource.name,
subTitle: `Type: ${dataSourceMeta.name}`,
text: datasource.name || '',
subTitle: dataSourceMeta.name ? `Type: ${dataSourceMeta.name}` : '',
children: (pageNav.main.children || []).map((navModelItem) => ({
...navModelItem,
url: navModelItem.url?.replace('datasources/edit/', '/connections/datasources/edit/'),

View File

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

View File

@ -108,6 +108,14 @@ describe('<EditDataSource>', () => {
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', () => {

View File

@ -114,7 +114,7 @@ export function EditDataSourceView({
}: ViewProps) {
const { plugin, loadError, testingStatus, loading } = dataSourceSettings;
const { readOnly, hasWriteRights, hasDeleteRights } = dataSourceRights;
const hasDataSource = dataSource.id > 0;
const hasDataSource = dataSource.id > 0 && dataSource.uid;
const { components, isLoading } = useDataSourceConfigPluginExtensions();
// 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
@ -150,9 +150,14 @@ export function EditDataSourceView({
onTest();
};
if (loadError) {
if (loading || isLoading) {
return <PageLoader />;
}
if (loadError || !hasDataSource || !dsi) {
return (
<DataSourceLoadError
notFound={!hasDataSource || !dsi}
dataSourceRights={dataSourceRights}
onDelete={() => {
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) {
return (
<DataSourcePluginContextProvider instanceSettings={dsi}>

View File

@ -90,7 +90,7 @@ const mockDataSource = getMockDataSource({
// Mock useDataSource hook
jest.mock('../state/hooks', () => ({
useDataSource: () => mockDataSource,
useDataSource: (uid: string) => (uid === 'not-found' ? {} : mockDataSource),
}));
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', () => {
it('should not render favorite button when feature toggle is disabled', () => {
config.featureToggles.favoriteDatasources = false;

View File

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

View File

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

View File

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