DataSource: show the uid in edit url, not the local id (#33818)

This commit is contained in:
Ryan McKinley 2021-05-08 09:13:26 -07:00 committed by GitHub
parent 48f4b87349
commit ccc0f7fc22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 110 additions and 42 deletions

View File

@ -546,6 +546,7 @@ export interface DataSourceJsonData {
*/
export interface DataSourceSettings<T extends DataSourceJsonData = DataSourceJsonData, S = {}> {
id: number;
uid: string;
orgId: number;
name: string;
typeLogoUrl: string;

View File

@ -8,6 +8,7 @@ const setup = (propOverrides?: object) => {
const props: Props = {
dataSourceConfig: {
id: 4,
uid: 'x',
orgId: 1,
name: 'gdev-influxdb',
type: 'influxdb',

View File

@ -8,6 +8,7 @@ import mdx from './DataSourceHttpSettings.mdx';
const settingsMock: DataSourceSettings<any, any> = {
id: 4,
orgId: 1,
uid: 'x',
name: 'gdev-influxdb',
type: 'influxdb',
typeName: 'Influxdb',

View File

@ -61,7 +61,7 @@ export class InputQueryEditor extends PureComponent<Props, State> {
render() {
const { datasource, query } = this.props;
const { id, name } = datasource;
const { uid, name } = datasource;
const { text } = this.state;
const selected = query.data ? options[0] : options[1];
@ -73,7 +73,7 @@ export class InputQueryEditor extends PureComponent<Props, State> {
{query.data ? (
<div style={{ alignSelf: 'center' }}>{describeDataFrame(query.data)}</div>
) : (
<LinkButton variant="link" href={`datasources/edit/${id}/`}>
<LinkButton variant="link" href={`datasources/edit/${uid}/`}>
{name}: {describeDataFrame(datasource.data)} &nbsp;&nbsp;
<Icon name="pen" />
</LinkButton>

View File

@ -96,13 +96,13 @@ export const RuleList: FC = () => {
)}
{promReqeustErrors.map(({ dataSource, error }) => (
<div key={dataSource.name}>
Failed to load rules state from <a href={`datasources/edit/${dataSource.id}`}>{dataSource.name}</a>:{' '}
Failed to load rules state from <a href={`datasources/edit/${dataSource.uid}`}>{dataSource.name}</a>:{' '}
{error.message || 'Unknown error.'}
</div>
))}
{rulerRequestErrors.map(({ dataSource, error }) => (
<div key={dataSource.name}>
Failed to load rules config from <a href={'datasources/edit/${dataSource.id}'}>{dataSource.name}</a>:{' '}
Failed to load rules config from <a href={'datasources/edit/${dataSource.uid}'}>{dataSource.name}</a>:{' '}
{error.message || 'Unknown error.'}
</div>
))}

View File

@ -11,7 +11,7 @@ const setup = (propOverrides?: object) => {
navModel: {} as NavModel,
dashboards: [] as PluginDashboard[],
dataSource: {} as DataSourceSettings,
dataSourceId: 1,
dataSourceId: 'x',
importDashboard: jest.fn(),
loadDataSource: jest.fn(),
loadPluginDashboards: jest.fn(),

View File

@ -17,10 +17,10 @@ import { getDataSource } from './state/selectors';
import { PluginDashboard, StoreState } from 'app/types';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
export interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> {}
export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
function mapStateToProps(state: StoreState, props: OwnProps) {
const dataSourceId = parseInt(props.match.params.id, 10);
const dataSourceId = props.match.params.uid;
return {
navModel: getNavModel(state.navIndex, `datasource-dashboards-${dataSourceId}`),

View File

@ -19,7 +19,7 @@ export const DataSourcesList: FC<Props> = ({ dataSources, layoutMode }) => {
{dataSources.map((dataSource, index) => {
return (
<li key={dataSource.id}>
<Card heading={dataSource.name} href={`datasources/edit/${dataSource.id}`}>
<Card heading={dataSource.name} href={`datasources/edit/${dataSource.uid}`}>
<Card.Figure>
<img src={dataSource.typeLogoUrl} alt={dataSource.name} />
</Card.Figure>

View File

@ -34,6 +34,7 @@ export const getMockDataSource = (): DataSourceSettings => {
withCredentials: false,
database: '',
id: 13,
uid: 'x',
isDefault: false,
jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
name: 'gdev-cloudwatch',

View File

@ -3,6 +3,7 @@ import { DataSourceSettings } from '@grafana/data';
export function createDatasourceSettings<T>(jsonData: T): DataSourceSettings<T> {
return {
id: 0,
uid: 'x',
orgId: 0,
name: 'datasource-test',
typeLogoUrl: '',

View File

@ -23,7 +23,7 @@ const getProps = (): Props => ({
},
dataSource: getMockDataSource(),
dataSourceMeta: getMockPlugin(),
dataSourceId: 1,
dataSourceId: 'x',
deleteDataSource: jest.fn(),
loadDataSource: jest.fn(),
setDataSourceName,

View File

@ -32,10 +32,10 @@ import { connect, ConnectedProps } from 'react-redux';
import { cleanUpAction } from 'app/core/actions/cleanUp';
import { ShowConfirmModalEvent } from '../../../types/events';
export interface OwnProps extends GrafanaRouteComponentProps<{ id: string }> {}
export interface OwnProps extends GrafanaRouteComponentProps<{ uid: string }> {}
function mapStateToProps(state: StoreState, props: OwnProps) {
const dataSourceId = props.match.params.id;
const dataSourceId = props.match.params.uid;
const params = new URLSearchParams(props.location.search);
const dataSource = getDataSource(state.dataSources, dataSourceId);
const { plugin, loadError, testingStatus } = state.dataSourceSettings;
@ -49,7 +49,7 @@ function mapStateToProps(state: StoreState, props: OwnProps) {
),
dataSource: getDataSource(state.dataSources, dataSourceId),
dataSourceMeta: getDataSourceMeta(state.dataSources, dataSource.type),
dataSourceId: parseInt(dataSourceId, 10),
dataSourceId: dataSourceId,
page,
plugin,
loadError,

View File

@ -72,23 +72,21 @@ describe('Find new name', () => {
});
describe('initDataSourceSettings', () => {
describe('when pageId is not a number', () => {
describe('when pageId is missing', () => {
it('then initDataSourceSettingsFailed should be dispatched', async () => {
const dispatchedActions = await thunkTester({})
.givenThunk(initDataSourceSettings)
.whenThunkIsDispatched('some page');
const dispatchedActions = await thunkTester({}).givenThunk(initDataSourceSettings).whenThunkIsDispatched('');
expect(dispatchedActions).toEqual([initDataSourceSettingsFailed(new Error('Invalid ID'))]);
});
});
describe('when pageId is a number', () => {
describe('when pageId is a valid', () => {
it('then initDataSourceSettingsSucceeded should be dispatched', async () => {
const thunkMock = (): ThunkResult<void> => (dispatch: ThunkDispatch, getState) => {};
const dataSource = { type: 'app' };
const dataSourceMeta = { id: 'some id' };
const dependencies: InitDataSourceSettingDependencies = {
loadDataSource: jest.fn(thunkMock),
loadDataSource: jest.fn(thunkMock) as any,
getDataSource: jest.fn().mockReturnValue(dataSource),
getDataSourceMeta: jest.fn().mockReturnValue(dataSourceMeta),
importDataSourcePlugin: jest.fn().mockReturnValue({} as GenericDataSourcePlugin),

View File

@ -3,7 +3,7 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { updateNavIndex } from 'app/core/actions';
import { buildNavModel } from './navModel';
import { DataSourcePluginMeta, DataSourceSettings } from '@grafana/data';
import { DataSourcePluginMeta, DataSourceSettings, locationUtil } from '@grafana/data';
import { DataSourcePluginCategory, ThunkResult, ThunkDispatch } from 'app/types';
import { getPluginSettings } from 'app/features/plugins/PluginSettingsCache';
import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
@ -41,7 +41,7 @@ export interface TestDataSourceDependencies {
}
export const initDataSourceSettings = (
pageId: number,
pageId: string,
dependencies: InitDataSourceSettingDependencies = {
loadDataSource,
getDataSource,
@ -49,14 +49,16 @@ export const initDataSourceSettings = (
importDataSourcePlugin,
}
): ThunkResult<void> => {
return async (dispatch: ThunkDispatch, getState) => {
if (isNaN(pageId)) {
return async (dispatch, getState) => {
if (!pageId) {
dispatch(initDataSourceSettingsFailed(new Error('Invalid ID')));
return;
}
try {
await dispatch(dependencies.loadDataSource(pageId));
// have we already loaded the plugin then we can skip the steps below?
if (getState().dataSourceSettings.plugin) {
return;
}
@ -111,9 +113,9 @@ export function loadDataSources(): ThunkResult<void> {
};
}
export function loadDataSource(id: number): ThunkResult<void> {
export function loadDataSource(uid: string): ThunkResult<void> {
return async (dispatch) => {
const dataSource = (await getBackendSrv().get(`/api/datasources/${id}`)) as DataSourceSettings;
const dataSource = await getDataSourceUsingUidOrId(uid);
const pluginInfo = (await getPluginSettings(dataSource.type)) as DataSourcePluginMeta;
const plugin = await importDataSourcePlugin(pluginInfo);
@ -123,6 +125,50 @@ export function loadDataSource(id: number): ThunkResult<void> {
};
}
/**
* Get data source by uid or id, if old id detected handles redirect
*/
async function getDataSourceUsingUidOrId(uid: string): Promise<DataSourceSettings> {
// Try first with uid api
try {
const byUid = await getBackendSrv()
.fetch<DataSourceSettings>({
method: 'GET',
url: `/api/datasources/uid/${uid}`,
showErrorAlert: false,
})
.toPromise();
if (byUid.ok) {
return byUid.data;
}
} catch (err) {
console.log('Failed to lookup data source by uid', err);
}
// try lookup by old db id
const id = parseInt(uid, 10);
if (!Number.isNaN(id)) {
const response = await getBackendSrv()
.fetch<DataSourceSettings>({
method: 'GET',
url: `/api/datasources/${id}`,
showErrorAlert: false,
})
.toPromise();
// Not ideal to do a full page reload here but so tricky to handle this otherwise
// We can update the location using react router, but need to fully reload the route as the nav model
// page index is not matching with the url in that case. And react router has no way to unmount remount a route
if (response.ok && response.data.id.toString() === uid) {
window.location.href = locationUtil.assureBaseUrl(`/datasources/edit/${response.data.uid}`);
return {} as DataSourceSettings; // avoids flashing an error
}
}
throw Error('Could not find data source');
}
export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> {
return async (dispatch, getStore) => {
await dispatch(loadDataSources());
@ -141,7 +187,7 @@ export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> {
}
const result = await getBackendSrv().post('/api/datasources', newInstance);
locationService.push(`/datasources/edit/${result.id}`);
locationService.push(`/datasources/edit/${result.datasource.uid}`);
};
}
@ -156,9 +202,9 @@ export function loadDataSourcePlugins(): ThunkResult<void> {
export function updateDataSource(dataSource: DataSourceSettings): ThunkResult<void> {
return async (dispatch) => {
await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource);
await getBackendSrv().put(`/api/datasources/${dataSource.id}`, dataSource); // by UID not yet supported
await updateFrontendSettings();
return dispatch(loadDataSource(dataSource.id));
return dispatch(loadDataSource(dataSource.uid));
};
}

View File

@ -7,7 +7,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
const navModel: NavModelItem = {
img: pluginMeta.info.logos.large,
id: 'datasource-' + dataSource.id,
id: 'datasource-' + dataSource.uid,
subTitle: `Type: ${pluginMeta.name}`,
url: '',
text: dataSource.name,
@ -16,9 +16,9 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
{
active: false,
icon: 'sliders-v-alt',
id: `datasource-settings-${dataSource.id}`,
id: `datasource-settings-${dataSource.uid}`,
text: 'Settings',
url: `datasources/edit/${dataSource.id}/`,
url: `datasources/edit/${dataSource.uid}/`,
},
],
};
@ -29,7 +29,7 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
active: false,
text: page.title,
icon: page.icon,
url: `datasources/edit/${dataSource.id}/?page=${page.id}`,
url: `datasources/edit/${dataSource.uid}/?page=${page.id}`,
id: `datasource-page-${page.id}`,
});
}
@ -39,9 +39,9 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
navModel.children!.push({
active: false,
icon: 'apps',
id: `datasource-dashboards-${dataSource.id}`,
id: `datasource-dashboards-${dataSource.uid}`,
text: 'Dashboards',
url: `datasources/edit/${dataSource.id}/dashboards`,
url: `datasources/edit/${dataSource.uid}/dashboards`,
});
}
@ -49,25 +49,25 @@ export function buildNavModel(dataSource: DataSourceSettings, plugin: GenericDat
navModel.children!.push({
active: false,
icon: 'lock',
id: `datasource-permissions-${dataSource.id}`,
id: `datasource-permissions-${dataSource.uid}`,
text: 'Permissions',
url: `datasources/edit/${dataSource.id}/permissions`,
url: `datasources/edit/${dataSource.uid}/permissions`,
});
navModel.children!.push({
active: false,
icon: 'info-circle',
id: `datasource-insights-${dataSource.id}`,
id: `datasource-insights-${dataSource.uid}`,
text: 'Insights',
url: `datasources/edit/${dataSource.id}/insights`,
url: `datasources/edit/${dataSource.uid}/insights`,
});
navModel.children!.push({
active: false,
icon: 'database',
id: `datasource-cache-${dataSource.id}`,
id: `datasource-cache-${dataSource.uid}`,
text: 'Cache',
url: `datasources/edit/${dataSource.id}/cache`,
url: `datasources/edit/${dataSource.uid}/cache`,
});
}
@ -84,6 +84,7 @@ export function getDataSourceLoadingNav(pageName: string): NavModel {
withCredentials: false,
database: '',
id: 1,
uid: 'x',
isDefault: false,
jsonData: { authType: 'credentials', defaultRegion: 'eu-west-2' },
name: 'Loading',

View File

@ -18,7 +18,7 @@ export const getDataSourcePlugins = (state: DataSourcesState) => {
};
export const getDataSource = (state: DataSourcesState, dataSourceId: UrlQueryValue): DataSourceSettings => {
if (state.dataSource.id === parseInt(dataSourceId as string, 10)) {
if (state.dataSource.uid === dataSourceId) {
return state.dataSource;
}
return {} as DataSourceSettings;

View File

@ -22,6 +22,7 @@ const setup = (propOverrides?: object) => {
const props: Props = {
options: {
id: 1,
uid: 'z',
orgId: 1,
typeLogoUrl: '',
name: 'CloudWatch',

View File

@ -37,6 +37,7 @@ exports[`Render should disable access key id field 1`] = `
"type": "cloudwatch",
"typeLogoUrl": "",
"typeName": "Cloudwatch",
"uid": "z",
"url": "",
"user": "",
"withCredentials": false,
@ -101,6 +102,7 @@ exports[`Render should render component 1`] = `
"type": "cloudwatch",
"typeLogoUrl": "",
"typeName": "Cloudwatch",
"uid": "z",
"url": "",
"user": "",
"withCredentials": false,
@ -165,6 +167,7 @@ exports[`Render should show access key and secret access key fields 1`] = `
"type": "cloudwatch",
"typeLogoUrl": "",
"typeName": "Cloudwatch",
"uid": "z",
"url": "",
"user": "",
"withCredentials": false,
@ -229,6 +232,7 @@ exports[`Render should show arn role field 1`] = `
"type": "cloudwatch",
"typeLogoUrl": "",
"typeName": "Cloudwatch",
"uid": "z",
"url": "",
"user": "",
"withCredentials": false,
@ -293,6 +297,7 @@ exports[`Render should show credentials profile name field 1`] = `
"type": "cloudwatch",
"typeLogoUrl": "",
"typeName": "Cloudwatch",
"uid": "z",
"url": "",
"user": "",
"withCredentials": false,

View File

@ -6,6 +6,7 @@ const setup = (propOverrides?: object) => {
const props: Props = {
options: {
id: 21,
uid: 'x',
orgId: 1,
name: 'Azure Monitor-10-10',
type: 'grafana-azure-monitor-datasource',

View File

@ -6,6 +6,7 @@ const setup = () => {
const props: Props = {
options: {
id: 21,
uid: 'y',
orgId: 1,
name: 'Azure Monitor-10-10',
type: 'grafana-azure-monitor-datasource',

View File

@ -6,6 +6,7 @@ const setup = (propOverrides?: object) => {
const props: Props = {
options: {
id: 21,
uid: 'x',
orgId: 1,
name: 'Azure Monitor-10-10',
type: 'grafana-azure-monitor-datasource',

View File

@ -31,6 +31,7 @@ exports[`Render should render component 1`] = `
"type": "grafana-azure-monitor-datasource",
"typeLogoUrl": "",
"typeName": "Azure",
"uid": "y",
"url": "",
"user": "",
"version": 1,
@ -69,6 +70,7 @@ exports[`Render should render component 1`] = `
"type": "grafana-azure-monitor-datasource",
"typeLogoUrl": "",
"typeName": "Azure",
"uid": "y",
"url": "/api/datasources/proxy/21",
"user": "",
"version": 1,
@ -109,6 +111,7 @@ exports[`Render should render component 1`] = `
"type": "grafana-azure-monitor-datasource",
"typeLogoUrl": "",
"typeName": "Azure",
"uid": "y",
"url": "",
"user": "",
"version": 1,
@ -145,6 +148,7 @@ exports[`Render should render component 1`] = `
"type": "grafana-azure-monitor-datasource",
"typeLogoUrl": "",
"typeName": "Azure",
"uid": "y",
"url": "",
"user": "",
"version": 1,

View File

@ -6,6 +6,7 @@ const setup = (propOverrides?: object) => {
const props: Props = {
options: {
id: 21,
uid: 'z',
orgId: 1,
name: 'InfluxDB-3',
type: 'influxdb',

View File

@ -93,6 +93,7 @@ exports[`Render should disable basic auth password input 1`] = `
"type": "influxdb",
"typeLogoUrl": "",
"typeName": "Influx",
"uid": "z",
"url": "",
"user": "",
"version": 1,
@ -389,6 +390,7 @@ exports[`Render should hide basic auth fields when switch off 1`] = `
"type": "influxdb",
"typeLogoUrl": "",
"typeName": "Influx",
"uid": "z",
"url": "",
"user": "",
"version": 1,
@ -685,6 +687,7 @@ exports[`Render should hide white listed cookies input when browser access chose
"type": "influxdb",
"typeLogoUrl": "",
"typeName": "Influx",
"uid": "z",
"url": "",
"user": "",
"version": 1,
@ -981,6 +984,7 @@ exports[`Render should render component 1`] = `
"type": "influxdb",
"typeLogoUrl": "",
"typeName": "Influx",
"uid": "z",
"url": "",
"user": "",
"version": 1,

View File

@ -94,7 +94,7 @@ export function getAppRoutes(): RouteDescriptor[] {
),
},
{
path: '/datasources/edit/:id/',
path: '/datasources/edit/:uid/',
component: SafeDynamicImport(
() =>
import(
@ -103,7 +103,7 @@ export function getAppRoutes(): RouteDescriptor[] {
),
},
{
path: '/datasources/edit/:id/dashboards',
path: '/datasources/edit/:uid/dashboards',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "DataSourceDashboards"*/ 'app/features/datasources/DataSourceDashboards')
),