mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* 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>
146 lines
5.0 KiB
TypeScript
146 lines
5.0 KiB
TypeScript
import React, { ReactElement } from 'react';
|
|
import { css } from '@emotion/css';
|
|
import { SelectableValue, GrafanaTheme2 } from '@grafana/data';
|
|
import { LoadingPlaceholder, Select, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
|
import { useLocation } from 'react-router-dom';
|
|
import { locationSearchToObject } from '@grafana/runtime';
|
|
import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
|
|
import { PluginList } from '../components/PluginList';
|
|
import { SearchField } from '../components/SearchField';
|
|
import { useHistory } from '../hooks/useHistory';
|
|
import { PluginAdminRoutes } from '../types';
|
|
import { Page as PluginPage } from '../components/Page';
|
|
import { HorizontalGroup } from '../components/HorizontalGroup';
|
|
import { Page } from 'app/core/components/Page/Page';
|
|
import { useSelector } from 'react-redux';
|
|
import { StoreState } from 'app/types/store';
|
|
import { getNavModel } from 'app/core/selectors/navModel';
|
|
import { useGetAll, useGetAllWithFilters } from '../state/hooks';
|
|
import { Sorters } from '../helpers';
|
|
|
|
export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null {
|
|
useGetAll();
|
|
const location = useLocation();
|
|
const locationSearch = locationSearchToObject(location.search);
|
|
const navModelId = getNavModelId(route.routeName);
|
|
const navModel = useSelector((state: StoreState) => getNavModel(state.navIndex, navModelId));
|
|
const styles = useStyles2(getStyles);
|
|
const history = useHistory();
|
|
const query = (locationSearch.q as string) || '';
|
|
const filterBy = (locationSearch.filterBy as string) || 'installed';
|
|
const filterByType = (locationSearch.filterByType as string) || 'all';
|
|
const sortBy = (locationSearch.sortBy as Sorters) || Sorters.nameAsc;
|
|
const { isLoading, error, plugins } = useGetAllWithFilters({
|
|
query,
|
|
filterBy,
|
|
filterByType,
|
|
sortBy,
|
|
});
|
|
|
|
const onSortByChange = (value: SelectableValue<string>) => {
|
|
history.push({ query: { sortBy: value.value } });
|
|
};
|
|
|
|
const onFilterByChange = (value: string) => {
|
|
history.push({ query: { filterBy: value } });
|
|
};
|
|
|
|
const onFilterByTypeChange = (value: string) => {
|
|
history.push({ query: { filterByType: value } });
|
|
};
|
|
|
|
const onSearch = (q: any) => {
|
|
history.push({ query: { filterBy: 'all', filterByType: 'all', q } });
|
|
};
|
|
|
|
// How should we handle errors?
|
|
if (error) {
|
|
console.error(error.message);
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Page navModel={navModel}>
|
|
<Page.Contents>
|
|
<PluginPage>
|
|
<HorizontalGroup wrap>
|
|
<SearchField value={query} onSearch={onSearch} />
|
|
<HorizontalGroup wrap className={styles.actionBar}>
|
|
<div>
|
|
<RadioButtonGroup
|
|
value={filterByType}
|
|
onChange={onFilterByTypeChange}
|
|
options={[
|
|
{ value: 'all', label: 'All' },
|
|
{ value: 'datasource', label: 'Data sources' },
|
|
{ value: 'panel', label: 'Panels' },
|
|
{ value: 'app', label: 'Applications' },
|
|
]}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<RadioButtonGroup
|
|
value={filterBy}
|
|
onChange={onFilterByChange}
|
|
options={[
|
|
{ value: 'all', label: 'All' },
|
|
{ value: 'installed', label: 'Installed' },
|
|
]}
|
|
/>
|
|
</div>
|
|
<div>
|
|
<Select
|
|
menuShouldPortal
|
|
width={24}
|
|
value={sortBy}
|
|
onChange={onSortByChange}
|
|
options={[
|
|
{ value: 'nameAsc', label: 'Sort by name (A-Z)' },
|
|
{ value: 'nameDesc', label: 'Sort by name (Z-A)' },
|
|
{ value: 'updated', label: 'Sort by updated date' },
|
|
{ value: 'published', label: 'Sort by published date' },
|
|
{ value: 'downloads', label: 'Sort by downloads' },
|
|
]}
|
|
/>
|
|
</div>
|
|
</HorizontalGroup>
|
|
</HorizontalGroup>
|
|
<div className={styles.listWrap}>
|
|
{isLoading ? (
|
|
<LoadingPlaceholder
|
|
className={css`
|
|
margin-bottom: 0;
|
|
`}
|
|
text="Loading results"
|
|
/>
|
|
) : (
|
|
<PluginList plugins={plugins} />
|
|
)}
|
|
</div>
|
|
</PluginPage>
|
|
</Page.Contents>
|
|
</Page>
|
|
);
|
|
}
|
|
|
|
const getStyles = (theme: GrafanaTheme2) => ({
|
|
actionBar: css`
|
|
${theme.breakpoints.up('xl')} {
|
|
margin-left: auto;
|
|
}
|
|
`,
|
|
listWrap: css`
|
|
margin-top: ${theme.spacing(2)};
|
|
`,
|
|
});
|
|
|
|
// Because the component is used under multiple paths (/plugins and /admin/plugins) we need to get
|
|
// the correct navModel from the store
|
|
const getNavModelId = (routeName?: string) => {
|
|
if (routeName === PluginAdminRoutes.HomeAdmin || routeName === PluginAdminRoutes.BrowseAdmin) {
|
|
return 'admin-plugins';
|
|
}
|
|
|
|
return 'plugins';
|
|
};
|