2025-04-01 10:38:02 +09:00

192 lines
6.8 KiB
TypeScript

import { css } from '@emotion/css';
import { useMemo, useState, MouseEvent } from 'react';
import { useLocation } from 'react-router-dom-v5-compat';
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, Trans } from 'app/core/internationalization';
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';
import { useGetAll, useIsRemotePluginsAvailable } from 'app/features/plugins/admin/state/hooks';
import { AccessControlAction } from 'app/types';
import { ROUTES } from '../../constants';
import { CardGrid, type CardGridItem } from './CardGrid';
import { CategoryHeader } from './CategoryHeader';
import { NoAccessModal } from './NoAccessModal';
const getStyles = (theme: GrafanaTheme2) => ({
spacer: css({
height: theme.spacing(2),
}),
modal: css({
width: '500px',
}),
modalContent: css({
overflow: 'visible',
}),
actionBar: css({
[theme.breakpoints.up('xl')]: {
marginLeft: 'auto',
},
}),
});
export function AddNewConnection() {
const [queryParams, setQueryParams] = useQueryParams();
const searchTerm = queryParams.search ? String(queryParams.search) : '';
const [isNoAccessModalOpen, setIsNoAccessModalOpen] = useState(false);
const [focusedItem, setFocusedItem] = useState<CardGridItem | null>(null);
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 styles = useStyles2(getStyles);
const handleSearchChange = (val: string) => {
setQueryParams({
search: val,
});
};
const remotePluginsAvailable = useIsRemotePluginsAvailable();
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) {
e.preventDefault();
e.stopPropagation();
openModal(item);
reportInteraction('connections_plugin_card_clicked', {
plugin_id: item.id,
creator_team: 'grafana_plugins_catalog',
schema_version: '1.0.0',
});
}
};
const openModal = (item: CardGridItem) => {
setIsNoAccessModalOpen(true);
setFocusedItem(item);
};
const closeModal = () => {
setIsNoAccessModalOpen(false);
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} />}
<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={t('common.loading', 'Loading...')} />
) : !!error ? (
<Trans i18nKey="alerting.policies.update-errors.error-code" values={{ error: error.message }}>
Error message: "{{ error: error.message }}"
</Trans>
) : (
<CardGrid items={cardGridItems} onClickItem={onClickCardGridItem} />
)}
{showNoResults && (
<EmptyState
variant="not-found"
message={t('connections.connect-data.empty-message', 'No results matching your query were found')}
/>
)}
<RoadmapLinks />
</>
);
}