Преглед изворни кода

Fixes bug #12972 with a new type of input that escapes and unescapes special regexp characters

Hugo Häggmark пре 6 година
родитељ
комит
5388541fd7

+ 5 - 5
public/app/core/components/OrgActionBar/OrgActionBar.tsx

@@ -1,5 +1,6 @@
 import React, { PureComponent } from 'react';
 import LayoutSelector, { LayoutMode } from '../LayoutSelector/LayoutSelector';
+import { RegExpSafeInput } from '../RegExpSafeInput/RegExpSafeInput';
 
 export interface Props {
   searchQuery: string;
@@ -23,12 +24,11 @@ export default class OrgActionBar extends PureComponent<Props> {
       <div className="page-action-bar">
         <div className="gf-form gf-form--grow">
           <label className="gf-form--has-input-icon">
-            <input
-              type="text"
-              className="gf-form-input width-20"
+            <RegExpSafeInput
+              className={'gf-form-input width-20'}
               value={searchQuery}
-              onChange={event => setSearchQuery(event.target.value)}
-              placeholder="Filter by name or type"
+              onChange={setSearchQuery}
+              placeholder={'Filter by name or type'}
             />
             <i className="gf-form-input-icon fa fa-search" />
           </label>

+ 2 - 3
public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap

@@ -10,11 +10,10 @@ exports[`Render should render component 1`] = `
     <label
       className="gf-form--has-input-icon"
     >
-      <input
+      <ForwardRef
         className="gf-form-input width-20"
-        onChange={[Function]}
+        onChange={[MockFunction]}
         placeholder="Filter by name or type"
-        type="text"
         value=""
       />
       <i

+ 48 - 0
public/app/core/components/RegExpSafeInput/RegExpSafeInput.tsx

@@ -0,0 +1,48 @@
+import React, { ChangeEvent, forwardRef } from 'react';
+
+const specialChars = ['(', '[', '{', '}', ']', ')', '|', '*', '+', '-', '.', '?', '<', '>', '#', '&', '^', '$'];
+
+export const escapeStringForRegex = (event: ChangeEvent<HTMLInputElement>) => {
+  const value = event.target.value;
+  if (!value) {
+    return value;
+  }
+
+  const newValue = specialChars.reduce(
+    (escaped, currentChar) => escaped.replace(currentChar, '\\' + currentChar),
+    value
+  );
+
+  return newValue;
+};
+
+export const unEscapeStringFromRegex = (value: string) => {
+  if (!value) {
+    return value;
+  }
+
+  const newValue = specialChars.reduce(
+    (escaped, currentChar) => escaped.replace('\\' + currentChar, currentChar),
+    value
+  );
+
+  return newValue;
+};
+
+export interface Props {
+  value: string | undefined;
+  placeholder?: string;
+  className?: string;
+  onChange: (value: string) => void;
+}
+
+export const RegExpSafeInput = forwardRef<HTMLInputElement, Props>((props, ref) => (
+  <input
+    ref={ref}
+    type="text"
+    className={props.className}
+    value={unEscapeStringFromRegex(props.value)}
+    onChange={event => props.onChange(escapeStringForRegex(event))}
+    placeholder={props.placeholder ? props.placeholder : null}
+  />
+));

+ 2 - 3
public/app/features/alerting/AlertRuleList.test.tsx

@@ -18,7 +18,7 @@ const setup = (propOverrides?: object) => {
     togglePauseAlertRule: jest.fn(),
     stateFilter: '',
     search: '',
-    isLoading: false
+    isLoading: false,
   };
 
   Object.assign(props, propOverrides);
@@ -147,9 +147,8 @@ describe('Functions', () => {
   describe('Search query change', () => {
     it('should set search query', () => {
       const { instance } = setup();
-      const mockEvent = { target: { value: 'dashboard' } } as React.ChangeEvent<HTMLInputElement>;
 
-      instance.onSearchQueryChange(mockEvent);
+      instance.onSearchQueryChange('dashboard');
 
       expect(instance.props.setSearchQuery).toHaveBeenCalledWith('dashboard');
     });

+ 5 - 6
public/app/features/alerting/AlertRuleList.tsx

@@ -9,6 +9,7 @@ import { getNavModel } from 'app/core/selectors/navModel';
 import { NavModel, StoreState, AlertRule } from 'app/types';
 import { getAlertRulesAsync, setSearchQuery, togglePauseAlertRule } from './state/actions';
 import { getAlertRuleItems, getSearchQuery } from './state/selectors';
+import { RegExpSafeInput } from 'app/core/components/RegExpSafeInput/RegExpSafeInput';
 
 export interface Props {
   navModel: NavModel;
@@ -69,8 +70,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
     });
   };
 
-  onSearchQueryChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
-    const { value } = evt.target;
+  onSearchQueryChange = (value: string) => {
     this.props.setSearchQuery(value);
   };
 
@@ -78,7 +78,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
     this.props.togglePauseAlertRule(rule.id, { paused: rule.state !== 'paused' });
   };
 
-  alertStateFilterOption = ({ text, value }: { text: string; value: string; }) => {
+  alertStateFilterOption = ({ text, value }: { text: string; value: string }) => {
     return (
       <option key={value} value={value}>
         {text}
@@ -95,8 +95,7 @@ export class AlertRuleList extends PureComponent<Props, any> {
           <div className="page-action-bar">
             <div className="gf-form gf-form--grow">
               <label className="gf-form--has-input-icon gf-form--grow">
-                <input
-                  type="text"
+                <RegExpSafeInput
                   className="gf-form-input"
                   placeholder="Search alerts"
                   value={search}
@@ -142,7 +141,7 @@ const mapStateToProps = (state: StoreState) => ({
   alertRules: getAlertRuleItems(state.alertRules),
   stateFilter: state.location.query.state,
   search: getSearchQuery(state.alertRules),
-  isLoading: state.alertRules.isLoading
+  isLoading: state.alertRules.isLoading,
 });
 
 const mapDispatchToProps = {

+ 2 - 4
public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap

@@ -16,11 +16,10 @@ exports[`Render should render alert rules 1`] = `
         <label
           className="gf-form--has-input-icon gf-form--grow"
         >
-          <input
+          <ForwardRef
             className="gf-form-input"
             onChange={[Function]}
             placeholder="Search alerts"
-            type="text"
             value=""
           />
           <i
@@ -170,11 +169,10 @@ exports[`Render should render component 1`] = `
         <label
           className="gf-form--has-input-icon gf-form--grow"
         >
-          <input
+          <ForwardRef
             className="gf-form-input"
             onChange={[Function]}
             placeholder="Search alerts"
-            type="text"
             value=""
           />
           <i

+ 4 - 5
public/app/features/api-keys/ApiKeysPage.test.tsx

@@ -8,11 +8,11 @@ const setup = (propOverrides?: object) => {
   const props: Props = {
     navModel: {
       main: {
-        text: 'Configuration'
+        text: 'Configuration',
       },
       node: {
-        text: 'Api Keys'
-      }
+        text: 'Api Keys',
+      },
     } as NavModel,
     apiKeys: [] as ApiKey[],
     searchQuery: '',
@@ -78,9 +78,8 @@ describe('Functions', () => {
   describe('on search query change', () => {
     it('should call setSearchQuery', () => {
       const { instance } = setup();
-      const mockEvent = { target: { value: 'test' } };
 
-      instance.onSearchQueryChange(mockEvent);
+      instance.onSearchQueryChange('test');
 
       expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
     });

+ 5 - 11
public/app/features/api-keys/ApiKeysPage.tsx

@@ -13,6 +13,7 @@ import config from 'app/core/config';
 import appEvents from 'app/core/app_events';
 import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
 import { DeleteButton } from '@grafana/ui';
+import { RegExpSafeInput } from 'app/core/components/RegExpSafeInput/RegExpSafeInput';
 
 export interface Props {
   navModel: NavModel;
@@ -59,8 +60,8 @@ export class ApiKeysPage extends PureComponent<Props, any> {
     this.props.deleteApiKey(key.id);
   }
 
-  onSearchQueryChange = evt => {
-    this.props.setSearchQuery(evt.target.value);
+  onSearchQueryChange = (value: string) => {
+    this.props.setSearchQuery(value);
   };
 
   onToggleAdding = () => {
@@ -187,8 +188,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
         <div className="page-action-bar">
           <div className="gf-form gf-form--grow">
             <label className="gf-form--has-input-icon gf-form--grow">
-              <input
-                type="text"
+              <RegExpSafeInput
                 className="gf-form-input"
                 placeholder="Search keys"
                 value={searchQuery}
@@ -241,13 +241,7 @@ export class ApiKeysPage extends PureComponent<Props, any> {
     return (
       <Page navModel={navModel}>
         <Page.Contents isLoading={!hasFetched}>
-          {hasFetched && (
-            apiKeysCount > 0 ? (
-              this.renderApiKeyList()
-            ) : (
-              this.renderEmptyList()
-            )
-          )}
+          {hasFetched && (apiKeysCount > 0 ? this.renderApiKeyList() : this.renderEmptyList())}
         </Page.Contents>
       </Page>
     );

+ 3 - 4
public/app/features/dashboard/panel_editor/VisualizationTab.tsx

@@ -17,6 +17,7 @@ import { FadeIn } from 'app/core/components/Animations/FadeIn';
 import { PanelModel } from '../state/PanelModel';
 import { DashboardModel } from '../state/DashboardModel';
 import { PanelPlugin } from 'app/types/plugins';
+import { RegExpSafeInput } from 'app/core/components/RegExpSafeInput/RegExpSafeInput';
 
 interface Props {
   panel: PanelModel;
@@ -170,8 +171,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
     this.setState({ isVizPickerOpen: false });
   };
 
-  onSearchQueryChange = evt => {
-    const value = evt.target.value;
+  onSearchQueryChange = (value: string) => {
     this.setState({
       searchQuery: value,
     });
@@ -185,8 +185,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
       return (
         <>
           <label className="gf-form--has-input-icon">
-            <input
-              type="text"
+            <RegExpSafeInput
               className="gf-form-input width-13"
               placeholder=""
               onChange={this.onSearchQueryChange}

+ 5 - 5
public/app/features/datasources/NewDataSourcePage.tsx

@@ -6,6 +6,7 @@ import { NavModel, Plugin, StoreState } from 'app/types';
 import { addDataSource, loadDataSourceTypes, setDataSourceTypeSearchQuery } from './state/actions';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { getDataSourceTypes } from './state/selectors';
+import { RegExpSafeInput } from 'app/core/components/RegExpSafeInput/RegExpSafeInput';
 
 export interface Props {
   navModel: NavModel;
@@ -26,8 +27,8 @@ class NewDataSourcePage extends PureComponent<Props> {
     this.props.addDataSource(plugin);
   };
 
-  onSearchQueryChange = (event: React.ChangeEvent<HTMLInputElement>) => {
-    this.props.setDataSourceTypeSearchQuery(event.target.value);
+  onSearchQueryChange = (value: string) => {
+    this.props.setDataSourceTypeSearchQuery(value);
   };
 
   render() {
@@ -39,8 +40,7 @@ class NewDataSourcePage extends PureComponent<Props> {
             <h2 className="add-data-source-header">Choose data source type</h2>
             <div className="add-data-source-search">
               <label className="gf-form--has-input-icon">
-                <input
-                  type="text"
+                <RegExpSafeInput
                   className="gf-form-input width-20"
                   value={dataSourceTypeSearchQuery}
                   onChange={this.onSearchQueryChange}
@@ -74,7 +74,7 @@ function mapStateToProps(state: StoreState) {
   return {
     navModel: getNavModel(state.navIndex, 'datasources'),
     dataSourceTypes: getDataSourceTypes(state.dataSources),
-    isLoading: state.dataSources.isLoadingDataSources
+    isLoading: state.dataSources.isLoadingDataSources,
   };
 }
 

+ 4 - 5
public/app/features/teams/TeamList.test.tsx

@@ -8,11 +8,11 @@ const setup = (propOverrides?: object) => {
   const props: Props = {
     navModel: {
       main: {
-        text: 'Configuration'
+        text: 'Configuration',
       },
       node: {
-        text: 'Team List'
-      }
+        text: 'Team List',
+      },
     } as NavModel,
     teams: [] as Team[],
     loadTeams: jest.fn(),
@@ -74,9 +74,8 @@ describe('Functions', () => {
   describe('on search query change', () => {
     it('should call setSearchQuery', () => {
       const { instance } = setup();
-      const mockEvent = { target: { value: 'test' } };
 
-      instance.onSearchQueryChange(mockEvent);
+      instance.onSearchQueryChange('test');
 
       expect(instance.props.setSearchQuery).toHaveBeenCalledWith('test');
     });

+ 5 - 7
public/app/features/teams/TeamList.tsx

@@ -8,6 +8,7 @@ import { NavModel, Team } from 'app/types';
 import { loadTeams, deleteTeam, setSearchQuery } from './state/actions';
 import { getSearchQuery, getTeams, getTeamsCount } from './state/selectors';
 import { getNavModel } from 'app/core/selectors/navModel';
+import { RegExpSafeInput } from 'app/core/components/RegExpSafeInput/RegExpSafeInput';
 
 export interface Props {
   navModel: NavModel;
@@ -33,8 +34,8 @@ export class TeamList extends PureComponent<Props, any> {
     this.props.deleteTeam(team.id);
   };
 
-  onSearchQueryChange = event => {
-    this.props.setSearchQuery(event.target.value);
+  onSearchQueryChange = (value: string) => {
+    this.props.setSearchQuery(value);
   };
 
   renderTeam(team: Team) {
@@ -90,8 +91,7 @@ export class TeamList extends PureComponent<Props, any> {
         <div className="page-action-bar">
           <div className="gf-form gf-form--grow">
             <label className="gf-form--has-input-icon gf-form--grow">
-              <input
-                type="text"
+              <RegExpSafeInput
                 className="gf-form-input"
                 placeholder="Search teams"
                 value={searchQuery}
@@ -141,9 +141,7 @@ export class TeamList extends PureComponent<Props, any> {
 
     return (
       <Page navModel={navModel}>
-        <Page.Contents isLoading={!hasFetched}>
-          {hasFetched && this.renderList()}
-        </Page.Contents>
+        <Page.Contents isLoading={!hasFetched}>{hasFetched && this.renderList()}</Page.Contents>
       </Page>
     );
   }

+ 1 - 2
public/app/features/teams/TeamMembers.test.tsx

@@ -55,9 +55,8 @@ describe('Functions', () => {
   describe('on search member query change', () => {
     it('it should call setSearchMemberQuery', () => {
       const { instance } = setup();
-      const mockEvent = { target: { value: 'member' } };
 
-      instance.onSearchQueryChange(mockEvent);
+      instance.onSearchQueryChange('member');
 
       expect(instance.props.setSearchMemberQuery).toHaveBeenCalledWith('member');
     });

+ 4 - 4
public/app/features/teams/TeamMembers.tsx

@@ -7,6 +7,7 @@ import { TagBadge } from 'app/core/components/TagFilter/TagBadge';
 import { TeamMember, User } from 'app/types';
 import { loadTeamMembers, addTeamMember, removeTeamMember, setSearchMemberQuery } from './state/actions';
 import { getSearchMemberQuery, getTeamMembers } from './state/selectors';
+import { RegExpSafeInput } from 'app/core/components/RegExpSafeInput/RegExpSafeInput';
 
 export interface Props {
   members: TeamMember[];
@@ -33,8 +34,8 @@ export class TeamMembers extends PureComponent<Props, State> {
     this.props.loadTeamMembers();
   }
 
-  onSearchQueryChange = event => {
-    this.props.setSearchMemberQuery(event.target.value);
+  onSearchQueryChange = (value: string) => {
+    this.props.setSearchMemberQuery(value);
   };
 
   onRemoveMember(member: TeamMember) {
@@ -90,8 +91,7 @@ export class TeamMembers extends PureComponent<Props, State> {
         <div className="page-action-bar">
           <div className="gf-form gf-form--grow">
             <label className="gf-form--has-input-icon gf-form--grow">
-              <input
-                type="text"
+              <RegExpSafeInput
                 className="gf-form-input"
                 placeholder="Search members"
                 value={searchMemberQuery}

+ 3 - 6
public/app/features/teams/__snapshots__/TeamMembers.test.tsx.snap

@@ -11,11 +11,10 @@ exports[`Render should render component 1`] = `
       <label
         className="gf-form--has-input-icon gf-form--grow"
       >
-        <input
+        <ForwardRef
           className="gf-form-input"
           onChange={[Function]}
           placeholder="Search members"
-          type="text"
           value=""
         />
         <i
@@ -105,11 +104,10 @@ exports[`Render should render team members 1`] = `
       <label
         className="gf-form--has-input-icon gf-form--grow"
       >
-        <input
+        <ForwardRef
           className="gf-form-input"
           onChange={[Function]}
           placeholder="Search members"
-          type="text"
           value=""
         />
         <i
@@ -325,11 +323,10 @@ exports[`Render should render team members when sync enabled 1`] = `
       <label
         className="gf-form--has-input-icon gf-form--grow"
       >
-        <input
+        <ForwardRef
           className="gf-form-input"
           onChange={[Function]}
           placeholder="Search members"
-          type="text"
           value=""
         />
         <i

+ 3 - 3
public/app/features/users/UsersActionBar.tsx

@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
 import classNames from 'classnames';
 import { setUsersSearchQuery } from './state/actions';
 import { getInviteesCount, getUsersSearchQuery } from './state/selectors';
+import { RegExpSafeInput } from 'app/core/components/RegExpSafeInput/RegExpSafeInput';
 
 export interface Props {
   searchQuery: string;
@@ -44,11 +45,10 @@ export class UsersActionBar extends PureComponent<Props> {
       <div className="page-action-bar">
         <div className="gf-form gf-form--grow">
           <label className="gf-form--has-input-icon">
-            <input
-              type="text"
+            <RegExpSafeInput
               className="gf-form-input width-20"
               value={searchQuery}
-              onChange={event => setUsersSearchQuery(event.target.value)}
+              onChange={setUsersSearchQuery}
               placeholder="Filter by name or type"
             />
             <i className="gf-form-input-icon fa fa-search" />

+ 8 - 12
public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap

@@ -10,11 +10,10 @@ exports[`Render should render component 1`] = `
     <label
       className="gf-form--has-input-icon"
     >
-      <input
+      <ForwardRef
         className="gf-form-input width-20"
-        onChange={[Function]}
+        onChange={[MockFunction]}
         placeholder="Filter by name or type"
-        type="text"
         value=""
       />
       <i
@@ -38,11 +37,10 @@ exports[`Render should render pending invites button 1`] = `
     <label
       className="gf-form--has-input-icon"
     >
-      <input
+      <ForwardRef
         className="gf-form-input width-20"
-        onChange={[Function]}
+        onChange={[MockFunction]}
         placeholder="Filter by name or type"
-        type="text"
         value=""
       />
       <i
@@ -90,11 +88,10 @@ exports[`Render should show external user management button 1`] = `
     <label
       className="gf-form--has-input-icon"
     >
-      <input
+      <ForwardRef
         className="gf-form-input width-20"
-        onChange={[Function]}
+        onChange={[MockFunction]}
         placeholder="Filter by name or type"
-        type="text"
         value=""
       />
       <i
@@ -128,11 +125,10 @@ exports[`Render should show invite button 1`] = `
     <label
       className="gf-form--has-input-icon"
     >
-      <input
+      <ForwardRef
         className="gf-form-input width-20"
-        onChange={[Function]}
+        onChange={[MockFunction]}
         placeholder="Filter by name or type"
-        type="text"
         value=""
       />
       <i