feature.tsx 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. import * as React from 'react';
  2. import HookStore from 'app/stores/hookStore';
  3. import {Config, Organization, Project} from 'app/types';
  4. import {FeatureDisabledHooks} from 'app/types/hooks';
  5. import {isRenderFunc} from 'app/utils/isRenderFunc';
  6. import withConfig from 'app/utils/withConfig';
  7. import withOrganization from 'app/utils/withOrganization';
  8. import withProject from 'app/utils/withProject';
  9. import ComingSoon from './comingSoon';
  10. type Props = {
  11. /**
  12. * The following properties will be set by the HoCs
  13. */
  14. organization: Organization;
  15. config: Config;
  16. /**
  17. * List of required feature tags. Note we do not enforce uniqueness of tags anywhere.
  18. * On the backend end, feature tags have a scope prefix string that is stripped out on the
  19. * frontend (since feature tags are attached to a context object).
  20. *
  21. * Use `organizations:` or `projects:` prefix strings to specify a feature with context.
  22. */
  23. features: string[];
  24. /**
  25. * Should the component require all features or just one or more.
  26. */
  27. requireAll?: boolean;
  28. /**
  29. * Custom renderer function for when the feature is not enabled.
  30. *
  31. * - [default] Set this to false to disable rendering anything. If the
  32. * feature is not enabled no children will be rendered.
  33. *
  34. * - Set this to `true` to use the default `ComingSoon` alert component.
  35. *
  36. * - Provide a custom render function to customize the rendered component.
  37. *
  38. * When a custom render function is used, the same object that would be
  39. * passed to `children` if a func is provided there, will be used here,
  40. * additionally `children` will also be passed.
  41. */
  42. renderDisabled?: boolean | RenderDisabledFn;
  43. /**
  44. * Specify the key to use for hookstore functionality.
  45. *
  46. * The hookName should be prefixed with `feature-disabled`.
  47. *
  48. * When specified, the hookstore will be checked if the feature is
  49. * disabled, and the first available hook will be used as the render
  50. * function.
  51. */
  52. hookName?: keyof FeatureDisabledHooks;
  53. /**
  54. * If children is a function then will be treated as a render prop and
  55. * passed FeatureRenderProps.
  56. *
  57. * The other interface is more simple, only show `children` if org/project has
  58. * all the required feature.
  59. */
  60. children: React.ReactNode | ChildrenRenderFn;
  61. project?: Project;
  62. };
  63. /**
  64. * Common props passed to children and disabled render handlers.
  65. */
  66. type FeatureRenderProps = {
  67. organization: Organization;
  68. features: string[];
  69. hasFeature: boolean;
  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 React.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)));