mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Data Connections: Add data sources (#52436)
* feat: show data-sources under the data-connections page * refactor: add a constant for data-sources routes * refactor: add a context that holds the currently active data-sources routes * refactor: use the data-sources routes constant wherever possible * refactor: use the data-sources routes context wherever possible * feat(data-connections): add edit and new pages * feat(data-connections): set the the custom routes via the context provider * fix(data-connections): set the active tab properly We needed to update the routes to match with the ones on the backend ("data-sources" vs "datasources"), and we also needed to check if it is the default tab, in which case we would like to highlight the Datasources tab. * tests: fix tests for Data Connections page * fix: address rebase issues * tests: find button based on role and text Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> * fix: add missing closing ) paren in tests * refactor: use implicit return types for components * tests: change role from "button" to "link" * refactor: stop using unnecessary wrapper components Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com>
This commit is contained in:
parent
53b8e528fc
commit
a1c565dec9
@ -4,12 +4,16 @@ import { Provider } from 'react-redux';
|
||||
import { Router } from 'react-router-dom';
|
||||
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { getMockDataSources } from 'app/features/datasources/__mocks__';
|
||||
import * as api from 'app/features/datasources/api';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
|
||||
import DataConnectionsPage from './DataConnectionsPage';
|
||||
import { navIndex } from './__mocks__/store.navIndex.mock';
|
||||
import { ROUTE_BASE_ID, ROUTES } from './constants';
|
||||
|
||||
jest.mock('app/features/datasources/api');
|
||||
|
||||
const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => {
|
||||
// @ts-ignore
|
||||
const store = configureStore({ navIndex });
|
||||
@ -25,6 +29,12 @@ const renderPage = (path = `/${ROUTE_BASE_ID}`): RenderResult => {
|
||||
};
|
||||
|
||||
describe('Data Connections Page', () => {
|
||||
const mockDatasources = getMockDataSources(3);
|
||||
|
||||
beforeEach(() => {
|
||||
(api.getDataSources as jest.Mock) = jest.fn().mockResolvedValue(mockDatasources);
|
||||
});
|
||||
|
||||
test('shows all the four tabs', async () => {
|
||||
renderPage();
|
||||
|
||||
@ -37,7 +47,8 @@ describe('Data Connections Page', () => {
|
||||
test('shows the "Data sources" tab by default', async () => {
|
||||
renderPage();
|
||||
|
||||
expect(await screen.findByText('The list of data sources is under development.')).toBeVisible();
|
||||
expect(await screen.findByRole('link', { name: /add data source/i })).toBeVisible();
|
||||
expect(await screen.findByText(mockDatasources[0].name)).toBeVisible();
|
||||
});
|
||||
|
||||
test('renders the correct tab even if accessing it with a "sub-url"', async () => {
|
||||
|
@ -2,29 +2,44 @@ import * as React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { DataSourcesList } from 'app/features/datasources/components/DataSourcesList';
|
||||
import { NewDataSource } from 'app/features/datasources/components/NewDataSource';
|
||||
import { DataSourcesRoutesContext } from 'app/features/datasources/state';
|
||||
|
||||
import { ROUTES } from './constants';
|
||||
import { useNavModel } from './hooks/useNavModel';
|
||||
import { CloudIntegrations } from './tabs/CloudIntegrations';
|
||||
import { DataSources } from './tabs/DataSources';
|
||||
import { DataSourcesEdit } from './tabs/DataSourcesEdit';
|
||||
import { Plugins } from './tabs/Plugins';
|
||||
import { RecordedQueries } from './tabs/RecordedQueries';
|
||||
|
||||
export default function DataConnectionsPage(): React.ReactElement | null {
|
||||
export default function DataConnectionsPage() {
|
||||
const navModel = useNavModel();
|
||||
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<Switch>
|
||||
<Route path={ROUTES.Plugins} component={Plugins} />
|
||||
<Route path={ROUTES.CloudIntegrations} component={CloudIntegrations} />
|
||||
<Route path={ROUTES.RecordedQueries} component={RecordedQueries} />
|
||||
<DataSourcesRoutesContext.Provider
|
||||
value={{
|
||||
New: ROUTES.DataSourcesNew,
|
||||
List: ROUTES.DataSources,
|
||||
Edit: ROUTES.DataSourcesEdit,
|
||||
Dashboards: ROUTES.DataSourcesDashboards,
|
||||
}}
|
||||
>
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
<Switch>
|
||||
<Route path={ROUTES.DataSourcesNew} component={NewDataSource} />
|
||||
<Route path={ROUTES.DataSourcesEdit} component={DataSourcesEdit} />
|
||||
<Route path={ROUTES.DataSources} component={DataSourcesList} />
|
||||
<Route path={ROUTES.Plugins} component={Plugins} />
|
||||
<Route path={ROUTES.CloudIntegrations} component={CloudIntegrations} />
|
||||
<Route path={ROUTES.RecordedQueries} component={RecordedQueries} />
|
||||
|
||||
{/* Default page */}
|
||||
<Route component={DataSources} />
|
||||
</Switch>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
{/* Default page */}
|
||||
<Route component={DataSourcesList} />
|
||||
</Switch>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
</DataSourcesRoutesContext.Provider>
|
||||
);
|
||||
}
|
||||
|
@ -5,7 +5,10 @@ export const CLOUD_ONBOARDING_APP_ID = 'grafana-easystart-app';
|
||||
export const ROUTE_BASE_ID = 'data-connections';
|
||||
|
||||
export enum ROUTES {
|
||||
DataSources = '/data-connections/data-sources',
|
||||
DataSources = '/data-connections/datasources',
|
||||
DataSourcesNew = '/data-connections/datasources/new',
|
||||
DataSourcesEdit = '/data-connections/datasources/edit/:uid',
|
||||
DataSourcesDashboards = '/data-connections/datasources/edit/:uid/dashboards',
|
||||
Plugins = '/data-connections/plugins',
|
||||
CloudIntegrations = '/data-connections/cloud-integrations',
|
||||
RecordedQueries = '/data-connections/recorded-queries',
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { StoreState } from 'app/types/store';
|
||||
|
||||
import { ROUTE_BASE_ID } from '../constants';
|
||||
@ -9,14 +10,17 @@ import { ROUTE_BASE_ID } from '../constants';
|
||||
// (In case we were using `getNavModel()` from app/core/selectors/navModel, then we would need to set
|
||||
// the child nav-model-item's ID on the call-site.)
|
||||
export const useNavModel = () => {
|
||||
const { pathname } = useLocation();
|
||||
const { pathname: currentPath } = useLocation();
|
||||
const navIndex = useSelector((state: StoreState) => state.navIndex);
|
||||
const node = navIndex[ROUTE_BASE_ID];
|
||||
const main = node;
|
||||
const isDefaultRoute = (item: NavModelItem) =>
|
||||
currentPath === `/${ROUTE_BASE_ID}` && item.id === 'data-connections-datasources';
|
||||
const isItemActive = (item: NavModelItem) => currentPath.startsWith(item.url || '');
|
||||
|
||||
main.children = main.children?.map((item) => ({
|
||||
...item,
|
||||
active: pathname.startsWith(item.url || ''),
|
||||
active: isItemActive(item) || isDefaultRoute(item),
|
||||
}));
|
||||
|
||||
return {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React, { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
@ -7,7 +7,7 @@ import { AppPluginLoader } from 'app/features/plugins/components/AppPluginLoader
|
||||
|
||||
import { CLOUD_ONBOARDING_APP_ID, ROUTES } from '../../constants';
|
||||
|
||||
export function CloudIntegrations(): ReactElement | null {
|
||||
export function CloudIntegrations() {
|
||||
const s = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
|
@ -1,5 +0,0 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
|
||||
export function DataSources(): ReactElement | null {
|
||||
return <div>The list of data sources is under development.</div>;
|
||||
}
|
@ -1 +0,0 @@
|
||||
export * from './DataSources';
|
@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
||||
import { EditDataSource } from 'app/features/datasources/components/EditDataSource';
|
||||
|
||||
export interface Props extends GrafanaRouteComponentProps<{ uid: string }> {}
|
||||
|
||||
export function DataSourcesEdit(props: Props) {
|
||||
const uid = props.match.params.uid;
|
||||
const params = new URLSearchParams(props.location.search);
|
||||
const pageId = params.get('page');
|
||||
|
||||
return <EditDataSource uid={uid} pageId={pageId} />;
|
||||
}
|
@ -0,0 +1 @@
|
||||
export * from './DataSourcesEdit';
|
@ -1,5 +1,5 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export function Plugins(): ReactElement | null {
|
||||
export function Plugins() {
|
||||
return <div>The list of plugins is under development</div>;
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React, { ReactElement } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
export function RecordedQueries(): ReactElement | null {
|
||||
export function RecordedQueries() {
|
||||
return <div>The recorded queries tab is under development.</div>;
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import PageLoader from 'app/core/components/PageLoader/PageLoader';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { StoreState, AccessControlAction } from 'app/types';
|
||||
|
||||
import { getDataSources, getDataSourcesCount, useLoadDataSources } from '../state';
|
||||
import { getDataSources, getDataSourcesCount, useDataSourcesRoutes, useLoadDataSources } from '../state';
|
||||
|
||||
import { DataSourcesListHeader } from './DataSourcesListHeader';
|
||||
|
||||
@ -40,6 +40,7 @@ export type ViewProps = {
|
||||
|
||||
export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading, hasCreateRights }: ViewProps) {
|
||||
const styles = useStyles(getStyles);
|
||||
const dataSourcesRoutes = useDataSourcesRoutes();
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader />;
|
||||
@ -51,7 +52,7 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading,
|
||||
buttonDisabled={!hasCreateRights}
|
||||
title="No data sources defined"
|
||||
buttonIcon="database"
|
||||
buttonLink="datasources/new"
|
||||
buttonLink={dataSourcesRoutes.New}
|
||||
buttonTitle="Add data source"
|
||||
proTip="You can also define data sources through configuration files."
|
||||
proTipLink="http://docs.grafana.org/administration/provisioning/#datasources?utm_source=grafana_ds_list"
|
||||
@ -71,7 +72,7 @@ export function DataSourcesListView({ dataSources, dataSourcesCount, isLoading,
|
||||
{dataSources.map((dataSource) => {
|
||||
return (
|
||||
<li key={dataSource.uid}>
|
||||
<Card href={`datasources/edit/${dataSource.uid}`}>
|
||||
<Card href={dataSourcesRoutes.Edit.replace(/:uid/gi, dataSource.uid)}>
|
||||
<Card.Heading>{dataSource.name}</Card.Heading>
|
||||
<Card.Figure>
|
||||
<img src={dataSource.typeLogoUrl} alt="" height="40px" width="40px" className={styles.logo} />
|
||||
|
@ -6,7 +6,7 @@ import PageActionBar from 'app/core/components/PageActionBar/PageActionBar';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { AccessControlAction, StoreState } from 'app/types';
|
||||
|
||||
import { getDataSourcesSearchQuery, setDataSourcesSearchQuery } from '../state';
|
||||
import { getDataSourcesSearchQuery, setDataSourcesSearchQuery, useDataSourcesRoutes } from '../state';
|
||||
|
||||
export function DataSourcesListHeader() {
|
||||
const dispatch = useDispatch();
|
||||
@ -30,8 +30,9 @@ export type ViewProps = {
|
||||
};
|
||||
|
||||
export function DataSourcesListHeaderView({ searchQuery, setSearchQuery, canCreateDataSource }: ViewProps) {
|
||||
const dataSourcesRoutes = useDataSourcesRoutes();
|
||||
const linkButton = {
|
||||
href: 'datasources/new',
|
||||
href: dataSourcesRoutes.New,
|
||||
title: 'Add data source',
|
||||
disabled: !canCreateDataSource,
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import {
|
||||
useLoadDataSourcePlugins,
|
||||
getFilteredDataSourcePlugins,
|
||||
setDataSourceTypeSearchQuery,
|
||||
useDataSourcesRoutes,
|
||||
} from '../state';
|
||||
|
||||
export function NewDataSource() {
|
||||
@ -57,6 +58,8 @@ export function NewDataSourceView({
|
||||
onAddDataSource,
|
||||
onSetSearchQuery,
|
||||
}: ViewProps) {
|
||||
const dataSourcesRoutes = useDataSourcesRoutes();
|
||||
|
||||
if (isLoading) {
|
||||
return <PageLoader />;
|
||||
}
|
||||
@ -67,7 +70,7 @@ export function NewDataSourceView({
|
||||
<div className="page-action-bar">
|
||||
<FilterInput value={searchQuery} onChange={onSetSearchQuery} placeholder="Filter by name or type" />
|
||||
<div className="page-action-bar__spacer" />
|
||||
<LinkButton href="datasources" fill="outline" variant="secondary" icon="arrow-left">
|
||||
<LinkButton href={dataSourcesRoutes.List} fill="outline" variant="secondary" icon="arrow-left">
|
||||
Cancel
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
12
public/app/features/datasources/constants.ts
Normal file
12
public/app/features/datasources/constants.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { DataSourcesRoutes } from './types';
|
||||
|
||||
/**
|
||||
* Default routes for data sources pages.
|
||||
* (Links to the pages can be overriden for this feature by using `DataSourcesRoutesContext`)
|
||||
*/
|
||||
export const DATASOURCES_ROUTES: DataSourcesRoutes = {
|
||||
List: '/datasources',
|
||||
Edit: '/datasources/edit/:uid',
|
||||
Dashboards: '/datasources/edit/:uid/dashboards',
|
||||
New: '/datasources/new',
|
||||
} as const;
|
@ -4,6 +4,7 @@ import { NavModel } from '@grafana/data';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
|
||||
import { NewDataSource } from '../components/NewDataSource';
|
||||
import { DATASOURCES_ROUTES } from '../constants';
|
||||
|
||||
const navModel = getNavModel();
|
||||
|
||||
@ -22,7 +23,7 @@ export function getNavModel(): NavModel {
|
||||
icon: 'database',
|
||||
id: 'datasource-new',
|
||||
text: 'Add data source',
|
||||
href: 'datasources/new',
|
||||
href: DATASOURCES_ROUTES.New,
|
||||
subTitle: 'Choose a data source type',
|
||||
};
|
||||
|
||||
|
@ -16,6 +16,7 @@ import { importDataSourcePlugin } from 'app/features/plugins/plugin_loader';
|
||||
import { DataSourcePluginCategory, ThunkDispatch, ThunkResult } from 'app/types';
|
||||
|
||||
import * as api from '../api';
|
||||
import { DATASOURCES_ROUTES } from '../constants';
|
||||
import { nameExits, findNewName } from '../utils';
|
||||
|
||||
import { buildCategories } from './buildCategories';
|
||||
@ -174,7 +175,7 @@ export function loadDataSourceMeta(dataSource: DataSourceSettings): ThunkResult<
|
||||
};
|
||||
}
|
||||
|
||||
export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> {
|
||||
export function addDataSource(plugin: DataSourcePluginMeta, editLink = DATASOURCES_ROUTES.Edit): ThunkResult<void> {
|
||||
return async (dispatch, getStore) => {
|
||||
await dispatch(loadDataSources());
|
||||
|
||||
@ -196,7 +197,7 @@ export function addDataSource(plugin: DataSourcePluginMeta): ThunkResult<void> {
|
||||
await getDatasourceSrv().reload();
|
||||
await contextSrv.fetchUserPermissions();
|
||||
|
||||
locationService.push(`/datasources/edit/${result.datasource.uid}`);
|
||||
locationService.push(editLink.replace(/:uid/gi, result.datasource.uid));
|
||||
};
|
||||
}
|
||||
|
||||
|
8
public/app/features/datasources/state/contexts.ts
Normal file
8
public/app/features/datasources/state/contexts.ts
Normal file
@ -0,0 +1,8 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
import { DATASOURCES_ROUTES } from '../constants';
|
||||
import { DataSourcesRoutes } from '../types';
|
||||
|
||||
// The purpose of this context is to be able to override the data-sources routes (used for links for example) used under
|
||||
// the app/features/datasources modules, so we can reuse them more easily in different parts of the application (e.g. under Data Connections)
|
||||
export const DataSourcesRoutesContext = createContext<DataSourcesRoutes>(DATASOURCES_ROUTES);
|
@ -1,4 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useContext, useEffect } from 'react';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
|
||||
import { DataSourcePluginMeta, DataSourceSettings, urlUtil } from '@grafana/data';
|
||||
@ -21,6 +21,7 @@ import {
|
||||
updateDataSource,
|
||||
deleteLoadedDataSource,
|
||||
} from './actions';
|
||||
import { DataSourcesRoutesContext } from './contexts';
|
||||
import { getDataSourceLoadingNav, buildNavModel, getDataSourceNav } from './navModel';
|
||||
import { getDataSource, getDataSourceMeta } from './selectors';
|
||||
|
||||
@ -72,9 +73,10 @@ export const useLoadDataSourcePlugins = () => {
|
||||
|
||||
export const useAddDatasource = () => {
|
||||
const dispatch = useDispatch();
|
||||
const dataSourcesRoutes = useDataSourcesRoutes();
|
||||
|
||||
return (plugin: DataSourcePluginMeta) => {
|
||||
dispatch(addDataSource(plugin));
|
||||
dispatch(addDataSource(plugin, dataSourcesRoutes.Edit));
|
||||
};
|
||||
};
|
||||
|
||||
@ -159,3 +161,7 @@ export const useDataSourceRights = (uid: string): DataSourceRights => {
|
||||
hasDeleteRights,
|
||||
};
|
||||
};
|
||||
|
||||
export const useDataSourcesRoutes = () => {
|
||||
return useContext(DataSourcesRoutesContext);
|
||||
};
|
||||
|
@ -1,5 +1,6 @@
|
||||
export * from './actions';
|
||||
export * from './buildCategories';
|
||||
export * from './contexts';
|
||||
export * from './hooks';
|
||||
export * from './navModel';
|
||||
export * from './reducers';
|
||||
|
@ -7,3 +7,10 @@ export type DataSourceRights = {
|
||||
hasWriteRights: boolean;
|
||||
hasDeleteRights: boolean;
|
||||
};
|
||||
|
||||
export type DataSourcesRoutes = {
|
||||
New: string;
|
||||
Edit: string;
|
||||
List: string;
|
||||
Dashboards: string;
|
||||
};
|
||||
|
@ -9,6 +9,7 @@ import UserAdminPage from 'app/features/admin/UserAdminPage';
|
||||
import LdapPage from 'app/features/admin/ldap/LdapPage';
|
||||
import { getAlertingRoutes } from 'app/features/alerting/routes';
|
||||
import { getRoutes as getDataConnectionsRoutes } from 'app/features/data-connections/routes';
|
||||
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
|
||||
import { getLiveRoutes } from 'app/features/live/pages/routes';
|
||||
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
|
||||
import { getProfileRoutes } from 'app/features/profile/routes';
|
||||
@ -90,19 +91,19 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/datasources',
|
||||
path: DATASOURCES_ROUTES.List,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "DataSourcesListPage"*/ 'app/features/datasources/pages/DataSourcesListPage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/datasources/edit/:uid/',
|
||||
path: DATASOURCES_ROUTES.Edit,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "EditDataSourcePage"*/ '../features/datasources/pages/EditDataSourcePage')
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/datasources/edit/:uid/dashboards',
|
||||
path: DATASOURCES_ROUTES.Dashboards,
|
||||
component: SafeDynamicImport(
|
||||
() =>
|
||||
import(
|
||||
@ -111,7 +112,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '/datasources/new',
|
||||
path: DATASOURCES_ROUTES.New,
|
||||
component: SafeDynamicImport(
|
||||
() => import(/* webpackChunkName: "NewDataSourcePage"*/ '../features/datasources/pages/NewDataSourcePage')
|
||||
),
|
||||
|
Loading…
Reference in New Issue
Block a user