Files
grafana/public/app/features/plugins/admin/pages/Browse.tsx

191 lines
6.6 KiB
TypeScript
Raw Normal View History

import { css } from '@emotion/css';
import { ReactElement, useState } from 'react';
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 { Page } from 'app/core/components/Page/Page';
import { Trans } from 'app/core/internationalization';
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
import { getNavModel } from 'app/core/selectors/navModel';
import { ROUTES as CONNECTIONS_ROUTES } from 'app/features/connections/constants';
import { useSelector } from 'app/types';
import { HorizontalGroup } from '../components/HorizontalGroup';
import { PluginList } from '../components/PluginList';
import { RoadmapLinks } from '../components/RoadmapLinks';
import { SearchField } from '../components/SearchField';
import { UpdateAllModal } from '../components/UpdateAllModal';
import { Sorters } from '../helpers';
import { useHistory } from '../hooks/useHistory';
import { useGetAll, useGetUpdatable, useIsRemotePluginsAvailable } from '../state/hooks';
export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null {
const location = useLocation();
Plugins Catalog: migrate state handling to Redux (#38876) * feat(Plugins/Catalog): start adding necessary apis * feat(PLugins/Catalog): add extra helpers for merging local & remote plugins * feat(Plugins/Catalog): add plugin details as an optional field of CatalogPlugin * feat(PLugins/Catalog): add scaffolding for the new redux model * feat(PLugins/Catalog): export reducers based on a feature-flag * refactor(Plugins/Admin): rename api methods * feat(Plugin/Catalog): add an api method for fetching a single plugin * feat(Plugins/Admin): try cleaning stuff around plugin fetching * ffeat(Plugins/Catalog): return the catalog reducer when the feature flag is set * refactor(Plugins/Admin): fix typings * feat(Plugins/Admin): use the new reducer for the browse page * feat(catalog): introduce selectors to search and filter plugins list * refactor(Plugins/Details): rename page prop type * refactor(Plugins/Admin): add a const for a state prefix * refactor(Plugins/Admin): use the state prefix in the actions * feat(Plugins/Admin): add types for the requests * refactor(Plugins/Admin): add request info to the reducer * refactor(Plugins/Admin): add request handling to the hooks & selectors * refactor(Plugins/Details): start using the data stored in Redux * refactor(Plugins/Admin): rename selector to start with "select" * fix(Plugins/Admin): only fetch plugins once * refactor(Plugins/Admin): make the tab selection work in details * refactor(catalog): put back loading and error states in plugin list * refactor(Plugins/Admin): use CatalogPlugin for <PluginDetailsSignature /> * feat(Plugins/Admin): add an api method for fetching plugin details * refactor(Plugins/Admin): add action for updating the details * irefactor(Plugins/Admin): show basic plugin details info * refactor(Plugin Details): migrate the plugin details header * refactor(Plugins/Admin): make the config and dashboards tabs work * refactor(Plugins/Admin): add old reducer state to the new one * feat(catalog): introduce actions, reducers and hooks for install & uninstall * refactor(catalog): wire up InstallControls component to redux * refactor(catalog): move parentUrl inside PluginDetailsHeader and uncomment InstallControls * feat(catalog): introduce code for plugin updates to install action * refactor(Plugins/Admin): add backward compatible actions * test(catalog): update PluginDetails and Browse tests to work with catalog store * refactor(Plugins/Admin): make the dashboards and panels work again * refactor(Plugins/Admin): fix linter and typescript errors * fix(Plugins/Admin): put the local-only plugins to the beginning of the list * fix(Plugins/Admin): fix the mocks in the tests for PluginDetails * refactor(Plugins/Admin): remove unecessary hook usePluginsByFilter() * refactor(Plugins/Admin): extract the useTabs() hook to its own file * refactor(Plugins/Admin): remove unused helpers and types * fix(Plugins/Admin): show the first tab when uninstalling an app plugin This can cause the user to find themselves on a dissappeared tab, as the config and dashboards tabs are removed. * fix(catalog): correct logic for checking if activeTabIndex is greater than total tabs * fix(Plugins/Admin): fix race-condition between fetching plugin details and all plugins * fix(Plugins): fix strict type errors * chore(catalog): remove todos * feat(catalog): render an alert in PluginDetails when a plugin cannot be found * feat(catalog): use the proper store state * refactor(Plugins/Admin): fetch local and remote plugins in parallell Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * style(catalog): fix prettier error in api * fix(catalog): prevent throwing error if InstallControlsButton is unmounted during install * refactor(Plugins/Admin): add a separate hook for filtering & sorting plugins Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
2021-09-09 12:20:35 +02:00
const locationSearch = locationSearchToObject(location.search);
const navModel = useSelector((state) => getNavModel(state.navIndex, 'plugins'));
const styles = useStyles2(getStyles);
const history = useHistory();
const remotePluginsAvailable = useIsRemotePluginsAvailable();
const keyword = locationSearch.q?.toString() || '';
const filterBy = locationSearch.filterBy?.toString() || 'all';
const filterByType = (locationSearch.filterByType as PluginType | 'all') || 'all';
Plugins Catalog: migrate state handling to Redux (#38876) * feat(Plugins/Catalog): start adding necessary apis * feat(PLugins/Catalog): add extra helpers for merging local & remote plugins * feat(Plugins/Catalog): add plugin details as an optional field of CatalogPlugin * feat(PLugins/Catalog): add scaffolding for the new redux model * feat(PLugins/Catalog): export reducers based on a feature-flag * refactor(Plugins/Admin): rename api methods * feat(Plugin/Catalog): add an api method for fetching a single plugin * feat(Plugins/Admin): try cleaning stuff around plugin fetching * ffeat(Plugins/Catalog): return the catalog reducer when the feature flag is set * refactor(Plugins/Admin): fix typings * feat(Plugins/Admin): use the new reducer for the browse page * feat(catalog): introduce selectors to search and filter plugins list * refactor(Plugins/Details): rename page prop type * refactor(Plugins/Admin): add a const for a state prefix * refactor(Plugins/Admin): use the state prefix in the actions * feat(Plugins/Admin): add types for the requests * refactor(Plugins/Admin): add request info to the reducer * refactor(Plugins/Admin): add request handling to the hooks & selectors * refactor(Plugins/Details): start using the data stored in Redux * refactor(Plugins/Admin): rename selector to start with "select" * fix(Plugins/Admin): only fetch plugins once * refactor(Plugins/Admin): make the tab selection work in details * refactor(catalog): put back loading and error states in plugin list * refactor(Plugins/Admin): use CatalogPlugin for <PluginDetailsSignature /> * feat(Plugins/Admin): add an api method for fetching plugin details * refactor(Plugins/Admin): add action for updating the details * irefactor(Plugins/Admin): show basic plugin details info * refactor(Plugin Details): migrate the plugin details header * refactor(Plugins/Admin): make the config and dashboards tabs work * refactor(Plugins/Admin): add old reducer state to the new one * feat(catalog): introduce actions, reducers and hooks for install & uninstall * refactor(catalog): wire up InstallControls component to redux * refactor(catalog): move parentUrl inside PluginDetailsHeader and uncomment InstallControls * feat(catalog): introduce code for plugin updates to install action * refactor(Plugins/Admin): add backward compatible actions * test(catalog): update PluginDetails and Browse tests to work with catalog store * refactor(Plugins/Admin): make the dashboards and panels work again * refactor(Plugins/Admin): fix linter and typescript errors * fix(Plugins/Admin): put the local-only plugins to the beginning of the list * fix(Plugins/Admin): fix the mocks in the tests for PluginDetails * refactor(Plugins/Admin): remove unecessary hook usePluginsByFilter() * refactor(Plugins/Admin): extract the useTabs() hook to its own file * refactor(Plugins/Admin): remove unused helpers and types * fix(Plugins/Admin): show the first tab when uninstalling an app plugin This can cause the user to find themselves on a dissappeared tab, as the config and dashboards tabs are removed. * fix(catalog): correct logic for checking if activeTabIndex is greater than total tabs * fix(Plugins/Admin): fix race-condition between fetching plugin details and all plugins * fix(Plugins): fix strict type errors * chore(catalog): remove todos * feat(catalog): render an alert in PluginDetails when a plugin cannot be found * feat(catalog): use the proper store state * refactor(Plugins/Admin): fetch local and remote plugins in parallell Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com> * style(catalog): fix prettier error in api * fix(catalog): prevent throwing error if InstallControlsButton is unmounted during install * refactor(Plugins/Admin): add a separate hook for filtering & sorting plugins Co-authored-by: Jack Westbrook <jack.westbrook@gmail.com> Co-authored-by: Marcus Andersson <marcus.andersson@grafana.com>
2021-09-09 12:20:35 +02:00
const sortBy = (locationSearch.sortBy as Sorters) || Sorters.nameAsc;
const { isLoading, error, plugins } = useGetAll(
{
keyword,
type: filterByType !== 'all' ? filterByType : undefined,
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 updatablePlugins = useGetUpdatable();
const [showUpdateModal, setShowUpdateModal] = useState(false);
const disableUpdateAllButton = updatablePlugins.length <= 0;
const onSortByChange = (value: SelectableValue<string>) => {
history.push({ query: { sortBy: value.value } });
};
const onFilterByChange = (value: string) => {
history.push({ query: { filterBy: value } });
};
const onFilterByTypeChange = (value: SelectableValue<string>) => {
history.push({ query: { filterByType: value.value } });
};
const onSearch = (q: string) => {
history.push({ query: { filterBy, filterByType, q } });
};
const onUpdateAll = () => {
setShowUpdateModal(true);
};
// How should we handle errors?
if (error) {
console.error(error.message);
return null;
}
const subTitle = (
<div>
Extend the Grafana experience with panel plugins and apps. To find more data sources go to{' '}
<a className="external-link" href={`${CONNECTIONS_ROUTES.AddNewConnection}?cat=data-source`}>
Connections
</a>
.
</div>
);
const updateAll = (
<Button disabled={disableUpdateAllButton} onClick={onUpdateAll}>
<Trans i18nKey="plugins.catalog.update-all.button">Update all</Trans>
{!disableUpdateAllButton ? ` (${updatablePlugins.length})` : ''}
</Button>
);
return (
<Page navModel={navModel} actions={updateAll} subTitle={subTitle}>
<Page.Contents>
<HorizontalGroup wrap>
<Field label="Search">
<SearchField value={keyword} onSearch={onSearch} />
</Field>
<HorizontalGroup wrap className={styles.actionBar}>
{/* Filter by type */}
<Field label="Type">
<Select
aria-label="Plugin type filter"
value={filterByType}
onChange={onFilterByTypeChange}
width={18}
options={[
{ value: 'all', label: 'All' },
{ value: 'datasource', label: 'Data sources' },
{ value: 'panel', label: 'Panels' },
{ value: 'app', label: 'Applications' },
]}
/>
</Field>
{/* Filter by installed / all */}
{remotePluginsAvailable ? (
<Field label="State">
<RadioButtonGroup value={filterBy} onChange={onFilterByChange} options={filterByOptions} />
</Field>
) : (
<Tooltip
content="This filter has been disabled because the Grafana server cannot access grafana.com"
placement="top"
>
<div>
<Field label="State">
<RadioButtonGroup
disabled={true}
value={filterBy}
onChange={onFilterByChange}
options={filterByOptions}
/>
</Field>
</div>
</Tooltip>
)}
{/* Sorting */}
<Field label="Sort">
<Select
aria-label="Sort Plugins List"
width={24}
value={sortBy}
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>
<div className={styles.listWrap}>
<PluginList plugins={plugins} isLoading={isLoading} />
</div>
<RoadmapLinks />
<UpdateAllModal
isOpen={showUpdateModal}
onDismiss={() => setShowUpdateModal(false)}
plugins={updatablePlugins}
/>
</Page.Contents>
</Page>
);
}
const getStyles = (theme: GrafanaTheme2) => ({
actionBar: css({
[theme.breakpoints.up('xl')]: {
marginLeft: 'auto',
},
}),
listWrap: css({
marginTop: theme.spacing(2),
}),
displayAs: css({
svg: {
marginRight: 0,
},
}),
});