Browse Source

feat(typescript): Add TypeScript compatibility (#13786)

Updates tsconfig with allowJs: false and noImplicityAny: false.
This prevents the TypeScript engine from processing *.jsx files and makes it easier to import jsx files in TypeScript code. Once we have migrated more code to *.tsx we should look into turning `noImplicitAny` back to true.

This migrates the following code to TypeScript
- All Discover functionality
- Some utility functions
Daniel Griesser 5 years ago
parent
commit
7823ddb647

+ 8 - 1
package.json

@@ -22,7 +22,14 @@
     "@sentry/integrations": "^5.4.2",
     "@sentry/typescript": "^5.3.0",
     "@types/lodash": "^4.14.134",
-    "@types/react-dom": "^16.8.4",
+    "@types/moment-timezone": "^0.5.12",
+    "@types/papaparse": "^4.5.11",
+    "@types/react": "^16.7.0",
+    "@types/react-bootstrap": "^0.32.19",
+    "@types/react-document-title": "^2.0.3",
+    "@types/react-dom": "^16.7.0",
+    "@types/react-router": "^3.0.20",
+    "@types/react-virtualized": "^9.20.1",
     "algoliasearch": "^3.32.0",
     "babel-core": "^7.0.0-bridge.0",
     "babel-loader": "^8.0.0",

+ 0 - 0
src/sentry/static/sentry/app/constants/chartPalette.jsx → src/sentry/static/sentry/app/constants/chartPalette.tsx


+ 1 - 1
src/sentry/static/sentry/app/styles/space.jsx → src/sentry/static/sentry/app/styles/space.tsx

@@ -1,4 +1,4 @@
-const space = size => {
+const space = (size: number): string => {
   switch (size) {
     //Our spacing scale is based on a base unit of 8
     //We use a case switch to prevent odd numbers, decimals, and big numbers.

+ 124 - 112
src/sentry/static/sentry/app/utils/theme.jsx → src/sentry/static/sentry/app/utils/theme.tsx

@@ -1,8 +1,6 @@
 import CHART_PALETTE from 'app/constants/chartPalette';
 
-const theme = {
-  breakpoints: ['768px', '992px', '1200px'],
-
+const colors = {
   // Colors
   offWhite: '#FAF9FB',
   offWhite2: '#E7E1EC',
@@ -71,6 +69,107 @@ const theme = {
 
   background: '#fff',
   placeholderBackground: '#f5f5f5',
+};
+
+const warning = {
+  backgroundLight: colors.yellowLightest,
+  background: colors.yellowDarkest,
+  border: colors.yellowDark,
+  iconColor: colors.yellowDark,
+};
+
+const alert = {
+  muted: {
+    backgroundLight: colors.offWhite,
+    background: colors.gray1,
+    border: colors.gray6,
+  },
+  info: {
+    backgroundLight: colors.blueLightest,
+    border: colors.blueLight,
+    iconColor: colors.blue,
+    background: colors.blue,
+  },
+  warning,
+  //alias warn to warning
+  warn: warning,
+  success: {
+    backgroundLight: colors.greenLightest,
+    border: colors.green,
+    iconColor: colors.greenDark,
+    background: colors.green,
+  },
+  error: {
+    backgroundLight: colors.redLightest,
+    border: colors.redLight,
+    textLight: colors.redLight,
+    iconColor: colors.red,
+    background: colors.red,
+  },
+  beta: {
+    background: `linear-gradient(90deg, ${colors.pink}, ${colors.purple})`,
+  },
+};
+
+const aliases = {
+  textColor: colors.gray5,
+  success: colors.green,
+  error: colors.red,
+  disabled: colors.gray1,
+};
+
+const button = {
+  borderRadius: '3px',
+
+  default: {
+    color: '#2f2936',
+    colorActive: '#161319',
+    background: colors.white,
+    backgroundActive: colors.white,
+    border: '#d8d2de',
+    borderActive: '#c9c0d1',
+  },
+  primary: {
+    color: colors.white,
+    background: colors.purple,
+    backgroundActive: '#4e3fb4',
+    border: '#3d328e',
+    borderActive: '#352b7b',
+  },
+  success: {
+    color: colors.white,
+    background: '#3fa372',
+    backgroundActive: colors.green,
+    border: '#7ccca5',
+    borderActive: '#7ccca5',
+  },
+  danger: {
+    color: colors.white,
+    background: colors.red,
+    backgroundActive: '#bf2a1d',
+    border: '#bf2a1d',
+    borderActive: '#7d1c13',
+  },
+  link: {
+    color: colors.blue,
+    background: 'transparent',
+    // border: '#3d328e',
+    backgroundActive: 'transparent',
+    // borderActive: '#352b7b',
+  },
+  disabled: {
+    color: aliases.disabled,
+    border: '#e3e5e6',
+    borderActive: '#e3e5e6',
+    background: colors.white,
+    backgroundActive: colors.white,
+  },
+};
+
+const theme = {
+  breakpoints: ['768px', '992px', '1200px'],
+
+  ...colors,
 
   // Try to keep these ordered plz
   zIndex: {
@@ -142,123 +241,36 @@ const theme = {
     lineHeightHeading: '1.15',
     lineHeightBody: '1.4',
   },
-};
 
-// Aliases
-theme.textColor = theme.gray5;
-theme.success = theme.green;
-theme.error = theme.red;
-theme.disabled = theme.gray1;
+  // Aliases
+  ...aliases,
 
-theme.alert = {
-  muted: {
-    backgroundLight: theme.offWhite,
-    background: theme.gray1,
-    border: theme.gray6,
-  },
-  info: {
-    backgroundLight: theme.blueLightest,
-    border: theme.blueLight,
-    iconColor: theme.blue,
-    background: theme.blue,
-  },
-  warning: {
-    backgroundLight: theme.yellowLightest,
-    background: theme.yellowDarkest,
-    border: theme.yellowDark,
-    iconColor: theme.yellowDark,
-  },
-  success: {
-    backgroundLight: theme.greenLightest,
-    border: theme.green,
-    iconColor: theme.greenDark,
-    background: theme.green,
-  },
-  error: {
-    backgroundLight: theme.redLightest,
-    border: theme.redLight,
-    textLight: theme.redLight,
-    iconColor: theme.red,
-    background: theme.red,
-  },
-  beta: {
-    background: `linear-gradient(90deg, ${theme.pink}, ${theme.purple})`,
-  },
-};
+  alert,
+  button,
 
-//alias warn to warning
-theme.alert.warn = theme.alert.warning;
+  charts: {
+    colors: CHART_PALETTE[CHART_PALETTE.length - 1],
 
-theme.button = {
-  borderRadius: '3px',
+    // We have an array that maps `number + 1` --> list of `number` colors
+    getColorPalette: (length: number) =>
+      CHART_PALETTE[Math.min(CHART_PALETTE.length - 1, length + 1)],
 
-  default: {
-    color: '#2f2936',
-    colorActive: '#161319',
-    background: theme.white,
-    backgroundActive: theme.white,
-    border: '#d8d2de',
-    borderActive: '#c9c0d1',
+    previousPeriod: colors.gray1,
+    symbolSize: 6,
   },
-  primary: {
-    color: theme.white,
-    background: theme.purple,
-    backgroundActive: '#4e3fb4',
-    border: '#3d328e',
-    borderActive: '#352b7b',
-  },
-  success: {
-    color: theme.white,
-    background: '#3fa372',
-    backgroundActive: theme.green,
-    border: '#7ccca5',
-    borderActive: '#7ccca5',
-  },
-  danger: {
-    color: theme.white,
-    background: theme.red,
-    backgroundActive: '#bf2a1d',
-    border: '#bf2a1d',
-    borderActive: '#7d1c13',
-  },
-  link: {
-    color: theme.blue,
-    background: 'transparent',
-    // border: '#3d328e',
-    backgroundActive: 'transparent',
-    // borderActive: '#352b7b',
-  },
-  disabled: {
-    color: theme.disabled,
-    border: '#e3e5e6',
-    borderActive: '#e3e5e6',
-    background: theme.white,
-    backgroundActive: theme.white,
-  },
-};
-
-theme.charts = {
-  colors: CHART_PALETTE[CHART_PALETTE.length - 1],
 
-  // We have an array that maps `number + 1` --> list of `number` colors
-  getColorPalette: length =>
-    CHART_PALETTE[Math.min(CHART_PALETTE.length - 1, length + 1)],
-
-  previousPeriod: theme.gray1,
-  symbolSize: 6,
-};
-
-theme.diff = {
-  removedRow: '#fcefee',
-  addedRow: '#f5fbf8',
-  removed: '#f7ceca',
-  added: '#d8f0e4',
-};
+  diff: {
+    removedRow: '#fcefee',
+    addedRow: '#f5fbf8',
+    removed: '#f7ceca',
+    added: '#d8f0e4',
+  },
 
-// Similarity spectrum used in "Similar Issues" in group details
-theme.similarity = {
-  empty: '#e2dee6',
-  colors: ['#ec5e44', '#f38259', '#f9a66d', '#98b480', '#57be8c'],
+  // Similarity spectrum used in "Similar Issues" in group details
+  similarity: {
+    empty: '#e2dee6',
+    colors: ['#ec5e44', '#f38259', '#f9a66d', '#98b480', '#57be8c'],
+  },
 };
 
 export default theme;

+ 35 - 25
src/sentry/static/sentry/app/views/organizationDiscover/aggregations/aggregation.jsx → src/sentry/static/sentry/app/views/organizationDiscover/aggregations/aggregation.tsx

@@ -1,29 +1,37 @@
 import React from 'react';
 import {Value} from 'react-select';
-import PropTypes from 'prop-types';
 
-import SelectControl from 'app/components/forms/selectControl';
 import {t} from 'app/locale';
+import SelectControl from 'app/components/forms/selectControl';
 
 import {getInternal, getExternal} from './utils';
+import {Aggregation, DiscoverBaseProps, ReactSelectOption} from '../types';
 import {PlaceholderText} from '../styles';
 import {ARRAY_FIELD_PREFIXES} from '../data';
 
-export default class Aggregation extends React.Component {
-  static propTypes = {
-    value: PropTypes.array.isRequired,
-    onChange: PropTypes.func.isRequired,
-    columns: PropTypes.array.isRequired,
-    disabled: PropTypes.bool,
-  };
+type AggregationProps = DiscoverBaseProps & {
+  value: Aggregation;
+  onChange: (value: Aggregation) => void;
+};
 
-  constructor(props) {
-    super(props);
-    this.state = {
-      inputValue: '',
-      isOpen: false,
-    };
-  }
+type AggregationState = {
+  inputValue: string;
+  isOpen: boolean;
+};
+
+const initalState = {
+  inputValue: '',
+  isOpen: false,
+};
+
+export default class AggregationRow extends React.Component<
+  AggregationProps,
+  AggregationState
+> {
+  // This is the ref of the inner react-select component
+  private select: any;
+
+  state = initalState;
 
   getOptions() {
     const currentValue = getInternal(this.props.value);
@@ -34,7 +42,7 @@ export default class Aggregation extends React.Component {
   filterOptions = () => {
     const input = this.state.inputValue;
 
-    let optionList = [
+    let optionList: Array<ReactSelectOption> = [
       {value: 'count', label: 'count'},
       {value: 'uniq', label: 'uniq(...)'},
       {value: 'avg', label: 'avg(...)'},
@@ -65,7 +73,7 @@ export default class Aggregation extends React.Component {
     this.select.focus();
   }
 
-  handleChange = option => {
+  handleChange = (option: ReactSelectOption) => {
     if (option.value === 'uniq' || option.value === 'avg') {
       this.setState({inputValue: option.value}, this.focus);
     } else {
@@ -83,12 +91,14 @@ export default class Aggregation extends React.Component {
     }
   };
 
-  inputRenderer = props => {
-    const onChange = evt => {
-      if (evt.target.value === '') {
+  inputRenderer = (props: AggregationProps) => {
+    const onChange = (evt: any) => {
+      if (evt && evt.target && evt.target.value === '') {
         // React select won't trigger an onChange event when a value is completely
         // cleared, so we'll force this before calling onChange
-        this.setState({inputValue: evt.target.value}, props.onChange(evt));
+        this.setState({inputValue: evt.target.value}, () => {
+          props.onChange(evt);
+        });
       } else {
         props.onChange(evt);
       }
@@ -105,7 +115,7 @@ export default class Aggregation extends React.Component {
     );
   };
 
-  valueComponent = props => {
+  valueComponent = (props: AggregationProps) => {
     if (this.state.isOpen) {
       return null;
     }
@@ -113,7 +123,7 @@ export default class Aggregation extends React.Component {
     return <Value {...props} />;
   };
 
-  handleInputChange = value => {
+  handleInputChange = (value: string) => {
     this.setState({
       inputValue: value,
     });
@@ -123,7 +133,7 @@ export default class Aggregation extends React.Component {
     return (
       <div>
         <SelectControl
-          innerRef={ref => (this.select = ref)}
+          innerRef={(ref: any) => (this.select = ref)}
           value={getInternal(this.props.value)}
           placeholder={
             <PlaceholderText>{t('Add aggregation function...')}</PlaceholderText>

+ 16 - 16
src/sentry/static/sentry/app/views/organizationDiscover/aggregations/index.jsx → src/sentry/static/sentry/app/views/organizationDiscover/aggregations/index.tsx

@@ -1,32 +1,32 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import Link from 'app/components/links/link';
-import InlineSvg from 'app/components/inlineSvg';
 import {t} from 'app/locale';
 
-import Aggregation from './aggregation';
+import InlineSvg from 'app/components/inlineSvg';
+import Link from 'app/components/links/link';
+
+import AggregationRow from './aggregation';
 import {PlaceholderText, SelectListItem, AddText, SidebarLabel} from '../styles';
+import {Aggregation, DiscoverBaseProps} from '../types';
 
-export default class Aggregations extends React.Component {
-  static propTypes = {
-    value: PropTypes.array.isRequired,
-    onChange: PropTypes.func.isRequired,
-    columns: PropTypes.array,
-    disabled: PropTypes.bool,
-  };
+type AggregationsProps = DiscoverBaseProps & {
+  value: Aggregation[];
+  onChange: (value: Aggregation[]) => void;
+};
 
+export default class Aggregations extends React.Component<AggregationsProps> {
   addRow() {
-    this.props.onChange([...this.props.value, [null, null, null]]);
+    const aggregations: any[] = [...this.props.value, [null, null, null]];
+    this.props.onChange(aggregations);
   }
 
-  removeRow(idx) {
+  removeRow(idx: number) {
     const aggregations = this.props.value.slice();
     aggregations.splice(idx, 1);
     this.props.onChange(aggregations);
   }
 
-  handleChange(val, idx) {
+  handleChange(val: Aggregation, idx: number) {
     const aggregations = this.props.value.slice();
 
     aggregations[idx] = val;
@@ -52,9 +52,9 @@ export default class Aggregations extends React.Component {
         )}
         {value.map((aggregation, idx) => (
           <SelectListItem key={`${idx}_${aggregation[2]}`}>
-            <Aggregation
+            <AggregationRow
               value={aggregation}
-              onChange={val => this.handleChange(val, idx)}
+              onChange={(val: Aggregation) => this.handleChange(val, idx)}
               columns={columns}
               disabled={disabled}
             />

+ 29 - 30
src/sentry/static/sentry/app/views/organizationDiscover/aggregations/utils.jsx → src/sentry/static/sentry/app/views/organizationDiscover/aggregations/utils.tsx

@@ -1,13 +1,15 @@
+import {Column, Aggregation} from '../types';
+
 /**
  * Returns true if an aggregation is valid and false if not
  *
- * @param {Array} aggregation Aggregation in external Snuba format
- * @param {Object} cols List of column objects
- * @param {String} cols.name Column name
- * @param {String} cols.type Type of column
- * @returns {Boolean} True if valid aggregation, false if not
+ * @param aggregation Aggregation in external Snuba format
+ * @param cols List of column objects
+ * @param cols.name Column name
+ * @param cols.type Type of column
+ * @returns True if valid aggregation, false if not
  */
-export function isValidAggregation(aggregation, cols) {
+export function isValidAggregation(aggregation: Aggregation, cols: Column[]): boolean {
   const columns = new Set(cols.map(({name}) => name));
   const [func, col] = aggregation;
 
@@ -16,18 +18,18 @@ export function isValidAggregation(aggregation, cols) {
   }
 
   if (func === 'count()') {
-    return col === null;
+    return !col;
   }
 
   if (func === 'uniq') {
-    return columns.has(col);
+    return columns.has(col || '');
   }
 
   if (func === 'avg') {
     const validCols = new Set(
       cols.filter(({type}) => type === 'number').map(({name}) => name)
     );
-    return validCols.has(col);
+    return validCols.has(col || '');
   }
 
   return false;
@@ -36,13 +38,13 @@ export function isValidAggregation(aggregation, cols) {
 /**
  * Converts aggregation from external Snuba format to internal format for dropdown
  *
- * @param {Array} external Aggregation in external Snuba format
- * @return {String} Aggregation in internal format
- **/
-export function getInternal(external) {
+ * @param external Aggregation in external Snuba format
+ * @return Aggregation in internal format
+ */
+export function getInternal(external: Aggregation): string {
   const [func, col] = external;
 
-  if (func === null) {
+  if (!func) {
     return '';
   }
 
@@ -66,10 +68,10 @@ export function getInternal(external) {
  * or a string with an underscore instead of square brackets for tags. We'll also
  * replace the characters `.`, `:` and `-` from aliases.
  *
- * @param {String} columnName Name of column
- * @return {String} Alias
+ * @param columnName Name of column
+ * @return Alias
  */
-function getAlias(columnName) {
+function getAlias(columnName: string): string {
   const tagMatch = columnName.match(/^tags\[(.+)]$/);
   return tagMatch
     ? `tags_${tagMatch[1].replace(/[.:-]/, '_')}`
@@ -79,27 +81,24 @@ function getAlias(columnName) {
 /**
  * Converts aggregation internal string format to external Snuba representation
  *
- * @param {String} internal Aggregation in internal format
- * @return {Array} Aggregation in external Snuba format
+ * @param internal Aggregation in internal format
+ * @return Aggregation in external Snuba format
  */
-export function getExternal(internal) {
+export function getExternal(internal: string): Aggregation {
   const uniqRegex = /^uniq\((.+)\)$/;
   const avgRegex = /^avg\((.+)\)$/;
 
-  if (internal === 'count') {
-    return ['count()', null, 'count'];
-  }
-
-  if (internal.match(uniqRegex)) {
-    const column = internal.match(uniqRegex)[1];
-
+  let match = internal.match(uniqRegex);
+  if (match && match[1]) {
+    const column = match[1];
     return ['uniq', column, `uniq_${getAlias(column)}`];
   }
 
-  if (internal.match(avgRegex)) {
-    const column = internal.match(avgRegex)[1];
+  match = internal.match(avgRegex);
+  if (match && match[1]) {
+    const column = match[1];
     return ['avg', column, `avg_${getAlias(column)}`];
   }
 
-  return internal;
+  return ['count()', null, 'count'];
 }

+ 12 - 9
src/sentry/static/sentry/app/views/organizationDiscover/analytics.jsx → src/sentry/static/sentry/app/views/organizationDiscover/analytics.tsx

@@ -1,4 +1,5 @@
 import {analytics} from 'app/utils/analytics';
+import {Organization, Query} from './types';
 
 /**
  * Takes an organization and query and tracks in Redash as discover.query.
@@ -8,8 +9,8 @@ import {analytics} from 'app/utils/analytics';
  * @param {Object} query Query that is sent to Snuba
  * @returns {Void}
  */
-export function trackQuery(organization, query) {
-  const data = {
+export function trackQuery(organization: Organization, query: Query) {
+  const data: Query & {org_id: number} = {
     org_id: parseInt(organization.id, 10),
     projects: query.projects,
     fields: query.fields,
@@ -21,13 +22,15 @@ export function trackQuery(organization, query) {
     data.limit = query.limit;
   }
 
-  data.conditions = query.conditions.map(condition => {
-    return [
-      condition[0],
-      condition[1],
-      typeof condition[2] === 'string' ? '[REDACTED]' : condition[2],
-    ];
-  });
+  data.conditions =
+    query.conditions &&
+    query.conditions.map(condition => {
+      return [
+        condition[0],
+        condition[1],
+        typeof condition[2] === 'string' ? '[REDACTED]' : condition[2],
+      ];
+    });
 
   analytics('discover.query', data);
 }

+ 48 - 35
src/sentry/static/sentry/app/views/organizationDiscover/conditions/condition.jsx → src/sentry/static/sentry/app/views/organizationDiscover/conditions/condition.tsx

@@ -1,38 +1,45 @@
 import React from 'react';
 import {Value} from 'react-select';
-import PropTypes from 'prop-types';
 import styled from 'react-emotion';
 import {t} from 'app/locale';
-import SelectControl from 'app/components/forms/selectControl';
 import space from 'app/styles/space';
 
+import SelectControl from 'app/components/forms/selectControl';
+
 import {getInternal, getExternal, isValidCondition, ignoreCase} from './utils';
 import {CONDITION_OPERATORS, ARRAY_FIELD_PREFIXES} from '../data';
 import {PlaceholderText} from '../styles';
+import {DiscoverBaseProps, Condition, ReactSelectOption} from '../types';
 
-export default class Condition extends React.Component {
-  static propTypes = {
-    value: PropTypes.array.isRequired,
-    onChange: PropTypes.func.isRequired,
-    columns: PropTypes.arrayOf(
-      PropTypes.shape({name: PropTypes.string, type: PropTypes.string})
-    ).isRequired,
-    disabled: PropTypes.bool,
-  };
+type ConditionProps = DiscoverBaseProps & {
+  value: Condition;
+  onChange: (value: Condition) => void;
+};
 
-  constructor(props) {
-    super(props);
-    this.state = {
-      inputValue: '',
-      isOpen: false,
-    };
-  }
+type ConditionState = {
+  inputValue: string;
+  isOpen: boolean;
+};
+
+const initalState = {
+  inputValue: '',
+  isOpen: false,
+};
+
+export default class ConditionRow extends React.Component<
+  ConditionProps,
+  ConditionState
+> {
+  // This is the ref of the inner react-select component
+  private select: any;
+
+  state = initalState;
 
   focus() {
     this.select.focus();
   }
 
-  handleChange = option => {
+  handleChange = (option: ReactSelectOption) => {
     const external = getExternal(option.value, this.props.columns);
 
     if (isValidCondition(external, this.props.columns)) {
@@ -41,7 +48,9 @@ export default class Condition extends React.Component {
           inputValue: '',
           isOpen: false,
         },
-        this.props.onChange(external)
+        () => {
+          this.props.onChange(external);
+        }
       );
 
       return;
@@ -70,7 +79,7 @@ export default class Condition extends React.Component {
     return shouldDisplayValue ? [{label: currentValue, value: currentValue}] : [];
   }
 
-  getConditionsForColumn(colName) {
+  getConditionsForColumn(colName: string) {
     const column = this.props.columns.find(({name}) => name === colName);
     const colType = column ? column.type : 'string';
     const numberOnlyOperators = new Set(['>', '<', '>=', '<=']);
@@ -91,7 +100,7 @@ export default class Condition extends React.Component {
     });
   }
 
-  filterOptions = options => {
+  filterOptions = (options: ReactSelectOption[]) => {
     const input = this.state.inputValue;
 
     let optionList = options;
@@ -113,7 +122,7 @@ export default class Condition extends React.Component {
     }
 
     if (hasSelectedColumn && !hasSelectedOperator) {
-      const selectedColumn = external[0];
+      const selectedColumn = `${external[0]}`;
       optionList = this.getConditionsForColumn(selectedColumn).map(op => {
         const value = `${selectedColumn} ${op}`;
         return {
@@ -126,17 +135,19 @@ export default class Condition extends React.Component {
     return optionList.filter(({label}) => label.includes(input));
   };
 
-  isValidNewOption = ({label}) => {
+  isValidNewOption = ({label}: ReactSelectOption) => {
     label = ignoreCase(label);
     return isValidCondition(getExternal(label, this.props.columns), this.props.columns);
   };
 
-  inputRenderer = props => {
-    const onChange = evt => {
+  inputRenderer = (props: ConditionProps) => {
+    const onChange = (evt: any) => {
       if (evt.target.value === '') {
         // React select won't trigger an onChange event when a value is completely
         // cleared, so we'll force this before calling onChange
-        this.setState({inputValue: evt.target.value}, props.onChange(evt));
+        this.setState({inputValue: evt.target.value}, () => {
+          props.onChange(evt);
+        });
       } else {
         props.onChange(evt);
       }
@@ -153,7 +164,7 @@ export default class Condition extends React.Component {
     );
   };
 
-  valueComponent = props => {
+  valueComponent = (props: ConditionProps) => {
     if (this.state.inputValue) {
       return null;
     }
@@ -161,12 +172,12 @@ export default class Condition extends React.Component {
     return <Value {...props} />;
   };
 
-  shouldKeyDownEventCreateNewOption = keyCode => {
+  shouldKeyDownEventCreateNewOption = (keyCode: number) => {
     const createKeyCodes = new Set([13, 9]); // ENTER, TAB
     return createKeyCodes.has(keyCode);
   };
 
-  handleInputChange = value => {
+  handleInputChange = (value: string) => {
     this.setState({
       inputValue: ignoreCase(value),
     });
@@ -174,7 +185,7 @@ export default class Condition extends React.Component {
     return value;
   };
 
-  handleBlur = evt => {
+  handleBlur = (evt: any) => {
     const external = getExternal(evt.target.value, this.props.columns);
     const isValid = isValidCondition(external, this.props.columns);
     if (isValid) {
@@ -182,12 +193,14 @@ export default class Condition extends React.Component {
         {
           inputValue: '',
         },
-        this.props.onChange(external)
+        () => {
+          this.props.onChange(external);
+        }
       );
     }
   };
 
-  newOptionCreator = ({label, labelKey, valueKey}) => {
+  newOptionCreator = ({label, labelKey, valueKey}: any) => {
     label = ignoreCase(label);
     return {
       [valueKey]: label,
@@ -199,7 +212,7 @@ export default class Condition extends React.Component {
     return (
       <Box>
         <SelectControl
-          innerRef={ref => (this.select = ref)}
+          innerRef={(ref: any) => (this.select = ref)}
           value={getInternal(this.props.value)}
           placeholder={<PlaceholderText>{t('Add condition...')}</PlaceholderText>}
           options={this.getOptions()}
@@ -218,7 +231,7 @@ export default class Condition extends React.Component {
           onInputChange={this.handleInputChange}
           onBlur={this.handleBlur}
           creatable={true}
-          promptTextCreator={text => text}
+          promptTextCreator={(text: string) => text}
           shouldKeyDownEventCreateNewOption={this.shouldKeyDownEventCreateNewOption}
           disabled={this.props.disabled}
           newOptionCreator={this.newOptionCreator}

+ 12 - 14
src/sentry/static/sentry/app/views/organizationDiscover/conditions/index.jsx → src/sentry/static/sentry/app/views/organizationDiscover/conditions/index.tsx

@@ -1,32 +1,30 @@
 import React from 'react';
-import PropTypes from 'prop-types';
 
-import Link from 'app/components/links/link';
 import InlineSvg from 'app/components/inlineSvg';
 import {t} from 'app/locale';
+import Link from 'app/components/links/link';
 
-import Condition from './condition';
+import ConditionRow from './condition';
 import {PlaceholderText, SelectListItem, AddText, SidebarLabel} from '../styles';
+import {Condition, DiscoverBaseProps} from '../types';
 
-export default class Conditions extends React.Component {
-  static propTypes = {
-    value: PropTypes.arrayOf(PropTypes.array).isRequired,
-    onChange: PropTypes.func.isRequired,
-    columns: PropTypes.array.isRequired,
-    disabled: PropTypes.bool,
-  };
+type ConditionsProps = DiscoverBaseProps & {
+  value: Condition[];
+  onChange: (value: [any, any, any][]) => void;
+};
 
+export default class Conditions extends React.Component<ConditionsProps> {
   addRow() {
     this.props.onChange([...this.props.value, [null, null, null]]);
   }
 
-  removeRow(idx) {
+  removeRow(idx: number) {
     const conditions = this.props.value.slice();
     conditions.splice(idx, 1);
     this.props.onChange(conditions);
   }
 
-  handleChange(val, idx) {
+  handleChange(val: Condition, idx: number) {
     const conditions = this.props.value.slice();
 
     conditions[idx] = val;
@@ -52,9 +50,9 @@ export default class Conditions extends React.Component {
         )}
         {value.map((condition, idx) => (
           <SelectListItem key={`${idx}_${condition[2]}`}>
-            <Condition
+            <ConditionRow
               value={condition}
-              onChange={val => this.handleChange(val, idx)}
+              onChange={(val: Condition) => this.handleChange(val, idx)}
               columns={columns}
               disabled={disabled}
             />

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