123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198 |
- import {Component} from 'react';
- import HookStore from 'sentry/stores/hookStore';
- import {Config, Organization, Project} from 'sentry/types';
- import {FeatureDisabledHooks} from 'sentry/types/hooks';
- import {isRenderFunc} from 'sentry/utils/isRenderFunc';
- import withConfig from 'sentry/utils/withConfig';
- import withOrganization from 'sentry/utils/withOrganization';
- import withProject from 'sentry/utils/withProject';
- import ComingSoon from './comingSoon';
- type Props = {
- /**
- * If children is a function then will be treated as a render prop and
- * passed FeatureRenderProps.
- *
- * The other interface is more simple, only show `children` if org/project has
- * all the required feature.
- */
- children: React.ReactNode | ChildrenRenderFn;
- config: Config;
- /**
- * List of required feature tags. Note we do not enforce uniqueness of tags anywhere.
- * On the backend end, feature tags have a scope prefix string that is stripped out on the
- * frontend (since feature tags are attached to a context object).
- *
- * Use `organizations:` or `projects:` prefix strings to specify a feature with context.
- */
- features: string[];
- /**
- * The following properties will be set by the HoCs
- */
- organization: Organization;
- /**
- * Specify the key to use for hookstore functionality.
- *
- * The hookName should be prefixed with `feature-disabled`.
- *
- * When specified, the hookstore will be checked if the feature is
- * disabled, and the first available hook will be used as the render
- * function.
- */
- hookName?: keyof FeatureDisabledHooks;
- project?: Project;
- /**
- * Custom renderer function for when the feature is not enabled.
- *
- * - [default] Set this to false to disable rendering anything. If the
- * feature is not enabled no children will be rendered.
- *
- * - Set this to `true` to use the default `ComingSoon` alert component.
- *
- * - Provide a custom render function to customize the rendered component.
- *
- * When a custom render function is used, the same object that would be
- * passed to `children` if a func is provided there, will be used here,
- * additionally `children` will also be passed.
- */
- renderDisabled?: boolean | RenderDisabledFn;
- /**
- * Should the component require all features or just one or more.
- */
- requireAll?: boolean;
- };
- /**
- * Common props passed to children and disabled render handlers.
- */
- type FeatureRenderProps = {
- features: string[];
- hasFeature: boolean;
- organization: Organization;
- project?: Project;
- };
- /**
- * When a feature is disabled the caller of Feature may provide a `renderDisabled`
- * prop. This prop can be overridden by getsentry via hooks. Often getsentry will
- * call the original children function but override the `renderDisabled`
- * with another function/component.
- */
- type RenderDisabledProps = FeatureRenderProps & {
- children: React.ReactNode | ChildrenRenderFn;
- renderDisabled?: (props: FeatureRenderProps) => React.ReactNode;
- };
- export type RenderDisabledFn = (props: RenderDisabledProps) => React.ReactNode;
- type ChildRenderProps = FeatureRenderProps & {
- renderDisabled?: undefined | boolean | RenderDisabledFn;
- };
- export type ChildrenRenderFn = (props: ChildRenderProps) => React.ReactNode;
- type AllFeatures = {
- configFeatures: string[];
- organization: string[];
- project: string[];
- };
- /**
- * Component to handle feature flags.
- */
- class Feature extends Component<Props> {
- static defaultProps = {
- renderDisabled: false,
- requireAll: true,
- };
- getAllFeatures(): AllFeatures {
- const {organization, project, config} = this.props;
- return {
- configFeatures: config.features ? Array.from(config.features) : [],
- organization: (organization && organization.features) || [],
- project: (project && project.features) || [],
- };
- }
- hasFeature(feature: string, features: AllFeatures) {
- const shouldMatchOnlyProject = feature.match(/^projects:(.+)/);
- const shouldMatchOnlyOrg = feature.match(/^organizations:(.+)/);
- // Array of feature strings
- const {configFeatures, organization, project} = features;
- // Check config store first as this overrides features scoped to org or
- // project contexts.
- if (configFeatures.includes(feature)) {
- return true;
- }
- if (shouldMatchOnlyProject) {
- return project.includes(shouldMatchOnlyProject[1]);
- }
- if (shouldMatchOnlyOrg) {
- return organization.includes(shouldMatchOnlyOrg[1]);
- }
- // default, check all feature arrays
- return organization.includes(feature) || project.includes(feature);
- }
- render() {
- const {
- children,
- features,
- renderDisabled,
- hookName,
- organization,
- project,
- requireAll,
- } = this.props;
- const allFeatures = this.getAllFeatures();
- const method = requireAll ? 'every' : 'some';
- const hasFeature =
- !features || features[method](feat => this.hasFeature(feat, allFeatures));
- // Default renderDisabled to the ComingSoon component
- let customDisabledRender =
- renderDisabled === false
- ? false
- : typeof renderDisabled === 'function'
- ? renderDisabled
- : () => <ComingSoon />;
- // Override the renderDisabled function with a hook store function if there
- // is one registered for the feature.
- if (hookName) {
- const hooks = HookStore.get(hookName);
- if (hooks.length > 0) {
- customDisabledRender = hooks[0];
- }
- }
- const renderProps = {
- organization,
- project,
- features,
- hasFeature,
- };
- if (!hasFeature && customDisabledRender !== false) {
- return customDisabledRender({children, ...renderProps});
- }
- if (isRenderFunc<ChildrenRenderFn>(children)) {
- return children({renderDisabled, ...renderProps});
- }
- return hasFeature && children ? children : null;
- }
- }
- export default withOrganization(withProject(withConfig(Feature)));
|