Browse Source

ref(ts): Refactor Incident Details to typescript (#14510)

typescript all the things
Billy Vong 5 years ago
parent
commit
c5efd5b36f

+ 1 - 1
src/sentry/static/sentry/app/routes.jsx

@@ -1041,7 +1041,7 @@ function routes() {
             <Route
               path=":incidentId/"
               componentPromise={() =>
-                import(/* webpackChunkName: "OrganizationIncidentDetails" */ 'app/views/incidents/details')
+                import(/* webpackChunkName: "IncidentDetails" */ 'app/views/incidents/details')
               }
               component={errorHandler(LazyLoad)}
             />

+ 11 - 0
src/sentry/static/sentry/app/types/index.tsx

@@ -291,3 +291,14 @@ export type EventView = {
   tags: string[];
   columnWidths: string[];
 };
+
+export type Repository = {
+  dateCreated: string;
+  externalSlug: string;
+  id: string;
+  integrationId: string;
+  name: string;
+  provider: {id: string; name: string};
+  status: string;
+  url: string;
+};

+ 14 - 13
src/sentry/static/sentry/app/views/incidents/details/body.jsx → src/sentry/static/sentry/app/views/incidents/details/body.tsx

@@ -1,3 +1,4 @@
+import {Params} from 'react-router/lib/Router';
 import React from 'react';
 import styled from 'react-emotion';
 
@@ -10,7 +11,6 @@ import NavTabs from 'app/components/navTabs';
 import Placeholder from 'app/components/placeholder';
 import Projects from 'app/utils/projects';
 import SeenByList from 'app/components/seenByList';
-import SentryTypes from 'app/sentryTypes';
 import SideHeader from 'app/views/incidents/details/sideHeader';
 import space from 'app/styles/space';
 import theme from 'app/utils/theme';
@@ -19,16 +19,17 @@ import Activity from './activity';
 import RelatedIssues from './relatedIssues';
 import Suspects from './suspects';
 
-export default class DetailsBody extends React.Component {
-  static propTypes = {
-    incident: SentryTypes.Incident,
-  };
+import {Incident} from '../types';
 
+type Props = {
+  params: Params;
+  incident?: Incident;
+};
+
+export default class DetailsBody extends React.Component<Props> {
   render() {
     const {params, incident} = this.props;
 
-    // Considered loading when there is no incident object
-    const loading = !incident;
     return (
       <StyledPageContent>
         <Main>
@@ -51,15 +52,15 @@ export default class DetailsBody extends React.Component {
             <Activity
               params={params}
               incident={incident}
-              incidentStatus={!loading ? incident.status : null}
+              incidentStatus={!!incident ? incident.status : null}
             />
           </PageContent>
         </Main>
         <Sidebar>
           <PageContent>
-            <SideHeader loading={loading}>{t('Events in Incident')}</SideHeader>
+            <SideHeader loading={!incident}>{t('Events in Incident')}</SideHeader>
 
-            {!loading ? (
+            {incident ? (
               <Chart
                 data={incident.eventStats.data}
                 detected={incident.dateDetected}
@@ -70,11 +71,11 @@ export default class DetailsBody extends React.Component {
             )}
 
             <div>
-              <SideHeader loading={loading}>
-                {t('Projects Affected')} ({!loading ? incident.projects.length : '-'})
+              <SideHeader loading={!incident}>
+                {t('Projects Affected')} ({incident ? incident.projects.length : '-'})
               </SideHeader>
 
-              {!loading && (
+              {incident && (
                 <div>
                   <Projects slugs={incident.projects} orgId={params.orgId}>
                     {({projects}) => {

+ 12 - 15
src/sentry/static/sentry/app/views/incidents/details/chart.jsx → src/sentry/static/sentry/app/views/incidents/details/chart.tsx

@@ -1,4 +1,3 @@
-import PropTypes from 'prop-types';
 import React from 'react';
 import moment from 'moment';
 
@@ -9,6 +8,8 @@ import MarkPoint from 'app/components/charts/components/markPoint';
 import closedSymbol from './closedSymbol';
 import detectedSymbol from './detectedSymbol';
 
+type Data = [number, {count: number}[]][];
+
 /**
  * So we'll have to see how this looks with real data, but echarts requires
  * an explicit (x,y) value to draw a symbol (incident detected/closed bubble).
@@ -18,8 +19,11 @@ import detectedSymbol from './detectedSymbol';
  * AFAICT we can't give it an x-axis value and have it draw on the line,
  * so we probably need to calculate the y-axis value ourselves if we want it placed
  * at the exact time.
+ *
+ * @param data Data array
+ * @param needle the target timestamp
  */
-function getNearbyIndex(data, needle) {
+function getNearbyIndex(data: Data, needle: number) {
   // `data` is sorted, return the first index whose value (timestamp) is > `needle`
   const index = data.findIndex(([ts]) => ts > needle);
 
@@ -31,20 +35,13 @@ function getNearbyIndex(data, needle) {
   return index !== -1 ? index - 1 : data.length - 1;
 }
 
-export default class Chart extends React.PureComponent {
-  static propTypes = {
-    data: PropTypes.arrayOf(
-      PropTypes.arrayOf(
-        PropTypes.oneOfType([
-          PropTypes.number,
-          PropTypes.arrayOf(PropTypes.shape({count: PropTypes.number})),
-        ])
-      )
-    ),
-    detected: PropTypes.string,
-    closed: PropTypes.string,
-  };
+type Props = {
+  data: Data;
+  detected: string;
+  closed: string;
+};
 
+export default class Chart extends React.PureComponent<Props> {
   render() {
     const {data, detected, closed} = this.props;
 

+ 0 - 0
src/sentry/static/sentry/app/views/incidents/details/closedSymbol.jsx → src/sentry/static/sentry/app/views/incidents/details/closedSymbol.tsx


+ 0 - 0
src/sentry/static/sentry/app/views/incidents/details/detectedSymbol.jsx → src/sentry/static/sentry/app/views/incidents/details/detectedSymbol.tsx


+ 31 - 26
src/sentry/static/sentry/app/views/incidents/details/header.jsx → src/sentry/static/sentry/app/views/incidents/details/header.tsx

@@ -1,5 +1,5 @@
+import {Params} from 'react-router/lib/Router';
 import {Link} from 'react-router';
-import PropTypes from 'prop-types';
 import React from 'react';
 import moment from 'moment';
 import styled from 'react-emotion';
@@ -14,23 +14,25 @@ import InlineSvg from 'app/components/inlineSvg';
 import LoadingError from 'app/components/loadingError';
 import MenuItem from 'app/components/menuItem';
 import PageHeading from 'app/components/pageHeading';
-import SentryTypes from 'app/sentryTypes';
 import SubscribeButton from 'app/components/subscribeButton';
 import space from 'app/styles/space';
 import getDynamicText from 'app/utils/getDynamicText';
 
 import {isOpen} from '../utils';
 import Status from '../status';
-
-export default class DetailsHeader extends React.Component {
-  static propTypes = {
-    incident: SentryTypes.Incident,
-    params: PropTypes.object.isRequired,
-    hasIncidentDetailsError: PropTypes.bool.isRequired,
-    onSubscriptionChange: PropTypes.func.isRequired,
-    onStatusChange: PropTypes.func.isRequired,
-  };
-
+import {Incident} from '../types';
+
+type Props = {
+  className?: string;
+  hasIncidentDetailsError: boolean;
+  // Can be undefined when loading
+  incident?: Incident;
+  onSubscriptionChange: (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
+  onStatusChange: (eventKey: any) => void;
+  params: Params;
+};
+
+export default class DetailsHeader extends React.Component<Props> {
   renderStatus() {
     const {incident, onStatusChange} = this.props;
 
@@ -59,15 +61,18 @@ export default class DetailsHeader extends React.Component {
   render() {
     const {hasIncidentDetailsError, incident, params, onSubscriptionChange} = this.props;
     const isIncidentReady = !!incident && !hasIncidentDetailsError;
-    const eventLink = incident && {
-      pathname: `/organizations/${params.orgId}/events/`,
-
-      // Note we don't have project selector on here so there should be
-      // no query params to forward
-      query: {
-        group: incident.groups,
-      },
-    };
+    const eventLink = incident
+      ? {
+          pathname: `/organizations/${params.orgId}/events/`,
+
+          // Note we don't have project selector on here so there should be
+          // no query params to forward
+          query: {
+            group: incident.groups,
+          },
+        }
+      : '';
+
     const dateStarted = incident && moment(incident.dateStarted).format('LL');
     const duration =
       incident &&
@@ -93,7 +98,7 @@ export default class DetailsHeader extends React.Component {
               )}
             </Breadcrumb>
             <IncidentTitle data-test-id="incident-title" loading={!isIncidentReady}>
-              {isIncidentReady ? incident.title : 'Loading'}
+              {incident && !hasIncidentDetailsError ? incident.title : 'Loading'}
             </IncidentTitle>
           </PageHeading>
         </HeaderItem>
@@ -107,7 +112,7 @@ export default class DetailsHeader extends React.Component {
             </HeaderItem>
             <HeaderItem>
               <ItemTitle>{t('Duration')}</ItemTitle>
-              {isIncidentReady && (
+              {incident && (
                 <ItemValue>
                   <Duration seconds={getDynamicText({value: duration, fixed: 1200})} />
                 </ItemValue>
@@ -115,7 +120,7 @@ export default class DetailsHeader extends React.Component {
             </HeaderItem>
             <HeaderItem>
               <ItemTitle>{t('Users affected')}</ItemTitle>
-              {isIncidentReady && (
+              {incident && (
                 <ItemValue>
                   <Count value={incident.uniqueUsers} />
                 </ItemValue>
@@ -123,7 +128,7 @@ export default class DetailsHeader extends React.Component {
             </HeaderItem>
             <HeaderItem>
               <ItemTitle>{t('Total events')}</ItemTitle>
-              {isIncidentReady && (
+              {incident && (
                 <ItemValue>
                   <Count value={incident.totalEvents} />
                   <OpenLink to={eventLink}>
@@ -198,7 +203,7 @@ const Breadcrumb = styled('div')`
   margin-bottom: ${space(1)};
 `;
 
-const IncidentTitle = styled('div')`
+const IncidentTitle = styled('div')<{loading: boolean}>`
   ${p => p.loading && 'opacity: 0'};
 `;
 

+ 55 - 45
src/sentry/static/sentry/app/views/incidents/details/index.jsx → src/sentry/static/sentry/app/views/incidents/details/index.tsx

@@ -1,6 +1,7 @@
-import PropTypes from 'prop-types';
+import {Params} from 'react-router/lib/Router';
 import React from 'react';
 
+import {Client} from 'app/api';
 import {addErrorMessage} from 'app/actionCreators/indicator';
 import {fetchOrgMembers} from 'app/actionCreators/members';
 import {markIncidentAsSeen} from 'app/actionCreators/incident';
@@ -16,16 +17,21 @@ import {
 } from '../utils';
 import DetailsBody from './body';
 import DetailsHeader from './header';
+import {Incident} from '../types';
 
-class OrganizationIncidentDetails extends React.Component {
-  static propTypes = {
-    api: PropTypes.object.isRequired,
-  };
+type Props = {
+  api: Client;
+  params: Params;
+};
 
-  constructor(props) {
-    super(props);
-    this.state = {isLoading: false, hasError: false};
-  }
+type State = {
+  isLoading: boolean;
+  hasError: boolean;
+  incident?: Incident;
+};
+
+class IncidentDetails extends React.Component<Props, State> {
+  state: State = {isLoading: false, hasError: false};
 
   componentDidMount() {
     const {api, params} = this.props;
@@ -33,51 +39,61 @@ class OrganizationIncidentDetails extends React.Component {
     this.fetchData();
   }
 
-  fetchData = () => {
+  fetchData = async () => {
     this.setState({isLoading: true, hasError: false});
+
     const {
       api,
       params: {orgId, incidentId},
     } = this.props;
 
-    fetchIncident(api, orgId, incidentId)
-      .then(incident => {
-        this.setState({incident, isLoading: false, hasError: false});
-        markIncidentAsSeen(api, orgId, incident);
-      })
-      .catch(() => {
-        this.setState({isLoading: false, hasError: true});
-      });
+    try {
+      const incident = await fetchIncident(api, orgId, incidentId);
+      this.setState({incident, isLoading: false, hasError: false});
+      markIncidentAsSeen(api, orgId, incident);
+    } catch (_err) {
+      this.setState({isLoading: false, hasError: true});
+    }
   };
 
-  handleSubscriptionChange = () => {
+  handleSubscriptionChange = async () => {
     const {
       api,
       params: {orgId, incidentId},
     } = this.props;
 
+    if (!this.state.incident) {
+      return;
+    }
+
     const isSubscribed = this.state.incident.isSubscribed;
 
     const newIsSubscribed = !isSubscribed;
 
     this.setState(state => ({
-      incident: {...state.incident, isSubscribed: newIsSubscribed},
+      incident: {...(state.incident as Incident), isSubscribed: newIsSubscribed},
     }));
 
-    updateSubscription(api, orgId, incidentId, newIsSubscribed).catch(() => {
+    try {
+      updateSubscription(api, orgId, incidentId, newIsSubscribed);
+    } catch (_err) {
       this.setState(state => ({
-        incident: {...state.incident, isSubscribed},
+        incident: {...(state.incident as Incident), isSubscribed},
       }));
       addErrorMessage(t('An error occurred, your subscription status was not changed.'));
-    });
+    }
   };
 
-  handleStatusChange = () => {
+  handleStatusChange = async () => {
     const {
       api,
       params: {orgId, incidentId},
     } = this.props;
 
+    if (!this.state.incident) {
+      return;
+    }
+
     const {status} = this.state.incident;
 
     const newStatus = isOpen(this.state.incident)
@@ -85,22 +101,21 @@ class OrganizationIncidentDetails extends React.Component {
       : IncidentStatus.CREATED;
 
     this.setState(state => ({
-      incident: {...state.incident, status: newStatus},
+      incident: {...(state.incident as Incident), status: newStatus},
     }));
 
-    updateStatus(api, orgId, incidentId, newStatus)
-      .then(incident => {
-        // Update entire incident object because updating status can cause other parts
-        // of the model to change (e.g close date)
-        this.setState({incident});
-      })
-      .catch(() => {
-        this.setState(state => ({
-          incident: {...state.incident, status},
-        }));
-
-        addErrorMessage(t('An error occurred, your incident status was not changed.'));
-      });
+    try {
+      const incident = await updateStatus(api, orgId, incidentId, newStatus);
+      // Update entire incident object because updating status can cause other parts
+      // of the model to change (e.g close date)
+      this.setState({incident});
+    } catch (_err) {
+      this.setState(state => ({
+        incident: {...(state.incident as Incident), status},
+      }));
+
+      addErrorMessage(t('An error occurred, your incident status was not changed.'));
+    }
   };
 
   render() {
@@ -117,15 +132,10 @@ class OrganizationIncidentDetails extends React.Component {
           onStatusChange={this.handleStatusChange}
         />
 
-        <DetailsBody
-          hasIncidentDetailsError={hasError}
-          params={params}
-          incident={incident}
-        />
+        <DetailsBody params={params} incident={incident} />
       </React.Fragment>
     );
   }
 }
 
-export {OrganizationIncidentDetails};
-export default withApi(OrganizationIncidentDetails);
+export default withApi(IncidentDetails);

+ 8 - 2
src/sentry/static/sentry/app/views/incidents/details/sideHeader.jsx → src/sentry/static/sentry/app/views/incidents/details/sideHeader.tsx

@@ -3,7 +3,13 @@ import styled from 'react-emotion';
 
 import space from 'app/styles/space';
 
-const SideHeader = styled(function Styled({className, loading, children}) {
+type Props = {
+  className?: string;
+  loading: boolean;
+  children: React.ReactNode;
+};
+
+const SideHeader = styled(function SideHeader({className, loading, children}: Props) {
   return (
     <h6 className={className}>
       <Title loading={loading}>{children}</Title>
@@ -16,7 +22,7 @@ const SideHeader = styled(function Styled({className, loading, children}) {
   text-transform: uppercase;
 `;
 
-const Title = styled('span')`
+const Title = styled('span')<{loading: boolean}>`
   ${p =>
     p.loading
       ? `

+ 13 - 8
src/sentry/static/sentry/app/views/incidents/details/suspects.jsx → src/sentry/static/sentry/app/views/incidents/details/suspects.tsx

@@ -14,13 +14,18 @@ import overflowEllipsis from 'app/styles/overflowEllipsis';
 import space from 'app/styles/space';
 
 import SideHeader from './sideHeader';
+import {IncidentSuspect} from '../types';
+
+type Props = {
+  className?: string;
+  loading: boolean;
+  suspects: {
+    type: 'commit';
+    data: IncidentSuspect;
+  }[];
+};
 
-class Suspects extends React.Component {
-  static propTypes = {
-    suspects: PropTypes.arrayOf(SentryTypes.IncidentSuspect),
-    loading: PropTypes.bool,
-  };
-
+class Suspects extends React.Component<Props> {
   render() {
     const {className, loading, suspects} = this.props;
 
@@ -59,12 +64,12 @@ class Suspects extends React.Component {
   }
 }
 
-const StyledSuspects = styled(Suspects)`
+const StyledSuspects = styled(Suspects)<Props>`
   margin-top: ${space(1)};
 `;
 
 export default class SuspectsContainer extends AsyncComponent {
-  getEndpoints() {
+  getEndpoints(): [string, string][] {
     const {orgId, incidentId} = this.props.params;
 
     return [['data', `/organizations/${orgId}/incidents/${incidentId}/suspects/`]];

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