routeSource.tsx 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import {Component} from 'react';
  2. import {RouteComponentProps} from 'react-router';
  3. import flattenDepth from 'lodash/flattenDepth';
  4. import {Organization, Project} from 'sentry/types';
  5. import {createFuzzySearch, Fuse} from 'sentry/utils/fuzzySearch';
  6. import replaceRouterParams from 'sentry/utils/replaceRouterParams';
  7. import withLatestContext from 'sentry/utils/withLatestContext';
  8. import accountSettingsNavigation from 'sentry/views/settings/account/navigationConfiguration';
  9. import organizationSettingsNavigation from 'sentry/views/settings/organization/navigationConfiguration';
  10. import projectSettingsNavigation from 'sentry/views/settings/project/navigationConfiguration';
  11. import {NavigationItem} from 'sentry/views/settings/types';
  12. import {ChildProps, ResultItem} from './types';
  13. import {strGetFn} from './utils';
  14. type Config =
  15. | typeof accountSettingsNavigation
  16. | typeof organizationSettingsNavigation
  17. | typeof projectSettingsNavigation;
  18. // XXX(epurkhiser): We use the context in mapFunc to handle both producing the
  19. // NavigationSection list AND filtering out items in the sections that should
  20. // not be shown using the `show` attribute of the NavigationItem
  21. type Context = Parameters<Extract<Config, Function>>[0] &
  22. Parameters<Extract<NavigationItem['show'], Function>>[0];
  23. /**
  24. * navigation configuration can currently be either:
  25. *
  26. * - an array of {name: string, items: Array<{NavItem}>} OR
  27. * - a function that returns the above
  28. * (some navigation items require additional context, e.g. a badge based on
  29. * a `project` property)
  30. *
  31. * We need to go through all navigation configurations and get a flattened list
  32. * of all navigation item objects
  33. */
  34. const mapFunc = (config: Config, context: Context | null = null) =>
  35. (Array.isArray(config) ? config : context !== null ? config(context) : []).map(
  36. ({items}) =>
  37. items.filter(({show}) =>
  38. typeof show === 'function' && context !== null ? show(context) : true
  39. )
  40. );
  41. type DefaultProps = {
  42. /**
  43. * Fuse configuration for searching NavigationItem's
  44. */
  45. searchOptions: Fuse.IFuseOptions<NavigationItem>;
  46. };
  47. type Props = RouteComponentProps<{}, {}> &
  48. DefaultProps & {
  49. /**
  50. * Render function that renders the route matches
  51. */
  52. children: (props: ChildProps) => React.ReactNode;
  53. /**
  54. * The string to search the navigation routes for
  55. */
  56. query: string;
  57. organization?: Organization;
  58. project?: Project;
  59. };
  60. type State = {
  61. /**
  62. * A Fuse instance configured to search NavigationItem's
  63. */
  64. fuzzy: undefined | null | Fuse<NavigationItem>;
  65. };
  66. class RouteSource extends Component<Props, State> {
  67. static defaultProps: DefaultProps = {
  68. searchOptions: {},
  69. };
  70. state: State = {
  71. fuzzy: undefined,
  72. };
  73. componentDidMount() {
  74. this.createSearch();
  75. }
  76. componentDidUpdate(prevProps: Props) {
  77. if (
  78. prevProps.project === this.props.project &&
  79. prevProps.organization === this.props.organization
  80. ) {
  81. return;
  82. }
  83. this.createSearch();
  84. }
  85. async createSearch() {
  86. const {project, organization} = this.props;
  87. const context = {
  88. project,
  89. organization,
  90. access: new Set(organization?.access ?? []),
  91. features: new Set(project?.features ?? []),
  92. } as Context;
  93. const searchMap = flattenDepth<NavigationItem>(
  94. [
  95. mapFunc(accountSettingsNavigation, context),
  96. mapFunc(projectSettingsNavigation, context),
  97. mapFunc(organizationSettingsNavigation, context),
  98. ],
  99. 2
  100. );
  101. const options = {
  102. ...this.props.searchOptions,
  103. keys: ['title', 'description'],
  104. getFn: strGetFn,
  105. };
  106. const fuzzy = await createFuzzySearch(searchMap ?? [], options);
  107. this.setState({fuzzy});
  108. }
  109. render() {
  110. const {query, params, children} = this.props;
  111. const {fuzzy} = this.state;
  112. const results =
  113. fuzzy?.search(query).map(({item, ...rest}) => ({
  114. item: {
  115. ...item,
  116. sourceType: 'route',
  117. resultType: 'route',
  118. to: replaceRouterParams(item.path, params),
  119. } as ResultItem,
  120. ...rest,
  121. })) ?? [];
  122. return children({
  123. isLoading: this.state.fuzzy === undefined,
  124. results,
  125. });
  126. }
  127. }
  128. export default withLatestContext(RouteSource);
  129. export {RouteSource};