Browse.tsx 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185
  1. import { css } from '@emotion/css';
  2. import React, { ReactElement } from 'react';
  3. import { useSelector } from 'react-redux';
  4. import { useLocation } from 'react-router-dom';
  5. import { SelectableValue, GrafanaTheme2 } from '@grafana/data';
  6. import { locationSearchToObject } from '@grafana/runtime';
  7. import { LoadingPlaceholder, Select, RadioButtonGroup, useStyles2, Tooltip } from '@grafana/ui';
  8. import { Page } from 'app/core/components/Page/Page';
  9. import { GrafanaRouteComponentProps } from 'app/core/navigation/types';
  10. import { getNavModel } from 'app/core/selectors/navModel';
  11. import { StoreState } from 'app/types/store';
  12. import { HorizontalGroup } from '../components/HorizontalGroup';
  13. import { PluginList } from '../components/PluginList';
  14. import { SearchField } from '../components/SearchField';
  15. import { Sorters } from '../helpers';
  16. import { useHistory } from '../hooks/useHistory';
  17. import { useGetAllWithFilters, useIsRemotePluginsAvailable, useDisplayMode } from '../state/hooks';
  18. import { PluginAdminRoutes, PluginListDisplayMode } from '../types';
  19. export default function Browse({ route }: GrafanaRouteComponentProps): ReactElement | null {
  20. const location = useLocation();
  21. const locationSearch = locationSearchToObject(location.search);
  22. const navModelId = getNavModelId(route.routeName);
  23. const navModel = useSelector((state: StoreState) => getNavModel(state.navIndex, navModelId));
  24. const { displayMode, setDisplayMode } = useDisplayMode();
  25. const styles = useStyles2(getStyles);
  26. const history = useHistory();
  27. const remotePluginsAvailable = useIsRemotePluginsAvailable();
  28. const query = (locationSearch.q as string) || '';
  29. const filterBy = (locationSearch.filterBy as string) || 'installed';
  30. const filterByType = (locationSearch.filterByType as string) || 'all';
  31. const sortBy = (locationSearch.sortBy as Sorters) || Sorters.nameAsc;
  32. const { isLoading, error, plugins } = useGetAllWithFilters({
  33. query,
  34. filterBy,
  35. filterByType,
  36. sortBy,
  37. });
  38. const filterByOptions = [
  39. { value: 'all', label: 'All' },
  40. { value: 'installed', label: 'Installed' },
  41. ];
  42. const onSortByChange = (value: SelectableValue<string>) => {
  43. history.push({ query: { sortBy: value.value } });
  44. };
  45. const onFilterByChange = (value: string) => {
  46. history.push({ query: { filterBy: value } });
  47. };
  48. const onFilterByTypeChange = (value: string) => {
  49. history.push({ query: { filterByType: value } });
  50. };
  51. const onSearch = (q: any) => {
  52. history.push({ query: { filterBy: 'all', filterByType: 'all', q } });
  53. };
  54. // How should we handle errors?
  55. if (error) {
  56. console.error(error.message);
  57. return null;
  58. }
  59. return (
  60. <Page navModel={navModel}>
  61. <Page.Contents>
  62. <HorizontalGroup wrap>
  63. <SearchField value={query} onSearch={onSearch} />
  64. <HorizontalGroup wrap className={styles.actionBar}>
  65. {/* Filter by type */}
  66. <div>
  67. <RadioButtonGroup
  68. value={filterByType}
  69. onChange={onFilterByTypeChange}
  70. options={[
  71. { value: 'all', label: 'All' },
  72. { value: 'datasource', label: 'Data sources' },
  73. { value: 'panel', label: 'Panels' },
  74. { value: 'app', label: 'Applications' },
  75. ]}
  76. />
  77. </div>
  78. {/* Filter by installed / all */}
  79. {remotePluginsAvailable ? (
  80. <div>
  81. <RadioButtonGroup value={filterBy} onChange={onFilterByChange} options={filterByOptions} />
  82. </div>
  83. ) : (
  84. <Tooltip
  85. content="This filter has been disabled because the Grafana server cannot access grafana.com"
  86. placement="top"
  87. >
  88. <div>
  89. <RadioButtonGroup
  90. disabled={true}
  91. value={filterBy}
  92. onChange={onFilterByChange}
  93. options={filterByOptions}
  94. />
  95. </div>
  96. </Tooltip>
  97. )}
  98. {/* Sorting */}
  99. <div>
  100. <Select
  101. aria-label="Sort Plugins List"
  102. width={24}
  103. value={sortBy}
  104. onChange={onSortByChange}
  105. options={[
  106. { value: 'nameAsc', label: 'Sort by name (A-Z)' },
  107. { value: 'nameDesc', label: 'Sort by name (Z-A)' },
  108. { value: 'updated', label: 'Sort by updated date' },
  109. { value: 'published', label: 'Sort by published date' },
  110. { value: 'downloads', label: 'Sort by downloads' },
  111. ]}
  112. />
  113. </div>
  114. {/* Display mode */}
  115. <div>
  116. <RadioButtonGroup<PluginListDisplayMode>
  117. className={styles.displayAs}
  118. value={displayMode}
  119. onChange={setDisplayMode}
  120. options={[
  121. {
  122. value: PluginListDisplayMode.Grid,
  123. icon: 'table',
  124. description: 'Display plugins in a grid layout',
  125. },
  126. { value: PluginListDisplayMode.List, icon: 'list-ul', description: 'Display plugins in list' },
  127. ]}
  128. />
  129. </div>
  130. </HorizontalGroup>
  131. </HorizontalGroup>
  132. <div className={styles.listWrap}>
  133. {isLoading ? (
  134. <LoadingPlaceholder
  135. className={css`
  136. margin-bottom: 0;
  137. `}
  138. text="Loading results"
  139. />
  140. ) : (
  141. <PluginList plugins={plugins} displayMode={displayMode} />
  142. )}
  143. </div>
  144. </Page.Contents>
  145. </Page>
  146. );
  147. }
  148. const getStyles = (theme: GrafanaTheme2) => ({
  149. actionBar: css`
  150. ${theme.breakpoints.up('xl')} {
  151. margin-left: auto;
  152. }
  153. `,
  154. listWrap: css`
  155. margin-top: ${theme.spacing(2)};
  156. `,
  157. displayAs: css`
  158. svg {
  159. margin-right: 0;
  160. }
  161. `,
  162. });
  163. // Because the component is used under multiple paths (/plugins and /admin/plugins) we need to get
  164. // the correct navModel from the store
  165. const getNavModelId = (routeName?: string) => {
  166. if (routeName === PluginAdminRoutes.HomeAdmin || routeName === PluginAdminRoutes.BrowseAdmin) {
  167. return 'admin-plugins';
  168. }
  169. return 'plugins';
  170. };