feature.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. import {Component} from 'react';
  2. import HookStore from 'sentry/stores/hookStore';
  3. import {Config, Organization, Project} from 'sentry/types';
  4. import {FeatureDisabledHooks} from 'sentry/types/hooks';
  5. import {isRenderFunc} from 'sentry/utils/isRenderFunc';
  6. import withConfig from 'sentry/utils/withConfig';
  7. import withOrganization from 'sentry/utils/withOrganization';
  8. import withProject from 'sentry/utils/withProject';
  9. import ComingSoon from './comingSoon';
  10. type Props = {
  11. /**
  12. * If children is a function then will be treated as a render prop and
  13. * passed FeatureRenderProps.
  14. *
  15. * The other interface is more simple, only show `children` if org/project has
  16. * all the required feature.
  17. */
  18. children: React.ReactNode | ChildrenRenderFn;
  19. config: Config;
  20. /**
  21. * List of required feature tags. Note we do not enforce uniqueness of tags anywhere.
  22. * On the backend end, feature tags have a scope prefix string that is stripped out on the
  23. * frontend (since feature tags are attached to a context object).
  24. *
  25. * Use `organizations:` or `projects:` prefix strings to specify a feature with context.
  26. */
  27. features: string[];
  28. /**
  29. * The following properties will be set by the HoCs
  30. */
  31. organization: Organization;
  32. /**
  33. * Specify the key to use for hookstore functionality.
  34. *
  35. * The hookName should be prefixed with `feature-disabled`.
  36. *
  37. * When specified, the hookstore will be checked if the feature is
  38. * disabled, and the first available hook will be used as the render
  39. * function.
  40. */
  41. hookName?: keyof FeatureDisabledHooks;
  42. project?: Project;
  43. /**
  44. * Custom renderer function for when the feature is not enabled.
  45. *
  46. * - [default] Set this to false to disable rendering anything. If the
  47. * feature is not enabled no children will be rendered.
  48. *
  49. * - Set this to `true` to use the default `ComingSoon` alert component.
  50. *
  51. * - Provide a custom render function to customize the rendered component.
  52. *
  53. * When a custom render function is used, the same object that would be
  54. * passed to `children` if a func is provided there, will be used here,
  55. * additionally `children` will also be passed.
  56. */
  57. renderDisabled?: boolean | RenderDisabledFn;
  58. /**
  59. * Should the component require all features or just one or more.
  60. */
  61. requireAll?: boolean;
  62. };
  63. /**
  64. * Common props passed to children and disabled render handlers.
  65. */
  66. type FeatureRenderProps = {
  67. features: string[];
  68. hasFeature: boolean;
  69. organization: Organization;
  70. project?: Project;
  71. };
  72. /**
  73. * When a feature is disabled the caller of Feature may provide a `renderDisabled`
  74. * prop. This prop can be overridden by getsentry via hooks. Often getsentry will
  75. * call the original children function but override the `renderDisabled`
  76. * with another function/component.
  77. */
  78. type RenderDisabledProps = FeatureRenderProps & {
  79. children: React.ReactNode | ChildrenRenderFn;
  80. renderDisabled?: (props: FeatureRenderProps) => React.ReactNode;
  81. };
  82. export type RenderDisabledFn = (props: RenderDisabledProps) => React.ReactNode;
  83. type ChildRenderProps = FeatureRenderProps & {
  84. renderDisabled?: undefined | boolean | RenderDisabledFn;
  85. };
  86. export type ChildrenRenderFn = (props: ChildRenderProps) => React.ReactNode;
  87. type AllFeatures = {
  88. configFeatures: string[];
  89. organization: string[];
  90. project: string[];
  91. };
  92. /**
  93. * Component to handle feature flags.
  94. */
  95. class Feature extends Component<Props> {
  96. static defaultProps = {
  97. renderDisabled: false,
  98. requireAll: true,
  99. };
  100. getAllFeatures(): AllFeatures {
  101. const {organization, project, config} = this.props;
  102. return {
  103. configFeatures: config.features ? Array.from(config.features) : [],
  104. organization: (organization && organization.features) || [],
  105. project: (project && project.features) || [],
  106. };
  107. }
  108. hasFeature(feature: string, features: AllFeatures) {
  109. const shouldMatchOnlyProject = feature.match(/^projects:(.+)/);
  110. const shouldMatchOnlyOrg = feature.match(/^organizations:(.+)/);
  111. // Array of feature strings
  112. const {configFeatures, organization, project} = features;
  113. // Check config store first as this overrides features scoped to org or
  114. // project contexts.
  115. if (configFeatures.includes(feature)) {
  116. return true;
  117. }
  118. if (shouldMatchOnlyProject) {
  119. return project.includes(shouldMatchOnlyProject[1]);
  120. }
  121. if (shouldMatchOnlyOrg) {
  122. return organization.includes(shouldMatchOnlyOrg[1]);
  123. }
  124. // default, check all feature arrays
  125. return organization.includes(feature) || project.includes(feature);
  126. }
  127. render() {
  128. const {
  129. children,
  130. features,
  131. renderDisabled,
  132. hookName,
  133. organization,
  134. project,
  135. requireAll,
  136. } = this.props;
  137. const allFeatures = this.getAllFeatures();
  138. const method = requireAll ? 'every' : 'some';
  139. const hasFeature =
  140. !features || features[method](feat => this.hasFeature(feat, allFeatures));
  141. // Default renderDisabled to the ComingSoon component
  142. let customDisabledRender =
  143. renderDisabled === false
  144. ? false
  145. : typeof renderDisabled === 'function'
  146. ? renderDisabled
  147. : () => <ComingSoon />;
  148. // Override the renderDisabled function with a hook store function if there
  149. // is one registered for the feature.
  150. if (hookName) {
  151. const hooks = HookStore.get(hookName);
  152. if (hooks.length > 0) {
  153. customDisabledRender = hooks[0];
  154. }
  155. }
  156. const renderProps = {
  157. organization,
  158. project,
  159. features,
  160. hasFeature,
  161. };
  162. if (!hasFeature && customDisabledRender !== false) {
  163. return customDisabledRender({children, ...renderProps});
  164. }
  165. if (isRenderFunc<ChildrenRenderFn>(children)) {
  166. return children({renderDisabled, ...renderProps});
  167. }
  168. return hasFeature && children ? children : null;
  169. }
  170. }
  171. export default withOrganization(withProject(withConfig(Feature)));