Mirror of https://github.com/roostorg/osprey
github.com/roostorg/osprey
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;