Graphite: Migrate to React (part 2B: migrate FunctionEditor) (#37070)

* Add UMLs

* Add rendered diagrams

* Move QueryCtrl to flux

* Remove redundant param in the reducer

* Use named imports for lodash and fix typing for GraphiteTagOperator

* Add missing async/await

* Extract providers to a separate file

* Clean up async await

* Rename controller functions back to main

* Simplify creating actions

* Re-order controller functions

* Separate helpers from actions

* Rename vars

* Simplify helpers

* Move controller methods to state reducers

* Remove docs (they are added in design doc)

* Move actions.ts to state folder

* Add docs

* Add old methods stubs for easier review

* Check how state dependencies will be mapped

* Rename state to store

* Rename state to store

* Rewrite spec tests for Graphite Query Controller

* Update docs

* Update docs

* Add GraphiteTextEditor

* Add play button

* Add AddGraphiteFunction

* Use Segment to simplify AddGraphiteFunction

* Memoize function defs

* Fix useCallback deps

* Update public/app/plugins/datasource/graphite/state/helpers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/helpers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/helpers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Update public/app/plugins/datasource/graphite/state/providers.ts

Co-authored-by: Giordano Ricci <me@giordanoricci.com>

* Add more type definitions

* Remove submitOnClickAwayOption

This behavior is actually needed to remove parameters in functions

* Load function definitions before parsing the target on initial load

* Add button padding

* Fix loading function definitions

* Change targetChanged to updateQuery to avoid mutating state directly

It's also needed for extra refresh/runQuery execution as handleTargetChanged doesn't handle changing the raw query

* Fix updating query after adding a function

* Simplify updating function params

* Migrate function editor to react

* Simplify setting Segment Select min width

* Remove unnecessary changes to SegmentInput

* Extract view logic to a helper and update types definitions

* Clean up types

* Update FuncDef types and add tests

* Show red border for unknown functions

* Autofocus on new params

* Extract params mapping to a helper

* Split code between params and function editor

* Focus on the first param when a function is added even if it's an optional argument

* Add function editor tests

* Remove todo marker

* Fix adding new functions

* Allow empty value in selects for removing function params

* Add placeholders and fix styling

* Add more docs

* Rename .tsx -> .ts

* Update types

* Use ?? instead of || + add test for mapping options

* Use const (let is not needed here)

* Revert test name change

* Allow removing only optional params and mark additional params as optional (only the first one is required)

* Use theme.typography.bodySmall.fontSize

Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
Piotr Jamróz
2021-07-27 13:10:39 +02:00
committed by GitHub
parent 442a6677fc
commit 8d7e22e1bb
23 changed files with 581 additions and 349 deletions

View File

@@ -11,6 +11,7 @@ export interface SegmentSyncProps<T> extends SegmentProps<T>, Omit<HTMLProps<HTM
value?: T | SelectableValue<T>;
onChange: (item: SelectableValue<T>) => void;
options: Array<SelectableValue<T>>;
inputMinWidth?: number;
}
export function Segment<T>({
@@ -20,12 +21,16 @@ export function Segment<T>({
Component,
className,
allowCustomValue,
allowEmptyValue,
placeholder,
disabled,
inputMinWidth,
inputPlaceholder,
onExpandedChange,
autofocus = false,
...rest
}: React.PropsWithChildren<SegmentSyncProps<T>>) {
const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(false);
const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(autofocus, onExpandedChange);
const width = inputMinWidth ? Math.max(inputMinWidth, labelWidth) : labelWidth;
const styles = useStyles(getSegmentStyles);
@@ -59,10 +64,12 @@ export function Segment<T>({
<SegmentSelect
{...rest}
value={value && !isObject(value) ? { value } : value}
placeholder={inputPlaceholder}
options={options}
width={width}
onClickOutside={() => setExpanded(false)}
allowCustomValue={allowCustomValue}
allowEmptyValue={allowEmptyValue}
onChange={(item) => {
setExpanded(false);
onChange(item);

View File

@@ -15,6 +15,7 @@ export interface SegmentAsyncProps<T> extends SegmentProps<T>, Omit<HTMLProps<HT
loadOptions: (query?: string) => Promise<Array<SelectableValue<T>>>;
onChange: (item: SelectableValue<T>) => void;
noOptionMessageHandler?: (state: AsyncState<Array<SelectableValue<T>>>) => string;
inputMinWidth?: number;
}
export function SegmentAsync<T>({
@@ -24,14 +25,18 @@ export function SegmentAsync<T>({
Component,
className,
allowCustomValue,
allowEmptyValue,
disabled,
placeholder,
inputMinWidth,
inputPlaceholder,
autofocus = false,
onExpandedChange,
noOptionMessageHandler = mapStateToNoOptionsMessage,
...rest
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
const [state, fetchOptions] = useAsyncFn(loadOptions, [loadOptions]);
const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(false);
const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(autofocus, onExpandedChange);
const width = inputMinWidth ? Math.max(inputMinWidth, labelWidth) : labelWidth;
const styles = useStyles(getSegmentStyles);
@@ -66,10 +71,12 @@ export function SegmentAsync<T>({
<SegmentSelect
{...rest}
value={value && !isObject(value) ? { value } : value}
placeholder={inputPlaceholder}
options={state.value ?? []}
width={width}
noOptionsMessage={noOptionMessageHandler(state)}
allowCustomValue={allowCustomValue}
allowEmptyValue={allowEmptyValue}
onClickOutside={() => {
setExpanded(false);
}}

View File

@@ -10,7 +10,6 @@ import { useStyles } from '../../themes';
export interface SegmentInputProps<T> extends SegmentProps<T>, Omit<HTMLProps<HTMLInputElement>, 'value' | 'onChange'> {
value: string | number;
onChange: (text: string | number) => void;
autofocus?: boolean;
}
const FONT_SIZE = 14;
@@ -21,14 +20,16 @@ export function SegmentInput<T>({
Component,
className,
placeholder,
inputPlaceholder,
disabled,
autofocus = false,
onExpandedChange,
...rest
}: React.PropsWithChildren<SegmentInputProps<T>>) {
const ref = useRef<HTMLInputElement>(null);
const [value, setValue] = useState<number | string>(initialValue);
const [inputWidth, setInputWidth] = useState<number>(measureText((initialValue || '').toString(), FONT_SIZE).width);
const [Label, , expanded, setExpanded] = useExpandableLabel(autofocus);
const [Label, , expanded, setExpanded] = useExpandableLabel(autofocus, onExpandedChange);
const styles = useStyles(getSegmentStyles);
useClickAway(ref, () => {
@@ -71,6 +72,7 @@ export function SegmentInput<T>({
autoFocus
className={cx(`gf-form gf-form-input`, inputWidthStyle)}
value={value}
placeholder={inputPlaceholder}
onChange={(item) => {
const { width } = measureText(item.target.value, FONT_SIZE);
setInputWidth(width);

View File

@@ -15,17 +15,25 @@ export interface Props<T> extends Omit<HTMLProps<HTMLDivElement>, 'value' | 'onC
width: number;
noOptionsMessage?: string;
allowCustomValue?: boolean;
/**
* If true, empty value will be passed to onChange callback otherwise using empty value
* will work as canceling and using the previous value
*/
allowEmptyValue?: boolean;
placeholder?: string;
}
/** @internal */
export function SegmentSelect<T>({
value,
placeholder = '',
options = [],
onChange,
onClickOutside,
width: widthPixels,
noOptionsMessage = '',
allowCustomValue = false,
allowEmptyValue = false,
...rest
}: React.PropsWithChildren<Props<T>>) {
const ref = useRef<HTMLDivElement>(null);
@@ -38,7 +46,7 @@ export function SegmentSelect<T>({
<Select
width={width}
noOptionsMessage={noOptionsMessage}
placeholder=""
placeholder={placeholder}
autoFocus={true}
isOpen={true}
onChange={onChange}
@@ -53,7 +61,7 @@ export function SegmentSelect<T>({
// https://github.com/JedWatson/react-select/issues/188#issuecomment-279240292
// Unfortunately there's no other way of retrieving the value (not yet) created new option
const input = ref.current.querySelector('input[id^="react-select-"]') as HTMLInputElement;
if (input && input.value) {
if (input && (input.value || allowEmptyValue)) {
onChange({ value: input.value as any, label: input.value });
} else {
onClickOutside();

View File

@@ -6,5 +6,8 @@ export interface SegmentProps<T> {
allowCustomValue?: boolean;
placeholder?: string;
disabled?: boolean;
inputMinWidth?: number;
onExpandedChange?: (expanded: boolean) => void;
autofocus?: boolean;
allowEmptyValue?: boolean;
inputPlaceholder?: string;
}

View File

@@ -7,12 +7,20 @@ interface LabelProps {
}
export const useExpandableLabel = (
initialExpanded: boolean
initialExpanded: boolean,
onExpandedChange?: (expanded: boolean) => void
): [React.ComponentType<LabelProps>, number, boolean, (expanded: boolean) => void] => {
const ref = useRef<HTMLDivElement>(null);
const [expanded, setExpanded] = useState<boolean>(initialExpanded);
const [width, setWidth] = useState(0);
const setExpandedWrapper = (expanded: boolean) => {
setExpanded(expanded);
if (onExpandedChange) {
onExpandedChange(expanded);
}
};
const Label: React.FC<LabelProps> = ({ Component, onClick, disabled }) => (
<div
ref={ref}
@@ -20,7 +28,7 @@ export const useExpandableLabel = (
disabled
? undefined
: () => {
setExpanded(true);
setExpandedWrapper(true);
if (ref && ref.current) {
setWidth(ref.current.clientWidth * 1.25);
}
@@ -34,5 +42,5 @@ export const useExpandableLabel = (
</div>
);
return [Label, width, expanded, setExpanded];
return [Label, width, expanded, setExpandedWrapper];
};