Browse Source

feat(ts): Add types to HookStore (#15145)

Evan Purkhiser 5 years ago
parent
commit
efe86bdd87

+ 5 - 6
src/sentry/static/sentry/app/components/acl/feature.tsx

@@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
 import React from 'react';
 
 import {Project, Organization, Config} from 'app/types';
+import {FeatureDisabledHooks} from 'app/types/hooks';
 import HookStore from 'app/stores/hookStore';
 import SentryTypes from 'app/sentryTypes';
 import withConfig from 'app/utils/withConfig';
@@ -17,7 +18,7 @@ type FeatureProps = {
   features: string[];
   requireAll?: boolean;
   renderDisabled?: Function | boolean;
-  hookName?: string;
+  hookName?: keyof FeatureDisabledHooks;
   children: React.ReactNode;
 };
 
@@ -66,15 +67,13 @@ class Feature extends React.Component<FeatureProps> {
     /**
      * Specify the key to use for hookstore functionality.
      *
-     * The hookstore key that will be checked is:
-     *
-     *     feature-disabled:{hookName}
+     * 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: PropTypes.string,
+    hookName: PropTypes.string as any,
 
     /**
      * If children is a function then will be treated as a render prop and
@@ -164,7 +163,7 @@ class Feature extends React.Component<FeatureProps> {
     // Override the renderDisabled function with a hook store function if there
     // is one registered for the feature.
     if (hookName) {
-      const hooks: Function[] = HookStore.get(`feature-disabled:${hookName}`);
+      const hooks = HookStore.get(hookName);
 
       if (hooks.length > 0) {
         customDisabledRender = hooks[0];

+ 0 - 64
src/sentry/static/sentry/app/components/hook.jsx

@@ -1,64 +0,0 @@
-import PropTypes from 'prop-types';
-import Reflux from 'reflux';
-import createReactClass from 'create-react-class';
-
-import HookStore from 'app/stores/hookStore';
-
-/**
- * Instead of accessing the HookStore directly, use this.
- *
- * If the hook slot needs to perform anything w/ the hooks, you can pass a
- * function as a child and you will receive an object with a `hooks` key
- *
- * Example:
- *
- *   <Hook name="my-hook">
- *     {({hooks}) => hooks.map(hook => (
- *       <Wrapper>{hook}</Wrapper>
- *     ))}
- *   </Hook>
- */
-const Hook = createReactClass({
-  displayName: 'Hook',
-  propTypes: {
-    name: PropTypes.string.isRequired,
-  },
-  mixins: [Reflux.listenTo(HookStore, 'handleHooks')],
-
-  getInitialState() {
-    const {name, ...props} = this.props;
-
-    return {
-      hooks: HookStore.get(name).map(cb => cb(props)),
-    };
-  },
-
-  handleHooks(hookName, hooks) {
-    const {name, ...props} = this.props;
-
-    // Make sure that the incoming hook update matches this component's hook name
-    if (hookName !== name) {
-      return;
-    }
-
-    this.setState(state => ({
-      hooks: hooks.map(cb => cb(props)),
-    }));
-  },
-
-  render() {
-    const {children} = this.props;
-
-    if (!this.state.hooks || !this.state.hooks.length) {
-      return null;
-    }
-
-    if (typeof children === 'function') {
-      return children({hooks: this.state.hooks});
-    }
-
-    return this.state.hooks;
-  },
-});
-
-export default Hook;

+ 70 - 0
src/sentry/static/sentry/app/components/hook.tsx

@@ -0,0 +1,70 @@
+import React from 'react';
+import Reflux from 'reflux';
+import createReactClass from 'create-react-class';
+
+import {HookName, Hooks} from 'app/types/hooks';
+import HookStore from 'app/stores/hookStore';
+
+type Props<H extends HookName> = {
+  /**
+   * The name of the hook as listed in hookstore.add(hookName, callback)
+   */
+  name: H;
+  /**
+   * If children are provided as a function to the Hook, the hooks will be
+   * passed down as a render prop.
+   */
+  children?: (opts: {hooks: Array<Hooks[H]>}) => React.ReactNode;
+} & Omit<Parameters<Hooks[H]>[0], 'name'>;
+
+/**
+ * Instead of accessing the HookStore directly, use this.
+ *
+ * If the hook slot needs to perform anything w/ the hooks, you can pass a
+ * function as a child and you will receive an object with a `hooks` key
+ *
+ * Example:
+ *
+ *   <Hook name="my-hook">
+ *     {({hooks}) => hooks.map(hook => (
+ *       <Wrapper>{hook}</Wrapper>
+ *     ))}
+ *   </Hook>
+ */
+function Hook<H extends HookName>({name, ...props}: Props<H>) {
+  const HookComponent = createReactClass({
+    displayName: `Hook(${name})`,
+    mixins: [Reflux.listenTo(HookStore, 'handleHooks') as any],
+
+    getInitialState() {
+      return {hooks: HookStore.get(name).map(cb => cb(props))};
+    },
+
+    handleHooks(hookName: HookName, hooks: Array<Hooks[H]>) {
+      // Make sure that the incoming hook update matches this component's hook name
+      if (hookName !== name) {
+        return;
+      }
+
+      this.setState({hooks: hooks.map(cb => cb(props))});
+    },
+
+    render() {
+      const {children} = props;
+
+      if (!this.state.hooks || !this.state.hooks.length) {
+        return null;
+      }
+
+      if (typeof children === 'function') {
+        return children({hooks: this.state.hooks});
+      }
+
+      return this.state.hooks;
+    },
+  });
+
+  return <HookComponent />;
+}
+
+export default Hook;

+ 0 - 79
src/sentry/static/sentry/app/components/hookOrDefault.jsx

@@ -1,79 +0,0 @@
-import React from 'react';
-import Reflux from 'reflux';
-import createReactClass from 'create-react-class';
-
-import HookStore from 'app/stores/hookStore';
-
-/**
- * Use this instead of the usual ternery operator when using getsentry hooks. So in lieu of
- *		HookStore.get('component:org-auth-view').length
- *		 ? HookStore.get('component:org-auth-view')[0]()
- *		 : OrganizationAuth
- *
- * do this instead
- *  	HookOrDefault({hookName:'component:org-auth-view', defaultComponent: OrganizationAuth})
- *
- *
- * Note, you will need to add the hookstore function in getsentry first and then register
- * it within sentry as a validHookName
- * See: https://github.com/getsentry/getsentry/blob/master/static/getsentry/gsApp/index.jsx
- *		/app/stores/hookStore.jsx
- *
- * @param {String} name The name of the hook as listed in hookstore.add(hookName, callback)
- * @param {Component} defaultComponent Component that will be shown if no hook is available
- * @param {Function} defaultComponentPromise This is a function that returns a promise (more
- *                   specifically a function that returns the result of a dynamic import using
- *                   `import()`. This will use React.Suspense and React.lazy to render the component.
- *
- */
-
-function HookOrDefault({hookName, defaultComponent, defaultComponentPromise, params}) {
-  const HookOrDefaultComponent = createReactClass({
-    displayName: `HookOrDefaultComponent(${hookName})`,
-    mixins: [Reflux.listenTo(HookStore, 'handleHooks')],
-
-    getInitialState() {
-      return {
-        hooks: HookStore.get(hookName),
-      };
-    },
-
-    handleHooks(hookNameFromStore, hooks) {
-      // Make sure that the incoming hook update matches this component's hook name
-      if (hookName !== hookNameFromStore) {
-        return;
-      }
-
-      this.setState({
-        hooks,
-      });
-    },
-
-    getDefaultComponent() {
-      // If `defaultComponentPromise` is passed, then return a Suspended component
-      if (defaultComponentPromise) {
-        const Component = React.lazy(defaultComponentPromise);
-        return props => (
-          <React.Suspense fallback={null}>
-            <Component {...props} />
-          </React.Suspense>
-        );
-      }
-
-      return defaultComponent;
-    },
-
-    render() {
-      const hookExists = this.state.hooks && this.state.hooks.length;
-      const HookComponent =
-        hookExists && this.state.hooks[0]({params})
-          ? this.state.hooks[0]({params})
-          : this.getDefaultComponent();
-
-      return <HookComponent {...this.props} />;
-    },
-  });
-  return HookOrDefaultComponent;
-}
-
-export default HookOrDefault;

+ 108 - 0
src/sentry/static/sentry/app/components/hookOrDefault.tsx

@@ -0,0 +1,108 @@
+import React from 'react';
+import Reflux from 'reflux';
+import createReactClass from 'create-react-class';
+
+import HookStore from 'app/stores/hookStore';
+
+import {Hooks, HookName} from 'app/types/hooks';
+
+type Params<H extends HookName> = {
+  /**
+   * The name of the hook as listed in hookstore.add(hookName, callback)
+   */
+  hookName: H;
+  /**
+   * Component that will be shown if no hook is available
+   */
+  defaultComponent?: ReturnType<Hooks[H]>;
+  /**
+   * This is a function that returns a promise (more specifically a function
+   * that returns the result of a dynamic import using `import()`. This will
+   * use React.Suspense and React.lazy to render the component.
+   */
+  defaultComponentPromise?: () => Promise<ReturnType<Hooks[H]>>;
+  /**
+   * Parameters to pass into the hook callback
+   */
+  params?: Parameters<Hooks[H]>;
+};
+
+type State<H extends HookName> = {
+  hooks: Array<Hooks[H]>;
+};
+
+/**
+ * Use this instead of the usual ternery operator when using getsentry hooks.
+ * So in lieu of:
+ *
+ *  HookStore.get('component:org-auth-view').length
+ *   ? HookStore.get('component:org-auth-view')[0]()
+ *   : OrganizationAuth
+ *
+ * do this instead:
+ *
+ *   const HookedOrganizationAuth = HookOrDefault({
+ *     hookName:'component:org-auth-view',
+ *     defaultComponent: OrganizationAuth,
+ *   })
+ *
+ * Note, you will need to add the hookstore function in getsentry [0] first and
+ * then register tye types [2] and validHookName [1] in sentry.
+ *
+ * [0] /getsentry/static/getsentry/gsApp/registerHooks.jsx
+ * [1] /sentry/app/stores/hookStore.tsx
+ * [2] /sentry/app/types/hooks.ts
+ */
+function HookOrDefault<H extends HookName>({
+  hookName,
+  defaultComponent,
+  defaultComponentPromise,
+  params,
+}: Params<H>) {
+  type Props = React.ComponentProps<ReturnType<Hooks[H]>>;
+
+  return createReactClass<Props, State<H>>({
+    displayName: `HookOrDefaultComponent(${hookName})`,
+    mixins: [Reflux.listenTo(HookStore, 'handleHooks') as any],
+
+    getInitialState() {
+      return {hooks: HookStore.get(hookName)};
+    },
+
+    handleHooks(hookNameFromStore: HookName, hooks: Array<Hooks[H]>) {
+      // Make sure that the incoming hook update matches this component's hook name
+      if (hookName !== hookNameFromStore) {
+        return;
+      }
+
+      this.setState({hooks});
+    },
+
+    getDefaultComponent() {
+      // If `defaultComponentPromise` is passed, then return a Suspended component
+      if (defaultComponentPromise) {
+        const Component = React.lazy(defaultComponentPromise);
+
+        return (props: Props) => (
+          <React.Suspense fallback={null}>
+            <Component {...props} />
+          </React.Suspense>
+        );
+      }
+
+      return defaultComponent;
+    },
+
+    render() {
+      const hookExists = this.state.hooks && this.state.hooks.length;
+      const HookComponent =
+        hookExists && this.state.hooks[0]({params})
+          ? this.state.hooks[0]({params})
+          : this.getDefaultComponent();
+
+      return <HookComponent {...this.props} />;
+    },
+  });
+}
+
+export default HookOrDefault;

+ 3 - 3
src/sentry/static/sentry/app/components/modals/integrationDetailsModal.tsx

@@ -20,6 +20,7 @@ import marked, {singleLineRenderer} from 'app/utils/marked';
 import space from 'app/styles/space';
 import {IntegrationDetailsModalOptions} from 'app/actionCreators/modal';
 import {Integration} from 'app/types';
+import {Hooks} from 'app/types/hooks';
 
 type Props = {
   closeModal: () => void;
@@ -29,7 +30,6 @@ type Props = {
  * In sentry.io the features list supports rendering plan details. If the hook
  * is not registered for rendering the features list like this simply show the
  * features as a normal list.
- * TODO(TS): Add typing for feature gates
  */
 const defaultFeatureGateComponents = {
   IntegrationFeatures: p =>
@@ -46,7 +46,7 @@ const defaultFeatureGateComponents = {
       ))}
     </ul>
   ),
-};
+} as ReturnType<Hooks['integrations:feature-gates']>;
 
 class IntegrationDetailsModal extends React.Component<Props> {
   static propTypes = {
@@ -189,7 +189,7 @@ class IntegrationDetailsModal extends React.Component<Props> {
   }
 }
 
-const DisabledNotice = styled(({reason, ...p}: {reason: string}) => (
+const DisabledNotice = styled(({reason, ...p}: {reason: React.ReactNode}) => (
   <Flex align="center" flex={1} {...p}>
     <InlineSvg src="icon-circle-exclamation" size="1.5em" />
     <Box ml={1}>{reason}</Box>

+ 3 - 3
src/sentry/static/sentry/app/components/modals/sentryAppDetailsModal.tsx

@@ -18,6 +18,7 @@ import Tag from 'app/views/settings/components/tag';
 import {toPermissions} from 'app/utils/consolidatedScopes';
 import CircleIndicator from 'app/components/circleIndicator';
 import {SentryAppDetailsModalOptions} from 'app/actionCreators/modal';
+import {Hooks} from 'app/types/hooks';
 
 type Props = {
   closeOnInstall?: boolean;
@@ -25,7 +26,6 @@ type Props = {
 } & SentryAppDetailsModalOptions &
   AsyncComponent['props'];
 
-//TODO(TS): Add typing for feature gates
 const defaultFeatureGateComponents = {
   IntegrationFeatures: p =>
     p.children({
@@ -41,7 +41,7 @@ const defaultFeatureGateComponents = {
       ))}
     </ul>
   ),
-};
+} as ReturnType<Hooks['integrations:feature-gates']>;
 
 export default class SentryAppDetailsModal extends AsyncComponent<Props> {
   static propTypes = {
@@ -219,7 +219,7 @@ const Author = styled(Box)`
   color: ${p => p.theme.gray2};
 `;
 
-const DisabledNotice = styled(({reason, ...p}: {reason: string}) => (
+const DisabledNotice = styled(({reason, ...p}: {reason: React.ReactNode}) => (
   <Flex align="center" flex={1} {...p}>
     <InlineSvg src="icon-circle-exclamation" size="1.5em" />
     <Box ml={1}>{reason}</Box>

+ 1 - 1
src/sentry/static/sentry/app/components/projectSelector.jsx

@@ -391,7 +391,7 @@ class ProjectSelectorItem extends React.PureComponent {
           renderCheckbox={({checkbox}) => (
             <Feature
               features={['organizations:global-views']}
-              hookName="project-selector-checkbox"
+              hookName="feature-disabled:project-selector-checkbox"
               renderDisabled={renderDisabledCheckbox}
             >
               {checkbox}

+ 2 - 2
src/sentry/static/sentry/app/components/sidebar/index.jsx

@@ -265,7 +265,7 @@ class Sidebar extends React.Component {
 
                   <Feature
                     features={['events']}
-                    hookName="events-sidebar-item"
+                    hookName="feature-disabled:events-sidebar-item"
                     organization={organization}
                   >
                     <SidebarItem
@@ -358,7 +358,7 @@ class Sidebar extends React.Component {
                   </Feature>
                   <Feature
                     features={['discover']}
-                    hookName="discover-sidebar-item"
+                    hookName="feature-disabled:discover-sidebar-item"
                     organization={organization}
                   >
                     <SidebarItem

+ 1 - 1
src/sentry/static/sentry/app/data/forms/projectDebugFiles.jsx

@@ -54,7 +54,7 @@ export const fields = {
     help: ({organization}) => (
       <Feature
         features={['organizations:custom-symbol-sources']}
-        hookName="custom-symbol-sources"
+        hookName="feature-disabled:custom-symbol-sources"
         organization={organization}
         renderDisabled={p => (
           <FeatureDisabled

Some files were not shown because too many files changed in this diff