mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user