mirror of https://github.com/grafana/grafana
Feat: Data source connections tab in plugin detail page (#99049)
* feat: datasource connections tab in plugin detail page * test: no ds defined test added * feat: configure feature toggle * chore: i18n extract * test: added unit tests for page and listpull/99337/head
parent
b11d3bc045
commit
97d8f68b70
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,127 @@ |
||||
import { screen } from '@testing-library/react'; |
||||
import { render } from 'test/test-utils'; |
||||
|
||||
import { DataSourceSettings } from '@grafana/data'; |
||||
import { config } from '@grafana/runtime'; |
||||
import { ContextSrv, setContextSrv } from 'app/core/services/context_srv'; |
||||
import { getMockDataSources } from 'app/features/datasources/__mocks__'; |
||||
import { AccessControlAction } from 'app/types'; |
||||
|
||||
import { datasourcePlugin } from '../__mocks__/catalogPlugin.mock'; |
||||
|
||||
import ConnectionsTab, { ConnectionsList } from './ConnectionsTab'; |
||||
|
||||
jest.mock('app/features/datasources/state', () => ({ |
||||
...jest.requireActual('app/features/datasources/state'), |
||||
useLoadDataSource: jest.fn().mockReturnValue({ isLoading: false }), |
||||
getDataSources: jest.fn(() => 'getDataSources mock implementation'), |
||||
})); |
||||
|
||||
const setupContextSrv = () => { |
||||
const testContextSrv = new ContextSrv(); |
||||
testContextSrv.user.permissions = { |
||||
[AccessControlAction.DataSourcesCreate]: true, |
||||
[AccessControlAction.DataSourcesWrite]: true, |
||||
[AccessControlAction.DataSourcesExplore]: true, |
||||
}; |
||||
|
||||
setContextSrv(testContextSrv); |
||||
}; |
||||
|
||||
describe('<ConnectionsTab>', () => { |
||||
const oldExporeEnabled = config.exploreEnabled; |
||||
const olddatasourceConnectionsTab = config.featureToggles.datasourceConnectionsTab; |
||||
config.exploreEnabled = true; |
||||
config.featureToggles.datasourceConnectionsTab = true; |
||||
afterEach(() => { |
||||
jest.clearAllMocks(); |
||||
}); |
||||
|
||||
afterAll(() => { |
||||
config.exploreEnabled = oldExporeEnabled; |
||||
config.featureToggles.datasourceConnectionsTab = olddatasourceConnectionsTab; |
||||
}); |
||||
|
||||
it('should onnly render list of datasources with type=plugin.id', async () => { |
||||
setupContextSrv(); |
||||
const mockedConnections = getMockDataSources(3, { type: datasourcePlugin.id }); |
||||
mockedConnections[2].type = 'other-plugin-id'; |
||||
jest.requireMock('app/features/datasources/state').getDataSources.mockReturnValue(mockedConnections); |
||||
|
||||
render(<ConnectionsTab plugin={datasourcePlugin} />); |
||||
|
||||
expect(await screen.findAllByRole('listitem')).toHaveLength(2); |
||||
expect(await screen.findAllByRole('heading')).toHaveLength(2); |
||||
expect(await screen.findByRole('link', { name: /Connections - Data sources/i })).toBeVisible(); |
||||
expect(await screen.findAllByRole('link', { name: /Build a dashboard/i })).toHaveLength(2); |
||||
expect(await screen.findAllByRole('link', { name: 'Explore' })).toHaveLength(2); |
||||
}); |
||||
|
||||
it('should render add new datasource button when no datasources are defined', async () => { |
||||
setupContextSrv(); |
||||
jest.requireMock('app/features/datasources/state').getDataSources.mockReturnValue(getMockDataSources(1)); |
||||
render(<ConnectionsTab plugin={datasourcePlugin} />); |
||||
|
||||
expect(screen.getByText('Add new data source')).toBeVisible(); |
||||
expect(screen.getByText(`No data sources defined`)).toBeVisible(); |
||||
}); |
||||
|
||||
describe('<ConnectionsList>', () => { |
||||
it('should render list of datasources', async () => { |
||||
const dss = getMockDataSources(2, { type: datasourcePlugin.id }); |
||||
render( |
||||
<ConnectionsList |
||||
plugin={datasourcePlugin} |
||||
hasExploreRights={true} |
||||
isLoading={false} |
||||
hasWriteRights={true} |
||||
dataSources={dss} |
||||
dataSourcesCount={dss.length} |
||||
/> |
||||
); |
||||
|
||||
expect(await screen.findAllByRole('listitem')).toHaveLength(2); |
||||
expect(await screen.findAllByRole('heading')).toHaveLength(2); |
||||
expect(await screen.findByRole('link', { name: /Connections - Data sources/i })).toBeVisible(); |
||||
expect(await screen.findAllByRole('link', { name: /Build a dashboard/i })).toHaveLength(2); |
||||
expect(await screen.findAllByRole('link', { name: 'Explore' })).toHaveLength(2); |
||||
}); |
||||
|
||||
it('should not render Explore button when user has no access', async () => { |
||||
const dss = getMockDataSources(2, { type: datasourcePlugin.id }); |
||||
render( |
||||
<ConnectionsList |
||||
plugin={datasourcePlugin} |
||||
hasExploreRights={false} |
||||
isLoading={false} |
||||
hasWriteRights={true} |
||||
dataSources={dss} |
||||
dataSourcesCount={dss.length} |
||||
/> |
||||
); |
||||
|
||||
expect(await screen.findAllByRole('listitem')).toHaveLength(2); |
||||
expect(await screen.findAllByRole('heading')).toHaveLength(2); |
||||
expect(await screen.findByRole('link', { name: /Connections - Data sources/i })).toBeVisible(); |
||||
expect(await screen.findAllByRole('link', { name: /Build a dashboard/i })).toHaveLength(2); |
||||
expect(screen.queryAllByRole('link', { name: 'Explore' })).toHaveLength(0); |
||||
}); |
||||
|
||||
it('should render add new datasource button when no datasources are defined', async () => { |
||||
const dss = [] as DataSourceSettings[]; |
||||
render( |
||||
<ConnectionsList |
||||
plugin={datasourcePlugin} |
||||
hasExploreRights={true} |
||||
isLoading={false} |
||||
hasWriteRights={true} |
||||
dataSources={dss} |
||||
dataSourcesCount={dss.length} |
||||
/> |
||||
); |
||||
|
||||
expect(screen.getByText('Add new data source')).toBeVisible(); |
||||
expect(screen.getByText(`No data sources defined`)).toBeVisible(); |
||||
}); |
||||
}); |
||||
}); |
@ -0,0 +1,107 @@ |
||||
import { css } from '@emotion/css'; |
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data'; |
||||
import { EmptyState, Stack, TextLink, useStyles2 } from '@grafana/ui'; |
||||
import { t, Trans } from 'app/core/internationalization'; |
||||
import { contextSrv } from 'app/core/services/context_srv'; |
||||
import { ViewProps } from 'app/features/datasources/components/DataSourcesList'; |
||||
import { DataSourcesListCard } from 'app/features/datasources/components/DataSourcesListCard'; |
||||
import { getDataSources, useLoadDataSources } from 'app/features/datasources/state'; |
||||
import { AccessControlAction, useSelector } from 'app/types'; |
||||
|
||||
import { CatalogPlugin } from '../types'; |
||||
|
||||
import { GetStartedWithDataSource } from './GetStartedWithPlugin/GetStartedWithDataSource'; |
||||
|
||||
interface Props { |
||||
plugin: CatalogPlugin; |
||||
} |
||||
|
||||
export default function ConnectionsTab({ plugin }: Props) { |
||||
const { isLoading } = useLoadDataSources(); |
||||
|
||||
const allDataSources = useSelector((state) => getDataSources(state.dataSources)); |
||||
const dataSources = allDataSources.filter((ds) => ds.type === plugin.id); |
||||
const hasWriteRights = contextSrv.hasPermission(AccessControlAction.DataSourcesWrite); |
||||
const hasExploreRights = contextSrv.hasAccessToExplore(); |
||||
|
||||
return ( |
||||
<ConnectionsList |
||||
dataSources={dataSources} |
||||
dataSourcesCount={dataSources.length} |
||||
isLoading={isLoading} |
||||
plugin={plugin} |
||||
hasWriteRights={hasWriteRights} |
||||
hasExploreRights={hasExploreRights} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
type ListProps = Omit<ViewProps, 'hasCreateRights'> & { |
||||
plugin: CatalogPlugin; |
||||
}; |
||||
|
||||
export function ConnectionsList({ |
||||
dataSources, |
||||
dataSourcesCount, |
||||
isLoading, |
||||
hasWriteRights, |
||||
hasExploreRights, |
||||
plugin, |
||||
}: ListProps) { |
||||
const styles = useStyles2(getStyles); |
||||
|
||||
if (!isLoading && dataSourcesCount === 0) { |
||||
return ( |
||||
<EmptyState |
||||
variant="call-to-action" |
||||
button={<GetStartedWithDataSource plugin={plugin} />} |
||||
message={t('data-source-list.empty-state.title', 'No data sources defined')} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
const getDataSourcesList = () => { |
||||
if (isLoading) { |
||||
return new Array(5) |
||||
.fill(null) |
||||
.map((_, index) => <DataSourcesListCard.Skeleton key={index} hasExploreRights={hasExploreRights} />); |
||||
} |
||||
|
||||
return dataSources.map((dataSource) => ( |
||||
<li key={dataSource.uid}> |
||||
<DataSourcesListCard |
||||
dataSource={dataSource} |
||||
hasWriteRights={hasWriteRights} |
||||
hasExploreRights={hasExploreRights} |
||||
/> |
||||
</li> |
||||
)); |
||||
}; |
||||
|
||||
return ( |
||||
<Stack direction="column" gap={2}> |
||||
<span> |
||||
<Trans i18nKey="plugins.details.connections-tab.description" values={{ pluginName: plugin.name }}> |
||||
The data source connections below are all {'{{pluginName}}'}. You can find all of your data source connections |
||||
of all types in{' '} |
||||
<TextLink href="/connections/datasources"> |
||||
<Trans i18nKey="nav.connections.title">Connections</Trans> -{' '} |
||||
<Trans i18nKey="nav.data-sources.title">Data sources</Trans>. |
||||
</TextLink> |
||||
</Trans> |
||||
</span> |
||||
<ul className={styles.list}>{getDataSourcesList()}</ul> |
||||
</Stack> |
||||
); |
||||
} |
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => { |
||||
return { |
||||
list: css({ |
||||
listStyle: 'none', |
||||
display: 'grid', |
||||
// gap: '8px', Add back when legacy support for old Card interface is dropped
|
||||
}), |
||||
}; |
||||
}; |
Loading…
Reference in new issue