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:
Yulia Shanyrova
2025-09-02 13:57:58 +02:00
committed by GitHub
parent 9d9f464679
commit 0c44a0c14a
4 changed files with 181 additions and 146 deletions
@@ -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,