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