Feat: OSS connections page state filter and update all added (#100688)
* feat: OSS connections page state filter and update all added * fix: use combobox instead of select * fix: show no updates available text * ref: extract update all button to a component
This commit is contained in:
+1
-2
@@ -3144,8 +3144,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use export all (\`export * from ...\`)", "0"]
|
||||
],
|
||||
"public/app/features/connections/tabs/ConnectData/ConnectData.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/connections/tabs/ConnectData/NoAccessModal/NoAccessModal.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings in text props. Wrap text with <Trans /> or use t()", "0"],
|
||||
|
||||
@@ -1,12 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { PluginType } from '@grafana/data';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
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';
|
||||
|
||||
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 onUpdateAll = () => {
|
||||
setShowUpdateModal(true);
|
||||
};
|
||||
|
||||
const updateAllButton = (
|
||||
<UpdateAllButton
|
||||
disabled={disableUpdateAllButton}
|
||||
onUpdateAll={onUpdateAll}
|
||||
updatablePluginsLength={updatableDSPlugins.length}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navId={'connections-add-new-connection'}>
|
||||
<Page navId={'connections-add-new-connection'} actions={updateAllButton}>
|
||||
<Page.Contents>
|
||||
<AddNewConnection />
|
||||
<UpdateAllModal
|
||||
isOpen={showUpdateModal}
|
||||
isLoading={areUpdatesLoading}
|
||||
onDismiss={() => setShowUpdateModal(false)}
|
||||
plugins={updatableDSPlugins}
|
||||
/>
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
|
||||
@@ -2,8 +2,19 @@ import { css } from '@emotion/css';
|
||||
import * as React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Card, Grid, useStyles2 } from '@grafana/ui';
|
||||
import { PluginAngularBadge } from 'app/features/plugins/admin/components/Badges';
|
||||
import { featureEnabled } from '@grafana/runtime';
|
||||
import { Card, Grid, useStyles2, Stack, Badge } from '@grafana/ui';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import {
|
||||
PluginAngularBadge,
|
||||
PluginDeprecatedBadge,
|
||||
PluginDisabledBadge,
|
||||
PluginInstalledBadge,
|
||||
PluginUpdateAvailableBadge,
|
||||
} from 'app/features/plugins/admin/components/Badges';
|
||||
import { getBadgeColor } from 'app/features/plugins/admin/components/Badges/sharedStyles';
|
||||
import { isPluginUpdatable } from 'app/features/plugins/admin/helpers';
|
||||
import { CatalogPlugin } from 'app/features/plugins/admin/types';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
heading: css({
|
||||
@@ -40,13 +51,28 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
}),
|
||||
});
|
||||
|
||||
export type CardGridItem = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
url: string;
|
||||
function PluginEnterpriseBadgeWithoutSignature() {
|
||||
const customBadgeStyles = useStyles2(getBadgeColor);
|
||||
|
||||
if (featureEnabled('enterprise.plugins')) {
|
||||
return <Badge text={t('get-enterprise.title', 'Enterprise')} color="blue" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Badge
|
||||
icon="lock"
|
||||
role="img"
|
||||
aria-label={t('lock-icon', 'lock icon')}
|
||||
text={t('get-enterprise.title', 'Enterprise')}
|
||||
color="darkgrey"
|
||||
className={customBadgeStyles}
|
||||
title={t('get-enterprise.requires-license', 'Requires a Grafana Enterprise license')}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export type CardGridItem = CatalogPlugin & {
|
||||
logo?: string;
|
||||
angularDetected?: boolean;
|
||||
};
|
||||
|
||||
export interface CardGridProps {
|
||||
@@ -75,12 +101,16 @@ export const CardGrid = ({ items, onClickItem }: CardGridProps) => {
|
||||
<Card.Figure align="center" className={styles.figure}>
|
||||
<img className={styles.logo} src={item.logo} alt="" />
|
||||
</Card.Figure>
|
||||
|
||||
{item.angularDetected ? (
|
||||
<Card.Meta className={styles.meta}>
|
||||
<PluginAngularBadge />
|
||||
</Card.Meta>
|
||||
) : null}
|
||||
<Card.Meta className={styles.meta}>
|
||||
<Stack height="auto" wrap="wrap">
|
||||
{item.isEnterprise && <PluginEnterpriseBadgeWithoutSignature />}
|
||||
{item.isDeprecated && <PluginDeprecatedBadge />}
|
||||
{item.isInstalled && <PluginInstalledBadge />}
|
||||
{item.isDisabled && <PluginDisabledBadge error={item.error} />}
|
||||
{isPluginUpdatable(item) && <PluginUpdateAvailableBadge plugin={item} />}
|
||||
{item.angularDetected && <PluginAngularBadge />}
|
||||
</Stack>
|
||||
</Card.Meta>
|
||||
</Card>
|
||||
))}
|
||||
</Grid>
|
||||
|
||||
@@ -3,6 +3,7 @@ import userEvent from '@testing-library/user-event';
|
||||
import { TestProvider } from 'test/helpers/TestProvider';
|
||||
|
||||
import { PluginType } from '@grafana/data';
|
||||
import { locationService } from '@grafana/runtime';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { getCatalogPluginMock, getPluginsStateMock } from 'app/features/plugins/admin/__mocks__';
|
||||
import { CatalogPlugin } from 'app/features/plugins/admin/types';
|
||||
@@ -17,7 +18,8 @@ jest.mock('@grafana/runtime', () => ({
|
||||
useChromeHeaderHeight: jest.fn(),
|
||||
}));
|
||||
|
||||
const renderPage = (plugins: CatalogPlugin[] = []): RenderResult => {
|
||||
const renderPage = (plugins: CatalogPlugin[] = [], path = '/add-new-connection'): RenderResult => {
|
||||
locationService.push(path);
|
||||
return render(
|
||||
<TestProvider storeState={{ plugins: getPluginsStateMock(plugins) }}>
|
||||
<AddNewConnection />
|
||||
@@ -31,7 +33,7 @@ const mockCatalogDataSourcePlugin = getCatalogPluginMock({
|
||||
id: 'sample-data-source',
|
||||
});
|
||||
|
||||
describe('Angular badge', () => {
|
||||
describe('Badges', () => {
|
||||
test('does not show angular badge for non-angular plugins', async () => {
|
||||
renderPage([
|
||||
getCatalogPluginMock({
|
||||
@@ -61,6 +63,28 @@ describe('Angular badge', () => {
|
||||
});
|
||||
expect(screen.queryByText('Angular')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('shows enterprise and deprecated badges for plugins', async () => {
|
||||
renderPage([
|
||||
getCatalogPluginMock({
|
||||
id: 'test-plugin',
|
||||
name: 'test Plugin',
|
||||
type: PluginType.datasource,
|
||||
isEnterprise: true,
|
||||
}),
|
||||
getCatalogPluginMock({
|
||||
id: 'test2-plugin',
|
||||
name: 'test2 Plugin',
|
||||
type: PluginType.datasource,
|
||||
isDeprecated: true,
|
||||
}),
|
||||
]);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('test Plugin')).toBeInTheDocument();
|
||||
});
|
||||
expect(screen.queryByText('Enterprise')).toBeVisible();
|
||||
expect(screen.queryByText('Deprecated')).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Add new connection', () => {
|
||||
@@ -81,20 +105,57 @@ describe('Add new connection', () => {
|
||||
|
||||
expect(await screen.findByText('Sample data source')).toBeVisible();
|
||||
});
|
||||
test('should list plugins with update when filtering by update', async () => {
|
||||
const { queryByText } = renderPage(
|
||||
[
|
||||
getCatalogPluginMock({
|
||||
id: 'plugin-1',
|
||||
name: 'Plugin 1',
|
||||
isInstalled: true,
|
||||
hasUpdate: true,
|
||||
type: PluginType.datasource,
|
||||
}),
|
||||
getCatalogPluginMock({ id: 'plugin-2', name: 'Plugin 2', isInstalled: false }),
|
||||
getCatalogPluginMock({
|
||||
id: 'plugin-3',
|
||||
name: 'Plugin 3',
|
||||
isInstalled: true,
|
||||
hasUpdate: true,
|
||||
type: PluginType.datasource,
|
||||
}),
|
||||
getCatalogPluginMock({ id: 'plugin-4', name: 'Plugin 4', isInstalled: true, isCore: true }),
|
||||
],
|
||||
'/add-new-connection?filterBy=has-update'
|
||||
);
|
||||
|
||||
await waitFor(() => expect(queryByText('Plugin 1')).toBeInTheDocument());
|
||||
expect(queryByText('Plugin 3')).toBeInTheDocument();
|
||||
|
||||
expect(queryByText('Plugin 2')).not.toBeInTheDocument();
|
||||
expect(queryByText('Plugin 4')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders card if search term matches', async () => {
|
||||
renderPage([getCatalogPluginMock(), mockCatalogDataSourcePlugin]);
|
||||
const searchField = await screen.findByRole('textbox');
|
||||
renderPage(
|
||||
[
|
||||
getCatalogPluginMock({ type: PluginType.datasource, id: 'test1', name: 'test33' }),
|
||||
getCatalogPluginMock({ id: 'test2', type: PluginType.datasource, name: 'querymatches' }),
|
||||
],
|
||||
'/add-new-connection?filterBy=all&sortBy=nameAsc&search=querymatches'
|
||||
);
|
||||
expect(await screen.findByText('querymatches')).toBeVisible();
|
||||
});
|
||||
|
||||
await userEvent.type(searchField, 'ampl');
|
||||
expect(await screen.findByText('Sample data source')).toBeVisible();
|
||||
test('renders no results if search term does not match', async () => {
|
||||
renderPage(
|
||||
[
|
||||
getCatalogPluginMock({ type: PluginType.datasource, id: 'test1', name: 'test33' }),
|
||||
getCatalogPluginMock({ id: 'test2', type: PluginType.datasource, name: 'querymatches' }),
|
||||
],
|
||||
'/add-new-connection?filterBy=all&sortBy=nameAsc&search=dfvdfv'
|
||||
);
|
||||
|
||||
await userEvent.clear(searchField);
|
||||
await userEvent.type(searchField, 'cramp');
|
||||
expect(screen.queryByText('No results matching your query were found')).toBeInTheDocument();
|
||||
|
||||
await userEvent.clear(searchField);
|
||||
expect(await screen.findByText('Sample data source')).toBeVisible();
|
||||
expect(await screen.findByText('No results matching your query were found')).toBeVisible();
|
||||
});
|
||||
|
||||
test('shows a "No access" modal if the user does not have permissions to create datasources', async () => {
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useMemo, useState, FormEvent, MouseEvent } from 'react';
|
||||
import { useMemo, useState, MouseEvent } from 'react';
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { GrafanaTheme2, PluginType } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { useStyles2, LoadingPlaceholder, EmptyState } from '@grafana/ui';
|
||||
import { PluginType, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { locationSearchToObject, reportInteraction } from '@grafana/runtime';
|
||||
import { LoadingPlaceholder, EmptyState, Field, RadioButtonGroup, Tooltip, Combobox, useStyles2 } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { useQueryParams } from 'app/core/hooks/useQueryParams';
|
||||
import { t } from 'app/core/internationalization';
|
||||
import { t, Trans } from 'app/core/internationalization';
|
||||
import { HorizontalGroup } from 'app/features/plugins/admin/components/HorizontalGroup';
|
||||
import { RoadmapLinks } from 'app/features/plugins/admin/components/RoadmapLinks';
|
||||
import { useGetAll } from 'app/features/plugins/admin/state/hooks';
|
||||
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';
|
||||
import { useGetAll, useIsRemotePluginsAvailable } from 'app/features/plugins/admin/state/hooks';
|
||||
import { AccessControlAction } from 'app/types';
|
||||
|
||||
import { ROUTES } from '../../constants';
|
||||
@@ -16,7 +21,6 @@ import { ROUTES } from '../../constants';
|
||||
import { CardGrid, type CardGridItem } from './CardGrid';
|
||||
import { CategoryHeader } from './CategoryHeader';
|
||||
import { NoAccessModal } from './NoAccessModal';
|
||||
import { Search } from './Search';
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
spacer: css({
|
||||
@@ -28,6 +32,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
modalContent: css({
|
||||
overflow: 'visible',
|
||||
}),
|
||||
actionBar: css({
|
||||
[theme.breakpoints.up('xl')]: {
|
||||
marginLeft: 'auto',
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
export function AddNewConnection() {
|
||||
@@ -35,32 +44,34 @@ export function AddNewConnection() {
|
||||
const searchTerm = queryParams.search ? String(queryParams.search) : '';
|
||||
const [isNoAccessModalOpen, setIsNoAccessModalOpen] = useState(false);
|
||||
const [focusedItem, setFocusedItem] = useState<CardGridItem | null>(null);
|
||||
const styles = useStyles2(getStyles);
|
||||
const location = useLocation();
|
||||
const history = useHistory();
|
||||
const locationSearch = locationSearchToObject(location.search);
|
||||
const sortBy = (locationSearch.sortBy as Sorters) || Sorters.nameAsc;
|
||||
const filterBy = locationSearch.filterBy?.toString() || 'all';
|
||||
const canCreateDataSources = contextSrv.hasPermission(AccessControlAction.DataSourcesCreate);
|
||||
|
||||
const handleSearchChange = (e: FormEvent<HTMLInputElement>) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const handleSearchChange = (val: string) => {
|
||||
setQueryParams({
|
||||
search: e.currentTarget.value.toLowerCase(),
|
||||
search: val,
|
||||
});
|
||||
};
|
||||
const remotePluginsAvailable = useIsRemotePluginsAvailable();
|
||||
|
||||
const { error, plugins, isLoading } = useGetAll({
|
||||
keyword: searchTerm,
|
||||
type: PluginType.datasource,
|
||||
});
|
||||
|
||||
const cardGridItems = useMemo(
|
||||
() =>
|
||||
plugins.map((plugin) => ({
|
||||
id: plugin.id,
|
||||
name: plugin.name,
|
||||
description: plugin.description,
|
||||
logo: plugin.info.logos.small,
|
||||
url: ROUTES.DataSourcesDetails.replace(':id', plugin.id),
|
||||
angularDetected: plugin.angularDetected,
|
||||
})),
|
||||
[plugins]
|
||||
const { error, plugins, isLoading } = useGetAll(
|
||||
{
|
||||
keyword: searchTerm,
|
||||
type: PluginType.datasource,
|
||||
isInstalled: filterBy === 'installed' ? true : undefined,
|
||||
hasUpdate: filterBy === 'has-update' ? true : undefined,
|
||||
},
|
||||
sortBy
|
||||
);
|
||||
const filterByOptions = [
|
||||
{ value: 'all', label: 'All' },
|
||||
{ value: 'installed', label: 'Installed' },
|
||||
{ value: 'has-update', label: 'New Updates' },
|
||||
];
|
||||
|
||||
const onClickCardGridItem = (e: MouseEvent<HTMLElement>, item: CardGridItem) => {
|
||||
if (!canCreateDataSources) {
|
||||
@@ -85,20 +96,86 @@ export function AddNewConnection() {
|
||||
setFocusedItem(null);
|
||||
};
|
||||
|
||||
const cardGridItems = useMemo(
|
||||
() =>
|
||||
plugins.map((plugin) => ({
|
||||
...plugin,
|
||||
logo: plugin.info.logos.small,
|
||||
url: ROUTES.DataSourcesDetails.replace(':id', plugin.id),
|
||||
})),
|
||||
[plugins]
|
||||
);
|
||||
|
||||
const onSortByChange = (value: SelectableValue<string>) => {
|
||||
history.push({ query: { sortBy: value.value } });
|
||||
};
|
||||
|
||||
const onFilterByChange = (value: string) => {
|
||||
history.push({ query: { filterBy: value } });
|
||||
};
|
||||
|
||||
const showNoResults = useMemo(() => !isLoading && !error && plugins.length < 1, [isLoading, error, plugins]);
|
||||
const categoryHeaderLabel = t('connections.connect-data.category-header-label', 'Data sources');
|
||||
|
||||
return (
|
||||
<>
|
||||
{focusedItem && <NoAccessModal item={focusedItem} isOpen={isNoAccessModalOpen} onDismiss={closeModal} />}
|
||||
<Search onChange={handleSearchChange} value={searchTerm} />
|
||||
{/* We need this extra spacing when there are no filters */}
|
||||
<div className={styles.spacer} />
|
||||
<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: 'By name (A-Z)' },
|
||||
{ value: 'nameDesc', label: 'By name (Z-A)' },
|
||||
{ value: 'updated', label: 'By updated date' },
|
||||
{ value: 'published', label: 'By published date' },
|
||||
{ value: 'downloads', label: 'By downloads' },
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
</HorizontalGroup>
|
||||
</HorizontalGroup>
|
||||
<CategoryHeader iconName="database" label={categoryHeaderLabel} />
|
||||
{isLoading ? (
|
||||
<LoadingPlaceholder text="Loading..." />
|
||||
<LoadingPlaceholder text={t('common.loading', 'Loading...')} />
|
||||
) : !!error ? (
|
||||
<p>Error: {error.message}</p>
|
||||
<Trans i18nKey="alerting.policies.update-errors.error-code" values={{ error: error.message }}>
|
||||
Error message: "{{ error: error.message }}"
|
||||
</Trans>
|
||||
) : (
|
||||
<CardGrid items={cardGridItems} onClickItem={onClickCardGridItem} />
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { render, screen, fireEvent } from '@testing-library/react';
|
||||
|
||||
import UpdateAllButton from './UpdateAllButton';
|
||||
|
||||
describe('UpdateAllButton', () => {
|
||||
const onUpdateAllMock = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
onUpdateAllMock.mockClear();
|
||||
});
|
||||
|
||||
it('should display "No updates available" when disabled', () => {
|
||||
render(<UpdateAllButton disabled={true} onUpdateAll={onUpdateAllMock} updatablePluginsLength={0} />);
|
||||
|
||||
expect(screen.getByText('No updates available')).toBeInTheDocument();
|
||||
expect(screen.getByRole('button')).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should display update count and be clickable when enabled', () => {
|
||||
render(<UpdateAllButton disabled={false} onUpdateAll={onUpdateAllMock} updatablePluginsLength={3} />);
|
||||
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveTextContent('Update all (3)');
|
||||
expect(button).toBeEnabled();
|
||||
|
||||
fireEvent.click(button);
|
||||
expect(onUpdateAllMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
import { Button } from '@grafana/ui';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
|
||||
interface UpdateAllButtonProps {
|
||||
disabled: boolean;
|
||||
onUpdateAll: () => void;
|
||||
updatablePluginsLength: number;
|
||||
}
|
||||
|
||||
const UpdateAllButton = ({ disabled, onUpdateAll, updatablePluginsLength }: UpdateAllButtonProps) => {
|
||||
return (
|
||||
<Button disabled={disabled} onClick={onUpdateAll}>
|
||||
{disabled ? (
|
||||
<Trans i18nKey="plugins.catalog.no-updates-available">No updates available</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="plugins.catalog.update-all.button" values={{ length: updatablePluginsLength }}>
|
||||
Update all ({{ length }})
|
||||
</Trans>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
export default UpdateAllButton;
|
||||
@@ -4,9 +4,8 @@ import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { SelectableValue, GrafanaTheme2, PluginType } from '@grafana/data';
|
||||
import { locationSearchToObject } from '@grafana/runtime';
|
||||
import { Select, RadioButtonGroup, useStyles2, Tooltip, Field, Button } from '@grafana/ui';
|
||||
import { Select, RadioButtonGroup, useStyles2, Tooltip, Field } from '@grafana/ui';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
import { Trans } from 'app/core/internationalization';
|
||||
import { getNavModel } from 'app/core/selectors/navModel';
|
||||
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
|
||||
import { useSelector } from 'app/types';
|
||||
@@ -15,6 +14,7 @@ import { HorizontalGroup } from '../components/HorizontalGroup';
|
||||
import { PluginList } from '../components/PluginList';
|
||||
import { RoadmapLinks } from '../components/RoadmapLinks';
|
||||
import { SearchField } from '../components/SearchField';
|
||||
import UpdateAllButton from '../components/UpdateAllButton';
|
||||
import { UpdateAllModal } from '../components/UpdateAllModal';
|
||||
import { Sorters } from '../helpers';
|
||||
import { useHistory } from '../hooks/useHistory';
|
||||
@@ -86,15 +86,17 @@ export default function Browse() {
|
||||
.
|
||||
</div>
|
||||
);
|
||||
const updateAll = (
|
||||
<Button disabled={disableUpdateAllButton} onClick={onUpdateAll}>
|
||||
<Trans i18nKey="plugins.catalog.update-all.button">Update all</Trans>
|
||||
{disableUpdateAllButton ? '' : ` (${updatablePlugins.length})`}
|
||||
</Button>
|
||||
|
||||
const updateAllButton = (
|
||||
<UpdateAllButton
|
||||
disabled={disableUpdateAllButton}
|
||||
onUpdateAll={onUpdateAll}
|
||||
updatablePluginsLength={updatablePlugins.length}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Page navModel={navModel} actions={updateAll} subTitle={subTitle}>
|
||||
<Page navModel={navModel} actions={updateAllButton} subTitle={subTitle}>
|
||||
<Page.Contents>
|
||||
<HorizontalGroup wrap>
|
||||
<Field label="Search">
|
||||
|
||||
@@ -810,6 +810,7 @@
|
||||
"collapse": "Collapse",
|
||||
"edit": "Edit",
|
||||
"help": "Help",
|
||||
"loading": "Loading...",
|
||||
"locale": {
|
||||
"default": "Default"
|
||||
},
|
||||
@@ -1642,6 +1643,10 @@
|
||||
"incomplete-request-error": "Sorry, I was unable to complete your request. Please try again.",
|
||||
"send-custom-feedback": "Send"
|
||||
},
|
||||
"get-enterprise": {
|
||||
"requires-license": "Requires a Grafana Enterprise license",
|
||||
"title": "Enterprise"
|
||||
},
|
||||
"grafana-ui": {
|
||||
"action-editor": {
|
||||
"button": {
|
||||
@@ -2045,6 +2050,7 @@
|
||||
"render-image-error-description": "An error occurred when generating the image"
|
||||
}
|
||||
},
|
||||
"lock-icon": "lock icon",
|
||||
"login": {
|
||||
"divider": {
|
||||
"connecting-text": "or"
|
||||
@@ -2839,10 +2845,11 @@
|
||||
},
|
||||
"plugins": {
|
||||
"catalog": {
|
||||
"no-updates-available": "No updates available",
|
||||
"update-all": {
|
||||
"all-plugins-updated": "All plugins updated!",
|
||||
"available-header": "Available",
|
||||
"button": "Update all",
|
||||
"button": "Update all ({{length}})",
|
||||
"cloud-update-message": "* It may take a few minutes for the plugins to be available for usage.",
|
||||
"error": "Error updating plugin:",
|
||||
"error-status-text": "failed - see error messages",
|
||||
@@ -2898,6 +2905,12 @@
|
||||
"empty-state": {
|
||||
"message": "No plugins found"
|
||||
},
|
||||
"filter": {
|
||||
"disabled": "This filter has been disabled because the Grafana server cannot access grafana.com",
|
||||
"sort": "Sort",
|
||||
"sort-list": "Sort Plugins List",
|
||||
"state": "State"
|
||||
},
|
||||
"plugin-help": {
|
||||
"error": "An error occurred when loading help.",
|
||||
"not-found": "No query help could be found."
|
||||
|
||||
@@ -810,6 +810,7 @@
|
||||
"collapse": "Cőľľäpşę",
|
||||
"edit": "Ēđįŧ",
|
||||
"help": "Ħęľp",
|
||||
"loading": "Ŀőäđįʼnģ...",
|
||||
"locale": {
|
||||
"default": "Đęƒäūľŧ"
|
||||
},
|
||||
@@ -1642,6 +1643,10 @@
|
||||
"incomplete-request-error": "Ŝőřřy, Ĩ ŵäş ūʼnäþľę ŧő čőmpľęŧę yőūř řęqūęşŧ. Pľęäşę ŧřy äģäįʼn.",
|
||||
"send-custom-feedback": "Ŝęʼnđ"
|
||||
},
|
||||
"get-enterprise": {
|
||||
"requires-license": "Ŗęqūįřęş ä Ğřäƒäʼnä Ēʼnŧęřpřįşę ľįčęʼnşę",
|
||||
"title": "Ēʼnŧęřpřįşę"
|
||||
},
|
||||
"grafana-ui": {
|
||||
"action-editor": {
|
||||
"button": {
|
||||
@@ -2045,6 +2050,7 @@
|
||||
"render-image-error-description": "Åʼn ęřřőř őččūřřęđ ŵĥęʼn ģęʼnęřäŧįʼnģ ŧĥę įmäģę"
|
||||
}
|
||||
},
|
||||
"lock-icon": "ľőčĸ įčőʼn",
|
||||
"login": {
|
||||
"divider": {
|
||||
"connecting-text": "őř"
|
||||
@@ -2839,10 +2845,11 @@
|
||||
},
|
||||
"plugins": {
|
||||
"catalog": {
|
||||
"no-updates-available": "Ńő ūpđäŧęş äväįľäþľę",
|
||||
"update-all": {
|
||||
"all-plugins-updated": "Åľľ pľūģįʼnş ūpđäŧęđ!",
|
||||
"available-header": "Åväįľäþľę",
|
||||
"button": "Ůpđäŧę äľľ",
|
||||
"button": "Ůpđäŧę äľľ ({{length}})",
|
||||
"cloud-update-message": "* Ĩŧ mäy ŧäĸę ä ƒęŵ mįʼnūŧęş ƒőř ŧĥę pľūģįʼnş ŧő þę äväįľäþľę ƒőř ūşäģę.",
|
||||
"error": "Ēřřőř ūpđäŧįʼnģ pľūģįʼn:",
|
||||
"error-status-text": "ƒäįľęđ - şęę ęřřőř męşşäģęş",
|
||||
@@ -2898,6 +2905,12 @@
|
||||
"empty-state": {
|
||||
"message": "Ńő pľūģįʼnş ƒőūʼnđ"
|
||||
},
|
||||
"filter": {
|
||||
"disabled": "Ŧĥįş ƒįľŧęř ĥäş þęęʼn đįşäþľęđ þęčäūşę ŧĥę Ğřäƒäʼnä şęřvęř čäʼnʼnőŧ äččęşş ģřäƒäʼnä.čőm",
|
||||
"sort": "Ŝőřŧ",
|
||||
"sort-list": "Ŝőřŧ Pľūģįʼnş Ŀįşŧ",
|
||||
"state": "Ŝŧäŧę"
|
||||
},
|
||||
"plugin-help": {
|
||||
"error": "Åʼn ęřřőř őččūřřęđ ŵĥęʼn ľőäđįʼnģ ĥęľp.",
|
||||
"not-found": "Ńő qūęřy ĥęľp čőūľđ þę ƒőūʼnđ."
|
||||
|
||||
Reference in New Issue
Block a user