Plugins: Pin plugin search and connections search to the page (#109903)
* pin page header for plugins and connections * fix tests
This commit is contained in:
@@ -1,18 +1,29 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { PluginType } from '@grafana/data';
|
||||
import { PluginType, GrafanaTheme2 } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { RoadmapLinks } from 'app/features/plugins/admin/components/RoadmapLinks';
|
||||
import UpdateAllButton from 'app/features/plugins/admin/components/UpdateAllButton';
|
||||
import UpdateAllModal from 'app/features/plugins/admin/components/UpdateAllModal';
|
||||
import { useGetUpdatable } from 'app/features/plugins/admin/state/hooks';
|
||||
|
||||
import { AddNewConnection } from '../tabs/ConnectData';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
pageContainer: css({
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
});
|
||||
|
||||
export function AddNewConnectionPage() {
|
||||
const { isLoading: areUpdatesLoading, updatablePlugins } = useGetUpdatable();
|
||||
const updatableDSPlugins = updatablePlugins.filter((plugin) => plugin.type === PluginType.datasource);
|
||||
const [showUpdateModal, setShowUpdateModal] = useState(false);
|
||||
const disableUpdateAllButton = updatableDSPlugins.length <= 0 || areUpdatesLoading;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const onUpdateAll = () => {
|
||||
setShowUpdateModal(true);
|
||||
@@ -27,9 +38,10 @@ export function AddNewConnectionPage() {
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navId={'connections-add-new-connection'} actions={updateAllButton}>
|
||||
<Page navId={'connections-add-new-connection'} actions={updateAllButton} className={styles.pageContainer}>
|
||||
<Page.Contents>
|
||||
<AddNewConnection />
|
||||
<RoadmapLinks />
|
||||
<UpdateAllModal
|
||||
isOpen={showUpdateModal}
|
||||
isLoading={areUpdatesLoading}
|
||||
|
||||
@@ -167,11 +167,4 @@ describe('Add new connection', () => {
|
||||
await userEvent.click(await screen.findByText('Sample data source'));
|
||||
expect(screen.queryByText(new RegExp(exampleSentenceInModal))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('Show request data source and roadmap links', async () => {
|
||||
renderPage([getCatalogPluginMock(), mockCatalogDataSourcePlugin]);
|
||||
|
||||
expect(await screen.findByText('Request a new data source')).toBeInTheDocument();
|
||||
expect(await screen.findByText('View roadmap')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import { LoadingPlaceholder, EmptyState, Field, RadioButtonGroup, Tooltip, Combo
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { HorizontalGroup } from 'app/features/plugins/admin/components/HorizontalGroup';
|
||||
import { RoadmapLinks } from 'app/features/plugins/admin/components/RoadmapLinks';
|
||||
import { SearchField } from 'app/features/plugins/admin/components/SearchField';
|
||||
import { Sorters } from 'app/features/plugins/admin/helpers';
|
||||
import { useHistory } from 'app/features/plugins/admin/hooks/useHistory';
|
||||
@@ -23,6 +22,17 @@ import { CategoryHeader } from './CategoryHeader';
|
||||
import { NoAccessModal } from './NoAccessModal';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
searchContainer: css({
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
contentWrap: css({
|
||||
height: 'calc(100vh - 350px)',
|
||||
overflowY: 'auto',
|
||||
}),
|
||||
spacer: css({
|
||||
height: theme.spacing(2),
|
||||
}),
|
||||
@@ -145,99 +155,105 @@ export function AddNewConnection() {
|
||||
return (
|
||||
<>
|
||||
{focusedItem && <NoAccessModal item={focusedItem} isOpen={isNoAccessModalOpen} onDismiss={closeModal} />}
|
||||
<HorizontalGroup wrap>
|
||||
<Field label={t('common.search', 'Search')}>
|
||||
<SearchField value={searchTerm} onSearch={handleSearchChange} />
|
||||
</Field>
|
||||
<HorizontalGroup className={styles.actionBar}>
|
||||
{/* Filter by installed / all */}
|
||||
{remotePluginsAvailable ? (
|
||||
<Field label={t('plugins.filter.state', 'State')}>
|
||||
<RadioButtonGroup value={filterBy} onChange={onFilterByChange} options={filterByOptions} />
|
||||
</Field>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={t(
|
||||
'plugins.filter.disabled',
|
||||
'This filter has been disabled because the Grafana server cannot access grafana.com'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<Field label={t('plugins.filter.state', 'State')}>
|
||||
<RadioButtonGroup
|
||||
disabled={true}
|
||||
value={filterBy}
|
||||
onChange={onFilterByChange}
|
||||
options={filterByOptions}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Sorting */}
|
||||
<Field label={t('plugins.filter.sort', 'Sort')}>
|
||||
<Combobox
|
||||
aria-label={t('plugins.filter.sort-list', 'Sort Plugins List')}
|
||||
width={24}
|
||||
value={sortBy?.toString()}
|
||||
onChange={onSortByChange}
|
||||
options={[
|
||||
{ value: 'nameAsc', label: t('connections.add-new-connection.label.by-name-az', 'By name (A-Z)') },
|
||||
{ value: 'nameDesc', label: t('connections.add-new-connection.label.by-name-za', 'By name (Z-A)') },
|
||||
{
|
||||
value: 'updated',
|
||||
label: t('connections.add-new-connection.label.by-updated-date', 'By updated date'),
|
||||
},
|
||||
{
|
||||
value: 'published',
|
||||
label: t('connections.add-new-connection.label.by-published-date', 'By published date'),
|
||||
},
|
||||
{ value: 'downloads', label: t('connections.add-new-connection.label.by-downloads', 'By downloads') },
|
||||
]}
|
||||
/>
|
||||
<div className={styles.searchContainer}>
|
||||
<HorizontalGroup wrap>
|
||||
<Field label={t('common.search', 'Search')}>
|
||||
<SearchField value={searchTerm} onSearch={handleSearchChange} />
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<HorizontalGroup className={styles.actionBar}>
|
||||
{/* Filter by installed / all */}
|
||||
{remotePluginsAvailable ? (
|
||||
<Field label={t('plugins.filter.state', 'State')}>
|
||||
<RadioButtonGroup value={filterBy} onChange={onFilterByChange} options={filterByOptions} />
|
||||
</Field>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={t(
|
||||
'plugins.filter.disabled',
|
||||
'This filter has been disabled because the Grafana server cannot access grafana.com'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<Field label={t('plugins.filter.state', 'State')}>
|
||||
<RadioButtonGroup
|
||||
disabled={true}
|
||||
value={filterBy}
|
||||
onChange={onFilterByChange}
|
||||
options={filterByOptions}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{isLoading ? (
|
||||
<LoadingPlaceholder text={t('common.loading', 'Loading...')} />
|
||||
) : !!error ? (
|
||||
<Trans i18nKey="alerting.policies.update-errors.error-code" values={{ error: error.message }}>
|
||||
Error message: "{{ error: error.message }}"
|
||||
</Trans>
|
||||
) : (
|
||||
<>
|
||||
{/* Data Sources Section */}
|
||||
{dataSourcesPlugins.length > 0 && (
|
||||
<>
|
||||
<CategoryHeader
|
||||
iconName="database"
|
||||
label={t('connections.connect-data.datasources-header', 'Data Sources')}
|
||||
{/* Sorting */}
|
||||
<Field label={t('plugins.filter.sort', 'Sort')}>
|
||||
<Combobox
|
||||
aria-label={t('plugins.filter.sort-list', 'Sort Plugins List')}
|
||||
width={24}
|
||||
value={sortBy?.toString()}
|
||||
onChange={onSortByChange}
|
||||
options={[
|
||||
{ value: 'nameAsc', label: t('connections.add-new-connection.label.by-name-az', 'By name (A-Z)') },
|
||||
{ value: 'nameDesc', label: t('connections.add-new-connection.label.by-name-za', 'By name (Z-A)') },
|
||||
{
|
||||
value: 'updated',
|
||||
label: t('connections.add-new-connection.label.by-updated-date', 'By updated date'),
|
||||
},
|
||||
{
|
||||
value: 'published',
|
||||
label: t('connections.add-new-connection.label.by-published-date', 'By published date'),
|
||||
},
|
||||
{
|
||||
value: 'downloads',
|
||||
label: t('connections.add-new-connection.label.by-downloads', 'By downloads'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<CardGrid items={datasourceCardGridItems} onClickItem={onClickCardGridItem} />
|
||||
</>
|
||||
)}
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={styles.contentWrap}>
|
||||
{isLoading ? (
|
||||
<LoadingPlaceholder text={t('common.loading', 'Loading...')} />
|
||||
) : !!error ? (
|
||||
<Trans i18nKey="alerting.policies.update-errors.error-code" values={{ error: error.message }}>
|
||||
Error message: "{{ error: error.message }}"
|
||||
</Trans>
|
||||
) : (
|
||||
<>
|
||||
{/* Data Sources Section */}
|
||||
{dataSourcesPlugins.length > 0 && (
|
||||
<>
|
||||
<CategoryHeader
|
||||
iconName="database"
|
||||
label={t('connections.connect-data.datasources-header', 'Data Sources')}
|
||||
/>
|
||||
<CardGrid items={datasourceCardGridItems} onClickItem={onClickCardGridItem} />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Apps Section */}
|
||||
{appsPlugins.length > 0 && (
|
||||
<>
|
||||
<div className={styles.spacer} />
|
||||
<CategoryHeader iconName="apps" label={t('connections.connect-data.apps-header', 'Apps')} />
|
||||
<CardGrid items={appsCardGridItems} onClickItem={onClickCardGridItem} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{/* Apps Section */}
|
||||
{appsPlugins.length > 0 && (
|
||||
<>
|
||||
<div className={styles.spacer} />
|
||||
<CategoryHeader iconName="apps" label={t('connections.connect-data.apps-header', 'Apps')} />
|
||||
<CardGrid items={appsCardGridItems} onClickItem={onClickCardGridItem} />
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{showNoResults && (
|
||||
<EmptyState
|
||||
variant="not-found"
|
||||
message={t('connections.connect-data.empty-message', 'No results matching your query were found')}
|
||||
/>
|
||||
)}
|
||||
<RoadmapLinks />
|
||||
{showNoResults && (
|
||||
<EmptyState
|
||||
variant="not-found"
|
||||
message={t('connections.connect-data.empty-message', 'No results matching your query were found')}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -94,57 +94,59 @@ export default function Browse() {
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navModel={navModel} actions={updateAllButton} subTitle={subTitle}>
|
||||
<Page navModel={navModel} actions={updateAllButton} subTitle={subTitle} className={styles.pageContainer}>
|
||||
<Page.Contents>
|
||||
<AdvisorRedirectNotice />
|
||||
<HorizontalGroup wrap>
|
||||
<Field label={t('plugins.browse.label-search', 'Search')}>
|
||||
<SearchField value={keyword} onSearch={onSearch} />
|
||||
</Field>
|
||||
<HorizontalGroup wrap className={styles.actionBar}>
|
||||
{/* Filter by type */}
|
||||
<Field label={t('plugins.browse.label-type', 'Type')}>
|
||||
<Select
|
||||
aria-label={t('plugins.browse.aria-label-plugin-type-filter', 'Plugin type filter')}
|
||||
value={filterByType}
|
||||
onChange={onFilterByTypeChange}
|
||||
width={18}
|
||||
options={[
|
||||
{ value: 'all', label: t('plugins.browse.label.all', 'All') },
|
||||
{ value: 'datasource', label: t('plugins.browse.label.data-sources', 'Data sources') },
|
||||
{ value: 'panel', label: t('plugins.browse.label.panels', 'Panels') },
|
||||
{ value: 'app', label: t('plugins.browse.label.applications', 'Applications') },
|
||||
]}
|
||||
/>
|
||||
<div className={styles.searchContainer}>
|
||||
<HorizontalGroup wrap>
|
||||
<Field label={t('plugins.browse.label-search', 'Search')}>
|
||||
<SearchField value={keyword} onSearch={onSearch} />
|
||||
</Field>
|
||||
|
||||
{/* Filter by installed / all */}
|
||||
{remotePluginsAvailable ? (
|
||||
<Field label={t('plugins.browse.label-state', 'State')}>
|
||||
<RadioButtonGroup value={filterBy} onChange={onFilterByChange} options={filterByOptions} />
|
||||
<HorizontalGroup wrap className={styles.actionBar}>
|
||||
{/* Filter by type */}
|
||||
<Field label={t('plugins.browse.label-type', 'Type')}>
|
||||
<Select
|
||||
aria-label={t('plugins.browse.aria-label-plugin-type-filter', 'Plugin type filter')}
|
||||
value={filterByType}
|
||||
onChange={onFilterByTypeChange}
|
||||
width={18}
|
||||
options={[
|
||||
{ value: 'all', label: t('plugins.browse.label.all', 'All') },
|
||||
{ value: 'datasource', label: t('plugins.browse.label.data-sources', 'Data sources') },
|
||||
{ value: 'panel', label: t('plugins.browse.label.panels', 'Panels') },
|
||||
{ value: 'app', label: t('plugins.browse.label.applications', 'Applications') },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={t(
|
||||
'plugins.browse.tooltip-filter-disabled',
|
||||
'This filter has been disabled because the Grafana server cannot access grafana.com'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<Field label={t('plugins.browse.label-state', 'State')}>
|
||||
<RadioButtonGroup
|
||||
disabled={true}
|
||||
value={filterBy}
|
||||
onChange={onFilterByChange}
|
||||
options={filterByOptions}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
|
||||
{/* Filter by installed / all */}
|
||||
{remotePluginsAvailable ? (
|
||||
<Field label={t('plugins.browse.label-state', 'State')}>
|
||||
<RadioButtonGroup value={filterBy} onChange={onFilterByChange} options={filterByOptions} />
|
||||
</Field>
|
||||
) : (
|
||||
<Tooltip
|
||||
content={t(
|
||||
'plugins.browse.tooltip-filter-disabled',
|
||||
'This filter has been disabled because the Grafana server cannot access grafana.com'
|
||||
)}
|
||||
placement="top"
|
||||
>
|
||||
<div>
|
||||
<Field label={t('plugins.browse.label-state', 'State')}>
|
||||
<RadioButtonGroup
|
||||
disabled={true}
|
||||
value={filterBy}
|
||||
onChange={onFilterByChange}
|
||||
options={filterByOptions}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
</div>
|
||||
<div className={styles.listWrap}>
|
||||
<PluginList plugins={plugins} isLoading={isLoading} />
|
||||
</div>
|
||||
@@ -161,14 +163,26 @@ export default function Browse() {
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
pageContainer: css({
|
||||
height: '100vh',
|
||||
overflow: 'hidden',
|
||||
}),
|
||||
searchContainer: css({
|
||||
backgroundColor: theme.colors.background.primary,
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
borderBottom: `1px solid ${theme.colors.border.weak}`,
|
||||
marginBottom: theme.spacing(2),
|
||||
}),
|
||||
listWrap: css({
|
||||
height: 'calc(100vh - 350px)',
|
||||
overflowY: 'auto',
|
||||
}),
|
||||
actionBar: css({
|
||||
[theme.breakpoints.up('xl')]: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
}),
|
||||
listWrap: css({
|
||||
marginTop: theme.spacing(2),
|
||||
}),
|
||||
displayAs: css({
|
||||
svg: {
|
||||
marginRight: 0,
|
||||
|
||||
Reference in New Issue
Block a user