Mirror of https://github.com/roostorg/osprey github.com/roostorg/osprey
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 193 lines 6.9 kB view raw
1import * as React from 'react'; 2import { AutoComplete, Input, Select } from 'antd'; 3 4import { SelectValue } from 'antd/lib/select'; 5import { OptionData } from 'rc-select/lib/interface/index'; 6 7import shallow from 'zustand/shallow'; 8 9import { validateQuery } from '../../actions/QueryActions'; 10import useApplicationConfigStore from '../../stores/ApplicationConfigStore'; 11import useErrorStore from '../../stores/ErrorStore'; 12import useQueryStore from '../../stores/QueryStore'; 13import { IntervalOptions, DefaultIntervals } from '../../types/QueryTypes'; 14import OspreyButton, { ButtonColors } from '../../uikit/OspreyButton'; 15import { TextSizes } from '../../uikit/Text'; 16import QueryIcon from '../../uikit/icons/QueryIcon'; 17import { filterAutoComplete, sortOptions } from '../../utils/AutoCompleteUtils'; 18import { getQueryDateRange, isEmptyDateRange, CUSTOM_RANGE_OPTION } from '../../utils/QueryUtils'; 19import IconHeader from '../common/IconHeader'; 20import QueryFilter from '../common/QueryFilter'; 21import OptionLabelWithInfo from '../common/OptionLabelWithInfo'; 22import QueryErrors from './QueryErrors'; 23 24import styles from './QueryInput.module.css'; 25 26const SelectOptions = [ 27 ...Object.entries(IntervalOptions).map(([option, { label }]) => ({ 28 value: option, 29 label: label, 30 })), 31 { value: CUSTOM_RANGE_OPTION, label: 'Custom Range' }, 32]; 33 34export interface QueryInputProps { 35 interval: DefaultIntervals; 36 dateRange: { start: string; end: string }; 37 onIntervalChange: (interval: DefaultIntervals) => void; 38} 39 40const QueryInput = ({ 41 interval: executedQueryInterval, 42 onIntervalChange: onExecutedQueryIntervalChange, 43 dateRange, 44}: QueryInputProps) => { 45 const [executedQuery, updateExecutedQuery] = useQueryStore( 46 (state) => [state.executedQuery, state.updateExecutedQuery], 47 shallow 48 ); 49 const [knownFeatureNames, knownActionNames, ruleInfoMapping] = useApplicationConfigStore( 50 (state) => [state.knownFeatureNames, state.knownActionNames, state.ruleInfoMapping], 51 shallow 52 ); 53 const [queryFilter, setQueryFilter] = React.useState(executedQuery.queryFilter); 54 const [searchOptions, setSearchOptions] = React.useState<OptionData[]>([]); 55 const [filteredOptions, setFilteredOptions] = React.useState<OptionData[]>([]); 56 57 // store the selected interval state separate from the current query's interval, so that changes 58 // to the interval can be made without losing WIP in the query input field 59 const [selectedInterval, setSelectedInterval] = React.useState(executedQueryInterval); 60 61 const [isLoading, setIsLoading] = React.useState(false); 62 63 const handleSelectChange = (value: DefaultIntervals) => { 64 // only update the active query's interval if the user has not made any changes to their query input 65 if (executedQuery.queryFilter === queryFilter) { 66 onExecutedQueryIntervalChange(value); 67 } 68 setSelectedInterval(value); 69 }; 70 71 React.useEffect(() => { 72 const options = [...knownFeatureNames, ...knownActionNames].map((option: string) => ({ 73 value: option, 74 label: <OptionLabelWithInfo option={option} optionInfoMapping={ruleInfoMapping} />, 75 })); 76 setSearchOptions(options); 77 setFilteredOptions(options); 78 }, [knownFeatureNames, knownActionNames]); 79 80 React.useEffect(() => { 81 setQueryFilter(executedQuery.queryFilter); 82 // if the user has selected a new query to execute from the saved queries list, make sure that 83 // we update the selected interval to reflect the correct one 84 if (executedQuery.interval !== selectedInterval) { 85 setSelectedInterval(executedQuery.interval); 86 } 87 }, [executedQuery, selectedInterval]); 88 89 const handleSelectAutoComplete = (value: SelectValue) => { 90 let insertValue = String(value); 91 92 if (knownActionNames.has(insertValue)) { 93 insertValue = `"${insertValue}"`; 94 } 95 96 const textAreaValueArr = queryFilter.split(' '); 97 textAreaValueArr[textAreaValueArr.length - 1] = insertValue; 98 const newTextAreaValue = textAreaValueArr.join(' '); 99 100 setQueryFilter(newTextAreaValue); 101 }; 102 103 const clearErrors = useErrorStore((state) => state.clearErrors); 104 105 const handleSubmitQuery = async () => { 106 setIsLoading(true); 107 clearErrors(); 108 const isValid = queryFilter === '' ? true : await validateQuery(queryFilter); 109 110 if (isValid) { 111 const { start, end } = getQueryDateRange(selectedInterval, dateRange); 112 updateExecutedQuery({ start, end, queryFilter, interval: selectedInterval }); 113 } 114 115 setIsLoading(false); 116 }; 117 118 const handleKeyPress = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { 119 const ctrlOrMetaKey = e.ctrlKey || e.metaKey; 120 if (ctrlOrMetaKey && e.key === 'Enter') { 121 handleSubmitQuery(); 122 } 123 }; 124 125 const handleTextAreaChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { 126 setQueryFilter(e.target.value); 127 handleSearch(e.target.value); 128 }; 129 130 const handleSearch = (inputValue: string) => { 131 const currentWord = String(inputValue.split(' ').slice(-1)); 132 const filtered = searchOptions 133 .filter((option) => filterAutoComplete(inputValue, option)) 134 .sort(sortOptions(currentWord)); 135 136 setFilteredOptions(filtered); 137 }; 138 139 return ( 140 <div className={styles.queryInput}> 141 <IconHeader size={TextSizes.H4} icon={<QueryIcon />} title="Query" /> 142 <div className={styles.textareaWrapper}> 143 <AutoComplete 144 className={styles.autoComplete} 145 value={queryFilter} 146 options={filteredOptions} 147 onSelect={handleSelectAutoComplete} 148 > 149 <Input.TextArea 150 className={styles.__invalid_textArea} 151 onChange={handleTextAreaChange} 152 onPressEnter={handleKeyPress} 153 autoSize={{ minRows: 2 }} 154 spellCheck="false" 155 /> 156 </AutoComplete> 157 <div className={styles.overlay}> 158 <QueryFilter 159 queryFilter={queryFilter} 160 className={styles.highlightedWrap} 161 codeClassName={styles.displayArea} 162 /> 163 </div> 164 </div> 165 <div className={styles.intervalSelect}> 166 <Select 167 onChange={handleSelectChange} 168 placeholder="Select date range" 169 style={{ width: 180 }} 170 value={selectedInterval ?? undefined} 171 > 172 {SelectOptions.map((option) => ( 173 <Select.Option value={option.value} key={option.value}> 174 {option.label} 175 </Select.Option> 176 ))} 177 </Select> 178 </div> 179 <OspreyButton 180 disabled={selectedInterval === CUSTOM_RANGE_OPTION && isEmptyDateRange(dateRange.start, dateRange.end)} 181 color={ButtonColors.DARK_BLUE} 182 className={styles.submitButton} 183 onClick={handleSubmitQuery} 184 loading={isLoading} 185 > 186 Submit Query 187 </OspreyButton> 188 <QueryErrors /> 189 </div> 190 ); 191}; 192 193export default QueryInput;