mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Graphite: Migrate to React (part 4 & 5: group all components) (#37590)
* 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 * Create basic implementation for metrics and tags * Post merge fixes These files are not .ts * Remove mapping to Angular dropdowns * Simplify mapping tag names, values and operators * Simplify mapping metrics * Fix removing tags and autocomplete * Simplify debouncing providers * Ensure options are loaded twice and segment is opened * Remove focusing new segments logic (not supported by React's segment) * Clean up * Move debouncing to components * Simplify mapping to selectable options * Add docs * Group all components * Remove unused controller methods * Create Dispatch context * Group Series and Tags Sections * Create Functions section * Create Section component * use getStyles * remove redundant async/await * Remove * remove redundant async/await * Remove console.log and silent test console output * Do not display the name of the selected dropdown option * Move Section to grafana-ui * Update storybook * Simplify SectionLabel * Fix Influx tests * Fix API Extractor warnings * Fix API Extractor warnings * Do not show hidden functions * Use block docs for better doc generation * Handle undefined values provided for autocomplete * Section -> SegmentSection * Simplify section styling * Remove redundant div * Simplify SegmentSection component * Use theme.spacing * Use empty label instead of a single space label Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
parent
f8d7726187
commit
d93d989a5a
@ -24,6 +24,7 @@ const getStyles = (theme: GrafanaTheme) => {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
align-content: flex-start;
|
align-content: flex-start;
|
||||||
|
row-gap: ${theme.spacing.xs};
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { Segment, Icon } from '@grafana/ui';
|
import { Segment, Icon, SegmentSection } from '@grafana/ui';
|
||||||
|
|
||||||
const AddButton = (
|
const AddButton = (
|
||||||
<a className="gf-form-label query-part">
|
<a className="gf-form-label query-part">
|
||||||
@ -17,13 +17,10 @@ const groupedOptions = [
|
|||||||
|
|
||||||
const SegmentFrame = ({ options, children }: any) => (
|
const SegmentFrame = ({ options, children }: any) => (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline">
|
<SegmentSection label="Segment Name">
|
||||||
<div className="gf-form">
|
|
||||||
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
|
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
<Segment Component={AddButton} onChange={({ value }) => action('New value added')(value)} options={options} />
|
<Segment Component={AddButton} onChange={({ value }) => action('New value added')(value)} options={options} />
|
||||||
</div>
|
</SegmentSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ import React, { useState } from 'react';
|
|||||||
import { AsyncState } from 'react-use/lib/useAsync';
|
import { AsyncState } from 'react-use/lib/useAsync';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { SegmentAsync, Icon } from '@grafana/ui';
|
import { SegmentAsync, Icon, SegmentSection } from '@grafana/ui';
|
||||||
|
|
||||||
const AddButton = (
|
const AddButton = (
|
||||||
<a className="gf-form-label query-part">
|
<a className="gf-form-label query-part">
|
||||||
@ -21,17 +21,14 @@ const loadOptionsErr = (): Promise<Array<SelectableValue<string>>> =>
|
|||||||
|
|
||||||
const SegmentFrame = ({ loadOptions, children }: any) => (
|
const SegmentFrame = ({ loadOptions, children }: any) => (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline">
|
<SegmentSection label="Segment Name">
|
||||||
<div className="gf-form">
|
|
||||||
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
|
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
<SegmentAsync
|
<SegmentAsync
|
||||||
Component={AddButton}
|
Component={AddButton}
|
||||||
onChange={(value) => action('New value added')(value)}
|
onChange={(value) => action('New value added')(value)}
|
||||||
loadOptions={() => loadOptions(options)}
|
loadOptions={() => loadOptions(options)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</SegmentSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -1,15 +1,10 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { SegmentInput, Icon } from '@grafana/ui';
|
import { SegmentInput, Icon, SegmentSection } from '@grafana/ui';
|
||||||
|
|
||||||
const SegmentFrame = ({ children }: any) => (
|
const SegmentFrame = ({ children }: any) => (
|
||||||
<>
|
<>
|
||||||
<div className="gf-form-inline">
|
<SegmentSection label="Segment Name">{children}</SegmentSection>
|
||||||
<div className="gf-form">
|
|
||||||
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
|
|
||||||
</div>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { useStyles2 } from '../../themes';
|
||||||
|
import { InlineLabel } from '../Forms/InlineLabel';
|
||||||
|
import { InlineFieldRow } from '../Forms/InlineFieldRow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Horizontal section for editor components.
|
||||||
|
*
|
||||||
|
* @alpha
|
||||||
|
*/
|
||||||
|
export const SegmentSection = ({
|
||||||
|
label,
|
||||||
|
children,
|
||||||
|
fill,
|
||||||
|
}: {
|
||||||
|
// Name of the section
|
||||||
|
label: string;
|
||||||
|
// List of components in the section
|
||||||
|
children: React.ReactNode;
|
||||||
|
// Fill the space at the end
|
||||||
|
fill?: boolean;
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<InlineFieldRow>
|
||||||
|
<InlineLabel width={12} className={styles.label}>
|
||||||
|
{label}
|
||||||
|
</InlineLabel>
|
||||||
|
{children}
|
||||||
|
{fill && (
|
||||||
|
<div className={styles.fill}>
|
||||||
|
<InlineLabel>{''}</InlineLabel>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</InlineFieldRow>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => ({
|
||||||
|
label: css`
|
||||||
|
color: ${theme.colors.primary.text};
|
||||||
|
`,
|
||||||
|
fill: css`
|
||||||
|
flex-grow: 1;
|
||||||
|
margin-bottom: ${theme.spacing(0.5)};
|
||||||
|
`,
|
||||||
|
});
|
@ -2,5 +2,6 @@ export { Segment } from './Segment';
|
|||||||
export { SegmentAsync } from './SegmentAsync';
|
export { SegmentAsync } from './SegmentAsync';
|
||||||
export { SegmentSelect } from './SegmentSelect';
|
export { SegmentSelect } from './SegmentSelect';
|
||||||
export { SegmentInput } from './SegmentInput';
|
export { SegmentInput } from './SegmentInput';
|
||||||
|
export { SegmentSection } from './SegmentSection';
|
||||||
export { SegmentProps } from './types';
|
export { SegmentProps } from './types';
|
||||||
export { useExpandableLabel } from './useExpandableLabel';
|
export { useExpandableLabel } from './useExpandableLabel';
|
||||||
|
@ -152,7 +152,7 @@ export { CertificationKey } from './DataSourceSettings/CertificationKey';
|
|||||||
export { Spinner } from './Spinner/Spinner';
|
export { Spinner } from './Spinner/Spinner';
|
||||||
export { FadeTransition } from './transitions/FadeTransition';
|
export { FadeTransition } from './transitions/FadeTransition';
|
||||||
export { SlideOutTransition } from './transitions/SlideOutTransition';
|
export { SlideOutTransition } from './transitions/SlideOutTransition';
|
||||||
export { Segment, SegmentAsync, SegmentInput, SegmentSelect } from './Segment/';
|
export { Segment, SegmentAsync, SegmentInput, SegmentSelect, SegmentSection } from './Segment/';
|
||||||
export { Drawer } from './Drawer/Drawer';
|
export { Drawer } from './Drawer/Drawer';
|
||||||
export { Slider } from './Slider/Slider';
|
export { Slider } from './Slider/Slider';
|
||||||
export { RangeSlider } from './Slider/RangeSlider';
|
export { RangeSlider } from './Slider/RangeSlider';
|
||||||
|
@ -12,12 +12,11 @@ import {
|
|||||||
DataSourceHttpSettings,
|
DataSourceHttpSettings,
|
||||||
GraphContextMenu,
|
GraphContextMenu,
|
||||||
Icon,
|
Icon,
|
||||||
Spinner,
|
|
||||||
LegacyForms,
|
LegacyForms,
|
||||||
SeriesColorPickerPopoverWithTheme,
|
SeriesColorPickerPopoverWithTheme,
|
||||||
|
Spinner,
|
||||||
UnitPicker,
|
UnitPicker,
|
||||||
} from '@grafana/ui';
|
} from '@grafana/ui';
|
||||||
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
|
|
||||||
import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor';
|
import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor';
|
||||||
import { HelpModal } from './components/help/HelpModal';
|
import { HelpModal } from './components/help/HelpModal';
|
||||||
import { Footer } from './components/Footer/Footer';
|
import { Footer } from './components/Footer/Footer';
|
||||||
@ -25,11 +24,7 @@ import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
|||||||
import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
|
import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
|
||||||
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
||||||
import QueryEditor from 'app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor';
|
import QueryEditor from 'app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor';
|
||||||
import { GraphiteTextEditor } from '../plugins/datasource/graphite/components/GraphiteTextEditor';
|
import { GraphiteQueryEditor } from '../plugins/datasource/graphite/components/GraphiteQueryEditor';
|
||||||
import { PlayButton } from '../plugins/datasource/graphite/components/PlayButton';
|
|
||||||
import { AddGraphiteFunction } from '../plugins/datasource/graphite/components/AddGraphiteFunction';
|
|
||||||
import { GraphiteFunctionEditor } from '../plugins/datasource/graphite/components/GraphiteFunctionEditor';
|
|
||||||
import { SeriesSection } from '../plugins/datasource/graphite/components/SeriesSection';
|
|
||||||
|
|
||||||
const { SecretFormField } = LegacyForms;
|
const { SecretFormField } = LegacyForms;
|
||||||
|
|
||||||
@ -207,10 +202,5 @@ export function registerAngularDirectives() {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Temporal wrappers for Graphite migration
|
// Temporal wrappers for Graphite migration
|
||||||
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
|
react2AngularDirective('graphiteQueryEditor', GraphiteQueryEditor, ['state', 'dispatch']);
|
||||||
react2AngularDirective('graphiteTextEditor', GraphiteTextEditor, ['rawQuery', 'dispatch']);
|
|
||||||
react2AngularDirective('playButton', PlayButton, ['dispatch']);
|
|
||||||
react2AngularDirective('addGraphiteFunction', AddGraphiteFunction, ['funcDefs', 'dispatch']);
|
|
||||||
react2AngularDirective('graphiteFunctionEditor', GraphiteFunctionEditor, ['func', 'dispatch']);
|
|
||||||
react2AngularDirective('seriesSection', SeriesSection, ['state', 'dispatch']);
|
|
||||||
}
|
}
|
||||||
|
@ -5,14 +5,14 @@ import { actions } from '../state/actions';
|
|||||||
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { mapFuncDefsToSelectables } from './helpers';
|
import { mapFuncDefsToSelectables } from './helpers';
|
||||||
import { Dispatch } from 'redux';
|
import { useDispatch } from '../state/context';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dispatch: Dispatch;
|
|
||||||
funcDefs: FuncDefs;
|
funcDefs: FuncDefs;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AddGraphiteFunction({ dispatch, funcDefs }: Props) {
|
export function AddGraphiteFunction({ funcDefs }: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const [value, setValue] = useState<SelectableValue<string> | undefined>(undefined);
|
const [value, setValue] = useState<SelectableValue<string> | undefined>(undefined);
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
@ -37,7 +37,7 @@ export function AddGraphiteFunction({ dispatch, funcDefs }: Props) {
|
|||||||
options={options}
|
options={options}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
inputMinWidth={150}
|
inputMinWidth={150}
|
||||||
></Segment>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { FunctionEditor } from './FunctionEditor';
|
import { FunctionEditor } from './FunctionEditor';
|
||||||
import { FuncInstance } from './gfunc';
|
import { FuncInstance } from '../gfunc';
|
||||||
|
|
||||||
function mockFunctionInstance(name: string, unknown?: boolean): FuncInstance {
|
function mockFunctionInstance(name: string, unknown?: boolean): FuncInstance {
|
||||||
const def = {
|
const def = {
|
@ -1,7 +1,7 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { PopoverController, Popover, ClickOutsideWrapper, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
import { PopoverController, Popover, ClickOutsideWrapper, Icon, Tooltip, useStyles2 } from '@grafana/ui';
|
||||||
import { FunctionEditorControls, FunctionEditorControlsProps } from './FunctionEditorControls';
|
import { FunctionEditorControls, FunctionEditorControlsProps } from './FunctionEditorControls';
|
||||||
import { FuncInstance } from './gfunc';
|
import { FuncInstance } from '../gfunc';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { Icon, Tooltip } from '@grafana/ui';
|
import { Icon, Tooltip } from '@grafana/ui';
|
||||||
import { FuncInstance } from './gfunc';
|
import { FuncInstance } from '../gfunc';
|
||||||
|
|
||||||
export interface FunctionEditorControlsProps {
|
export interface FunctionEditorControlsProps {
|
||||||
onMoveLeft: (func: FuncInstance) => void;
|
onMoveLeft: (func: FuncInstance) => void;
|
@ -0,0 +1,21 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { FuncDefs, FuncInstance } from '../gfunc';
|
||||||
|
import { GraphiteFunctionEditor } from './GraphiteFunctionEditor';
|
||||||
|
import { AddGraphiteFunction } from './AddGraphiteFunction';
|
||||||
|
import { SegmentSection } from '@grafana/ui';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
functions: FuncInstance[];
|
||||||
|
funcDefs: FuncDefs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function FunctionsSection({ functions = [], funcDefs }: Props) {
|
||||||
|
return (
|
||||||
|
<SegmentSection label="Functions" fill={true}>
|
||||||
|
{functions.map((func: FuncInstance, index: number) => {
|
||||||
|
return !func.hidden && <GraphiteFunctionEditor key={index} func={func} />;
|
||||||
|
})}
|
||||||
|
<AddGraphiteFunction funcDefs={funcDefs} />
|
||||||
|
</SegmentSection>
|
||||||
|
);
|
||||||
|
}
|
@ -5,18 +5,19 @@ import { css, cx } from '@emotion/css';
|
|||||||
import { FuncInstance } from '../gfunc';
|
import { FuncInstance } from '../gfunc';
|
||||||
import { EditableParam, FunctionParamEditor } from './FunctionParamEditor';
|
import { EditableParam, FunctionParamEditor } from './FunctionParamEditor';
|
||||||
import { actions } from '../state/actions';
|
import { actions } from '../state/actions';
|
||||||
import { FunctionEditor } from '../FunctionEditor';
|
import { FunctionEditor } from './FunctionEditor';
|
||||||
import { mapFuncInstanceToParams } from './helpers';
|
import { mapFuncInstanceToParams } from './helpers';
|
||||||
|
import { useDispatch } from '../state/context';
|
||||||
|
|
||||||
export type FunctionEditorProps = {
|
export type FunctionEditorProps = {
|
||||||
func: FuncInstance;
|
func: FuncInstance;
|
||||||
dispatch: (action: any) => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Allows editing function params and removing/moving a function (note: editing function name is not supported)
|
* Allows editing function params and removing/moving a function (note: editing function name is not supported)
|
||||||
*/
|
*/
|
||||||
export function GraphiteFunctionEditor({ func, dispatch }: FunctionEditorProps) {
|
export function GraphiteFunctionEditor({ func }: FunctionEditorProps) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
// keep track of mouse over and isExpanded state to display buttons for adding optional/multiple params
|
// keep track of mouse over and isExpanded state to display buttons for adding optional/multiple params
|
||||||
@ -81,6 +82,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
|||||||
borderRadius: theme.shape.borderRadius(),
|
borderRadius: theme.shape.borderRadius(),
|
||||||
marginRight: theme.spacing(0.5),
|
marginRight: theme.spacing(0.5),
|
||||||
padding: `0 ${theme.spacing(1)}`,
|
padding: `0 ${theme.spacing(1)}`,
|
||||||
|
height: `${theme.v1.spacing.formInputHeight}px`,
|
||||||
}),
|
}),
|
||||||
error: css`
|
error: css`
|
||||||
border: 1px solid ${theme.colors.error.main};
|
border: 1px solid ${theme.colors.error.main};
|
||||||
|
@ -0,0 +1,26 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Dispatch } from 'redux';
|
||||||
|
import { GraphiteQueryEditorState } from '../state/store';
|
||||||
|
import { GraphiteTextEditor } from './GraphiteTextEditor';
|
||||||
|
import { SeriesSection } from './SeriesSection';
|
||||||
|
import { GraphiteContext } from '../state/context';
|
||||||
|
import { FunctionsSection } from './FunctionsSection';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
state: GraphiteQueryEditorState;
|
||||||
|
dispatch: Dispatch;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function GraphiteQueryEditor({ dispatch, state }: Props) {
|
||||||
|
return (
|
||||||
|
<GraphiteContext dispatch={dispatch}>
|
||||||
|
{state.target?.textEditor && <GraphiteTextEditor rawQuery={state.target.target} />}
|
||||||
|
{!state.target?.textEditor && (
|
||||||
|
<>
|
||||||
|
<SeriesSection state={state} />
|
||||||
|
<FunctionsSection functions={state.queryModel?.functions} funcDefs={state.funcDefs!} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</GraphiteContext>
|
||||||
|
);
|
||||||
|
}
|
@ -1,14 +1,15 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { QueryField } from '@grafana/ui';
|
import { QueryField } from '@grafana/ui';
|
||||||
import { actions } from '../state/actions';
|
import { actions } from '../state/actions';
|
||||||
import { Dispatch } from 'redux';
|
import { useDispatch } from '../state/context';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
rawQuery: string;
|
rawQuery: string;
|
||||||
dispatch: Dispatch;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function GraphiteTextEditor({ rawQuery, dispatch }: Props) {
|
export function GraphiteTextEditor({ rawQuery }: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
|
||||||
const updateQuery = useCallback(
|
const updateQuery = useCallback(
|
||||||
(query: string) => {
|
(query: string) => {
|
||||||
dispatch(actions.updateQuery({ query }));
|
dispatch(actions.updateQuery({ query }));
|
||||||
@ -21,7 +22,6 @@ export function GraphiteTextEditor({ rawQuery, dispatch }: Props) {
|
|||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<QueryField
|
<QueryField
|
||||||
query={rawQuery}
|
query={rawQuery}
|
||||||
onChange={updateQuery}
|
onChange={updateQuery}
|
||||||
@ -30,6 +30,5 @@ export function GraphiteTextEditor({ rawQuery, dispatch }: Props) {
|
|||||||
placeholder={'Enter a Graphite query (run with Shift+Enter)'}
|
placeholder={'Enter a Graphite query (run with Shift+Enter)'}
|
||||||
portalOrigin="graphite"
|
portalOrigin="graphite"
|
||||||
/>
|
/>
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,16 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { SegmentAsync } from '@grafana/ui';
|
import { SegmentAsync } from '@grafana/ui';
|
||||||
import { actions } from '../state/actions';
|
import { actions } from '../state/actions';
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
import { GraphiteSegment } from '../types';
|
import { GraphiteSegment } from '../types';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { getAltSegmentsSelectables } from '../state/providers';
|
import { getAltSegmentsSelectables } from '../state/providers';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import { GraphiteQueryEditorState } from '../state/store';
|
import { GraphiteQueryEditorState } from '../state/store';
|
||||||
|
import { useDispatch } from '../state/context';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
segment: GraphiteSegment;
|
segment: GraphiteSegment;
|
||||||
metricIndex: number;
|
metricIndex: number;
|
||||||
dispatch: Dispatch;
|
|
||||||
state: GraphiteQueryEditorState;
|
state: GraphiteQueryEditorState;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -25,7 +24,8 @@ type Props = {
|
|||||||
* getAltSegmentsSelectables() also returns list of tags for segment with index=0. Once a tag is selected the editor
|
* getAltSegmentsSelectables() also returns list of tags for segment with index=0. Once a tag is selected the editor
|
||||||
* enters tag-adding mode (see SeriesSection and GraphiteQueryModel.seriesByTagUsed).
|
* enters tag-adding mode (see SeriesSection and GraphiteQueryModel.seriesByTagUsed).
|
||||||
*/
|
*/
|
||||||
export function MetricSegment({ dispatch, metricIndex, segment, state }: Props) {
|
export function MetricSegment({ metricIndex, segment, state }: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const loadOptions = useCallback(
|
const loadOptions = useCallback(
|
||||||
(value: string | undefined) => {
|
(value: string | undefined) => {
|
||||||
return getAltSegmentsSelectables(state, metricIndex, value || '');
|
return getAltSegmentsSelectables(state, metricIndex, value || '');
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import React, { PureComponent } from 'react';
|
import React, { PureComponent } from 'react';
|
||||||
import { MetadataInspectorProps, rangeUtil } from '@grafana/data';
|
import { MetadataInspectorProps, rangeUtil } from '@grafana/data';
|
||||||
import { GraphiteDatasource } from './datasource';
|
import { GraphiteDatasource } from '../datasource';
|
||||||
import { GraphiteQuery, GraphiteOptions, MetricTankSeriesMeta } from './types';
|
import { GraphiteQuery, GraphiteOptions, MetricTankSeriesMeta } from '../types';
|
||||||
import { parseSchemaRetentions, getRollupNotice, getRuntimeConsolidationNotice } from './meta';
|
import { parseSchemaRetentions, getRollupNotice, getRuntimeConsolidationNotice } from '../meta';
|
||||||
import { stylesFactory } from '@grafana/ui';
|
import { stylesFactory } from '@grafana/ui';
|
||||||
import { config } from 'app/core/config';
|
import { config } from 'app/core/config';
|
||||||
|
|
@ -1,34 +1,19 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
import { GraphiteSegment } from '../types';
|
import { GraphiteSegment } from '../types';
|
||||||
import { GraphiteQueryEditorState } from '../state/store';
|
import { GraphiteQueryEditorState } from '../state/store';
|
||||||
import { MetricSegment } from './MetricSegment';
|
import { MetricSegment } from './MetricSegment';
|
||||||
import { css } from '@emotion/css';
|
|
||||||
import { useStyles2 } from '@grafana/ui';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
segments: GraphiteSegment[];
|
segments: GraphiteSegment[];
|
||||||
dispatch: Dispatch;
|
|
||||||
state: GraphiteQueryEditorState;
|
state: GraphiteQueryEditorState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function MetricsSection({ dispatch, segments = [], state }: Props) {
|
export function MetricsSection({ segments = [], state }: Props) {
|
||||||
const styles = useStyles2(getStyles);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<>
|
||||||
{segments.map((segment, index) => {
|
{segments.map((segment, index) => {
|
||||||
return <MetricSegment segment={segment} metricIndex={index} key={index} dispatch={dispatch} state={state} />;
|
return <MetricSegment segment={segment} metricIndex={index} key={index} state={state} />;
|
||||||
})}
|
})}
|
||||||
</div>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStyles() {
|
|
||||||
return {
|
|
||||||
container: css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
import { Button } from '@grafana/ui';
|
import { Button } from '@grafana/ui';
|
||||||
import { actions } from '../state/actions';
|
import { actions } from '../state/actions';
|
||||||
import { Dispatch } from 'redux';
|
import { useDispatch } from '../state/context';
|
||||||
|
|
||||||
type Props = {
|
export function PlayButton() {
|
||||||
rawQuery: string;
|
const dispatch = useDispatch();
|
||||||
dispatch: Dispatch;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function PlayButton({ dispatch }: Props) {
|
|
||||||
const onClick = useCallback(() => {
|
const onClick = useCallback(() => {
|
||||||
dispatch(actions.unpause());
|
dispatch(actions.unpause());
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
@ -1,23 +1,23 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
import { GraphiteQueryEditorState } from '../state/store';
|
import { GraphiteQueryEditorState } from '../state/store';
|
||||||
import { TagsSection } from './TagsSection';
|
import { TagsSection } from './TagsSection';
|
||||||
import { MetricsSection } from './MetricsSection';
|
import { MetricsSection } from './MetricsSection';
|
||||||
|
import { SegmentSection } from '@grafana/ui';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dispatch: Dispatch;
|
|
||||||
state: GraphiteQueryEditorState;
|
state: GraphiteQueryEditorState;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function SeriesSection({ dispatch, state }: Props) {
|
export function SeriesSection({ state }: Props) {
|
||||||
return state.queryModel?.seriesByTagUsed ? (
|
const sectionContent = state.queryModel?.seriesByTagUsed ? (
|
||||||
<TagsSection
|
<TagsSection tags={state.queryModel?.tags} addTagSegments={state.addTagSegments} state={state} />
|
||||||
dispatch={dispatch}
|
|
||||||
tags={state.queryModel?.tags}
|
|
||||||
addTagSegments={state.addTagSegments}
|
|
||||||
state={state}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<MetricsSection dispatch={dispatch} segments={state.segments} state={state} />
|
<MetricsSection segments={state.segments} state={state} />
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SegmentSection label="Series" fill={true}>
|
||||||
|
{sectionContent}
|
||||||
|
</SegmentSection>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,15 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
import { Segment, SegmentAsync } from '@grafana/ui';
|
import { Segment, SegmentAsync } from '@grafana/ui';
|
||||||
import { actions } from '../state/actions';
|
import { actions } from '../state/actions';
|
||||||
import { GraphiteTag, GraphiteTagOperator } from '../types';
|
import { GraphiteTag, GraphiteTagOperator } from '../types';
|
||||||
import { getTagOperatorsSelectables, getTagsSelectables, getTagValuesSelectables } from '../state/providers';
|
import { getTagOperatorsSelectables, getTagsSelectables, getTagValuesSelectables } from '../state/providers';
|
||||||
import { GraphiteQueryEditorState } from '../state/store';
|
import { GraphiteQueryEditorState } from '../state/store';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { useDispatch } from '../state/context';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
tag: GraphiteTag;
|
tag: GraphiteTag;
|
||||||
tagIndex: number;
|
tagIndex: number;
|
||||||
dispatch: Dispatch;
|
|
||||||
state: GraphiteQueryEditorState;
|
state: GraphiteQueryEditorState;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -22,7 +21,8 @@ type Props = {
|
|||||||
* Options for tag names and values are reloaded while user is typing with backend taking care of auto-complete
|
* Options for tag names and values are reloaded while user is typing with backend taking care of auto-complete
|
||||||
* (auto-complete cannot be implemented in front-end because backend returns only limited number of entries)
|
* (auto-complete cannot be implemented in front-end because backend returns only limited number of entries)
|
||||||
*/
|
*/
|
||||||
export function TagEditor({ dispatch, tag, tagIndex, state }: Props) {
|
export function TagEditor({ tag, tagIndex, state }: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const getTagsOptions = useCallback(
|
const getTagsOptions = useCallback(
|
||||||
(inputValue: string | undefined) => {
|
(inputValue: string | undefined) => {
|
||||||
return getTagsSelectables(state, tagIndex, inputValue || '');
|
return getTagsSelectables(state, tagIndex, inputValue || '');
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import React, { useCallback, useMemo } from 'react';
|
import React, { useCallback, useMemo } from 'react';
|
||||||
import { Dispatch } from 'redux';
|
|
||||||
import { GraphiteSegment } from '../types';
|
import { GraphiteSegment } from '../types';
|
||||||
import { GraphiteTag } from '../graphite_query';
|
import { GraphiteTag } from '../graphite_query';
|
||||||
import { GraphiteQueryEditorState } from '../state/store';
|
import { GraphiteQueryEditorState } from '../state/store';
|
||||||
@ -11,9 +10,10 @@ import { css } from '@emotion/css';
|
|||||||
import { mapSegmentsToSelectables } from './helpers';
|
import { mapSegmentsToSelectables } from './helpers';
|
||||||
import { TagEditor } from './TagEditor';
|
import { TagEditor } from './TagEditor';
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
|
import { useDispatch } from '../state/context';
|
||||||
|
import { PlayButton } from './PlayButton';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
dispatch: Dispatch;
|
|
||||||
tags: GraphiteTag[];
|
tags: GraphiteTag[];
|
||||||
addTagSegments: GraphiteSegment[];
|
addTagSegments: GraphiteSegment[];
|
||||||
state: GraphiteQueryEditorState;
|
state: GraphiteQueryEditorState;
|
||||||
@ -25,7 +25,8 @@ type Props = {
|
|||||||
* Options for tag names are reloaded while user is typing with backend taking care of auto-complete
|
* Options for tag names are reloaded while user is typing with backend taking care of auto-complete
|
||||||
* (auto-complete cannot be implemented in front-end because backend returns only limited number of entries)
|
* (auto-complete cannot be implemented in front-end because backend returns only limited number of entries)
|
||||||
*/
|
*/
|
||||||
export function TagsSection({ dispatch, tags, state, addTagSegments }: Props) {
|
export function TagsSection({ tags, state, addTagSegments }: Props) {
|
||||||
|
const dispatch = useDispatch();
|
||||||
const styles = useStyles2(getStyles);
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
const newTagsOptions = mapSegmentsToSelectables(addTagSegments || []);
|
const newTagsOptions = mapSegmentsToSelectables(addTagSegments || []);
|
||||||
@ -43,9 +44,9 @@ export function TagsSection({ dispatch, tags, state, addTagSegments }: Props) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<>
|
||||||
{tags.map((tag, index) => {
|
{tags.map((tag, index) => {
|
||||||
return <TagEditor key={index} tagIndex={index} tag={tag} dispatch={dispatch} state={state} />;
|
return <TagEditor key={index} tagIndex={index} tag={tag} state={state} />;
|
||||||
})}
|
})}
|
||||||
{newTagsOptions.length && (
|
{newTagsOptions.length && (
|
||||||
<SegmentAsync<GraphiteSegment>
|
<SegmentAsync<GraphiteSegment>
|
||||||
@ -58,16 +59,13 @@ export function TagsSection({ dispatch, tags, state, addTagSegments }: Props) {
|
|||||||
Component={<Button icon="plus" variant="secondary" className={styles.button} />}
|
Component={<Button icon="plus" variant="secondary" className={styles.button} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
{state.paused && <PlayButton />}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStyles(theme: GrafanaTheme2) {
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
return {
|
return {
|
||||||
container: css`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
`,
|
|
||||||
button: css`
|
button: css`
|
||||||
margin-right: ${theme.spacing(0.5)};
|
margin-right: ${theme.spacing(0.5)};
|
||||||
`,
|
`,
|
||||||
|
@ -2,7 +2,7 @@ import { GraphiteDatasource } from './datasource';
|
|||||||
import { GraphiteQueryCtrl } from './query_ctrl';
|
import { GraphiteQueryCtrl } from './query_ctrl';
|
||||||
import { DataSourcePlugin } from '@grafana/data';
|
import { DataSourcePlugin } from '@grafana/data';
|
||||||
import { ConfigEditor } from './configuration/ConfigEditor';
|
import { ConfigEditor } from './configuration/ConfigEditor';
|
||||||
import { MetricTankMetaInspector } from './MetricTankMetaInspector';
|
import { MetricTankMetaInspector } from './components/MetricTankMetaInspector';
|
||||||
|
|
||||||
class AnnotationsQueryCtrl {
|
class AnnotationsQueryCtrl {
|
||||||
static templateUrl = 'partials/annotations.editor.html';
|
static templateUrl = 'partials/annotations.editor.html';
|
||||||
|
@ -1,42 +1,3 @@
|
|||||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
||||||
|
<graphite-query-editor state="ctrl.state" dispatch="ctrl.dispatch"></graphite-query-editor>
|
||||||
<div class="gf-form" ng-show="ctrl.state.target.textEditor">
|
|
||||||
<graphite-text-editor style="width: 100%" rawQuery="ctrl.state.target.target" dispatch="ctrl.dispatch"></graphite-text-editor>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-hide="ctrl.target.textEditor">
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-6 query-keyword">Series</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<series-section dispatch="ctrl.dispatch" tags="ctrl.state.queryModel.tags" addTagSegments="ctrl.state.addTagSegments" state="ctrl.state" segments="ctrl.state.segments" rawQuery="ctrl.state.target.target"></series-section>
|
|
||||||
|
|
||||||
<div ng-if="ctrl.state.paused" class="gf-form">
|
|
||||||
<play-button dispatch="ctrl.dispatch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form-inline">
|
|
||||||
<div class="gf-form">
|
|
||||||
<label class="gf-form-label width-6 query-keyword">Functions</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div ng-repeat="func in ctrl.state.queryModel.functions" class="gf-form">
|
|
||||||
<graphite-function-editor func="func" dispatch="ctrl.dispatch" ng-hide="func.hidden"></graphite-function-editor>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form dropdown">
|
|
||||||
<add-graphite-function funcDefs="ctrl.state.funcDefs" dispatch="ctrl.dispatch" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
|
||||||
<div class="gf-form-label gf-form-label--grow"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</query-editor-row>
|
</query-editor-row>
|
||||||
|
@ -4,13 +4,7 @@ import { auto } from 'angular';
|
|||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv } from '@grafana/runtime';
|
||||||
import { actions } from './state/actions';
|
import { actions } from './state/actions';
|
||||||
import { createStore, GraphiteQueryEditorState } from './state/store';
|
import { createStore, GraphiteQueryEditorState } from './state/store';
|
||||||
import {
|
import { GraphiteActionDispatcher, GraphiteQueryEditorAngularDependencies } from './types';
|
||||||
GraphiteActionDispatcher,
|
|
||||||
GraphiteQueryEditorAngularDependencies,
|
|
||||||
GraphiteSegment,
|
|
||||||
GraphiteTag,
|
|
||||||
} from './types';
|
|
||||||
import { ChangeEvent } from 'react';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @deprecated Moved to state/store
|
* @deprecated Moved to state/store
|
||||||
@ -81,174 +75,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
|||||||
this.dispatch(actions.init(deps as GraphiteQueryEditorAngularDependencies));
|
this.dispatch(actions.init(deps as GraphiteQueryEditorAngularDependencies));
|
||||||
}
|
}
|
||||||
|
|
||||||
parseTarget() {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
async toggleEditorMode() {
|
async toggleEditorMode() {
|
||||||
await this.dispatch(actions.toggleEditorMode());
|
await this.dispatch(actions.toggleEditorMode());
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSegments(modifyLastSegment = true) {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addSelectMetricSegment() {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkOtherSegments(fromIndex: number, modifyLastSegment = true) {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
setSegmentFocus(segmentIndex: any) {
|
|
||||||
// WIP: removed
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of options for an empty segment or a segment with metric when it's clicked/opened.
|
|
||||||
*
|
|
||||||
* This is used for new segments and segments with metrics selected.
|
|
||||||
*/
|
|
||||||
getAltSegments(index: number, text: string): void {
|
|
||||||
// WIP: moved to state/providers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
addAltTagSegments(prefix: string, altSegments: any[]) {
|
|
||||||
// WIP: moved to state/providers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTaggedEntry(altSegments: any[]) {
|
|
||||||
// WIP: moved to state/providers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply changes to a given metric segment
|
|
||||||
*/
|
|
||||||
async segmentValueChanged(segment: GraphiteSegment, index: number) {
|
|
||||||
// WIP: moved to MetricsSegment
|
|
||||||
}
|
|
||||||
|
|
||||||
spliceSegments(index: any) {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
emptySegments() {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
async targetTextChanged(event: ChangeEvent<HTMLInputElement>) {
|
|
||||||
// WIP: removed, handled by GraphiteTextEditor
|
|
||||||
}
|
|
||||||
|
|
||||||
updateModelTarget() {
|
|
||||||
// WIP: moved to state/helpers as handleTargetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
async addFunction(name: string) {
|
|
||||||
// WIP: removed, called from AddGraphiteFunction
|
|
||||||
}
|
|
||||||
|
|
||||||
removeFunction(func: any) {
|
|
||||||
// WIP: converted to "removeFunction" action and handled in state/store reducer
|
|
||||||
// It's now dispatched in func_editor
|
|
||||||
}
|
|
||||||
|
|
||||||
moveFunction(func: any, offset: any) {
|
|
||||||
// WIP: converted to "moveFunction" action and handled in state/store reducer
|
|
||||||
// It's now dispatched in func_editor
|
|
||||||
}
|
|
||||||
|
|
||||||
addSeriesByTagFunc(tag: string) {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
// It's now dispatched in func_editor
|
|
||||||
}
|
|
||||||
|
|
||||||
smartlyHandleNewAliasByNode(func: { def: { name: string }; params: number[]; added: boolean }) {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllTags() {
|
|
||||||
// WIP: removed. It was not used.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of tags for editing exiting tag with <gf-form-dropdown>
|
|
||||||
*/
|
|
||||||
getTags(index: number, query: string): void {
|
|
||||||
// WIP: removed, called from TagsSection
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get tag list when adding a new tag with <metric-segment>
|
|
||||||
*/
|
|
||||||
getTagsAsSegments(query: string): void {
|
|
||||||
// WIP: removed, called from TagsSection
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of available tag operators
|
|
||||||
*/
|
|
||||||
getTagOperators(): void {
|
|
||||||
// WIP: removed, called from TagsSection
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllTagValues(tag: { key: any }) {
|
|
||||||
// WIP: removed. It was not used.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get list of available tag values
|
|
||||||
*/
|
|
||||||
getTagValues(tag: GraphiteTag, index: number, query: string): void {
|
|
||||||
// WIP: removed, called from TagsSection
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply changes when a tag is changed
|
|
||||||
*/
|
|
||||||
async tagChanged(tag: GraphiteTag, index: number) {
|
|
||||||
// WIP: removed, called from TagsSection
|
|
||||||
}
|
|
||||||
|
|
||||||
async addNewTag(segment: GraphiteSegment) {
|
|
||||||
// WIP: removed, called from TagsSection
|
|
||||||
}
|
|
||||||
|
|
||||||
removeTag(index: any) {
|
|
||||||
// WIP: removed. It was not used.
|
|
||||||
// Tags are removed by selecting the segment called "-- remove tag --"
|
|
||||||
}
|
|
||||||
|
|
||||||
fixTagSegments() {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
showDelimiter(index: number) {
|
|
||||||
// WIP: removed. It was not used because of broken syntax in the template. The logic has been moved directly to the template
|
|
||||||
}
|
|
||||||
|
|
||||||
pause() {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
async unpause() {
|
|
||||||
// WIP: removed, called from PlayButton
|
|
||||||
}
|
|
||||||
|
|
||||||
getCollapsedText() {
|
|
||||||
// WIP: removed. It was not used.
|
|
||||||
}
|
|
||||||
|
|
||||||
handleTagsAutoCompleteError(error: Error): void {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
|
|
||||||
handleMetricsAutoCompleteError(error: Error): void {
|
|
||||||
// WIP: moved to state/helpers (the same name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WIP: moved to state/providers (the same names)
|
|
||||||
// function mapToDropdownOptions(results: any[]) {}
|
|
||||||
// function removeTagPrefix(value: string): string {}
|
|
||||||
|
16
public/app/plugins/datasource/graphite/state/context.tsx
Normal file
16
public/app/plugins/datasource/graphite/state/context.tsx
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import React, { createContext, Dispatch, PropsWithChildren, useContext } from 'react';
|
||||||
|
import { AnyAction } from '@reduxjs/toolkit';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
dispatch: Dispatch<AnyAction>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DispatchContext = createContext<Dispatch<AnyAction>>({} as Dispatch<AnyAction>);
|
||||||
|
|
||||||
|
export const useDispatch = () => {
|
||||||
|
return useContext(DispatchContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GraphiteContext = ({ children, dispatch }: PropsWithChildren<Props>) => {
|
||||||
|
return <DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider>;
|
||||||
|
};
|
@ -50,12 +50,12 @@ describe('InfluxDB InfluxQL Visual Editor', () => {
|
|||||||
};
|
};
|
||||||
assertEditor(
|
assertEditor(
|
||||||
query,
|
query,
|
||||||
'from[default][select measurement]where[+]' +
|
'FROM[default][select measurement]WHERE[+]' +
|
||||||
'select[field]([value])[mean]()[+]' +
|
'SELECT[field]([value])[mean]()[+]' +
|
||||||
'group by[time]([$__interval])[fill]([null])[+]' +
|
'GROUP BY[time]([$__interval])[fill]([null])[+]' +
|
||||||
'timezone[(optional)]order by time[ASC]' +
|
'TIMEZONE[(optional)]ORDER BY TIME[ASC]' +
|
||||||
'limit[(optional)]slimit[(optional)]' +
|
'LIMIT[(optional)]SLIMIT[(optional)]' +
|
||||||
'format as[time_series]alias[Naming pattern]'
|
'FORMAT AS[time_series]ALIAS[Naming pattern]'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should have the alias-field hidden when format-as-table', () => {
|
it('should have the alias-field hidden when format-as-table', () => {
|
||||||
@ -66,12 +66,12 @@ describe('InfluxDB InfluxQL Visual Editor', () => {
|
|||||||
};
|
};
|
||||||
assertEditor(
|
assertEditor(
|
||||||
query,
|
query,
|
||||||
'from[default][select measurement]where[+]' +
|
'FROM[default][select measurement]WHERE[+]' +
|
||||||
'select[field]([value])[mean]()[+]' +
|
'SELECT[field]([value])[mean]()[+]' +
|
||||||
'group by[time]([$__interval])[fill]([null])[+]' +
|
'GROUP BY[time]([$__interval])[fill]([null])[+]' +
|
||||||
'timezone[(optional)]order by time[ASC]' +
|
'TIMEZONE[(optional)]ORDER BY TIME[ASC]' +
|
||||||
'limit[(optional)]slimit[(optional)]' +
|
'LIMIT[(optional)]SLIMIT[(optional)]' +
|
||||||
'format as[table]'
|
'FORMAT AS[table]'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
it('should handle complex query', () => {
|
it('should handle complex query', () => {
|
||||||
@ -145,13 +145,13 @@ describe('InfluxDB InfluxQL Visual Editor', () => {
|
|||||||
};
|
};
|
||||||
assertEditor(
|
assertEditor(
|
||||||
query,
|
query,
|
||||||
'from[default][cpu]where[cpu][=][cpu1][AND][cpu][<][cpu3][+]' +
|
'FROM[default][cpu]WHERE[cpu][=][cpu1][AND][cpu][<][cpu3][+]' +
|
||||||
'select[field]([usage_idle])[mean]()[+]' +
|
'SELECT[field]([usage_idle])[mean]()[+]' +
|
||||||
'[field]([usage_guest])[median]()[holt_winters_with_fit]([10],[2])[+]' +
|
'[field]([usage_guest])[median]()[holt_winters_with_fit]([10],[2])[+]' +
|
||||||
'group by[time]([$__interval])[tag]([cpu])[tag]([host])[fill]([null])[+]' +
|
'GROUP BY[time]([$__interval])[tag]([cpu])[tag]([host])[fill]([null])[+]' +
|
||||||
'timezone[UTC]order by time[DESC]' +
|
'TIMEZONE[UTC]ORDER BY TIME[DESC]' +
|
||||||
'limit[4]slimit[5]' +
|
'LIMIT[4]SLIMIT[5]' +
|
||||||
'format as[logs]alias[all i as]'
|
'FORMAT AS[logs]ALIAS[all i as]'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -24,10 +24,11 @@ import {
|
|||||||
changeGroupByPart,
|
changeGroupByPart,
|
||||||
} from '../../queryUtils';
|
} from '../../queryUtils';
|
||||||
import { FormatAsSection } from './FormatAsSection';
|
import { FormatAsSection } from './FormatAsSection';
|
||||||
import { SectionLabel } from './SectionLabel';
|
|
||||||
import { SectionFill } from './SectionFill';
|
|
||||||
import { DEFAULT_RESULT_FORMAT } from '../constants';
|
import { DEFAULT_RESULT_FORMAT } from '../constants';
|
||||||
import { getNewSelectPartOptions, getNewGroupByPartOptions, makePartList } from './partListUtils';
|
import { getNewSelectPartOptions, getNewGroupByPartOptions, makePartList } from './partListUtils';
|
||||||
|
import { InlineLabel, SegmentSection, useStyles2 } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
query: InfluxQuery;
|
query: InfluxQuery;
|
||||||
@ -51,15 +52,8 @@ function withTemplateVariableOptions(optionsPromise: Promise<string[]>): Promise
|
|||||||
return optionsPromise.then((options) => [...getTemplateVariableOptions(), ...options]);
|
return optionsPromise.then((options) => [...getTemplateVariableOptions(), ...options]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SectionWrap = ({ initialName, children }: { initialName: string; children: React.ReactNode }) => (
|
|
||||||
<div className="gf-form-inline">
|
|
||||||
<SectionLabel name={initialName} isInitial={true} />
|
|
||||||
{children}
|
|
||||||
<SectionFill />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
export const Editor = (props: Props): JSX.Element => {
|
export const Editor = (props: Props): JSX.Element => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
const query = normalizeQuery(props.query);
|
const query = normalizeQuery(props.query);
|
||||||
const { datasource } = props;
|
const { datasource } = props;
|
||||||
const { measurement, policy } = query;
|
const { measurement, policy } = query;
|
||||||
@ -112,7 +106,7 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<SectionWrap initialName="from">
|
<SegmentSection label="FROM" fill={true}>
|
||||||
<FromSection
|
<FromSection
|
||||||
policy={policy}
|
policy={policy}
|
||||||
measurement={measurement}
|
measurement={measurement}
|
||||||
@ -124,7 +118,9 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
}
|
}
|
||||||
onChange={handleFromSectionChange}
|
onChange={handleFromSectionChange}
|
||||||
/>
|
/>
|
||||||
<SectionLabel name="where" />
|
<InlineLabel width="auto" className={styles.inlineLabel}>
|
||||||
|
WHERE
|
||||||
|
</InlineLabel>
|
||||||
<TagsSection
|
<TagsSection
|
||||||
tags={query.tags ?? []}
|
tags={query.tags ?? []}
|
||||||
onChange={handleTagsSectionChange}
|
onChange={handleTagsSectionChange}
|
||||||
@ -133,9 +129,9 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
withTemplateVariableOptions(getTagValues(key, measurement, policy, query.tags ?? [], datasource))
|
withTemplateVariableOptions(getTagValues(key, measurement, policy, query.tags ?? [], datasource))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</SectionWrap>
|
</SegmentSection>
|
||||||
{selectLists.map((sel, index) => (
|
{selectLists.map((sel, index) => (
|
||||||
<SectionWrap key={index} initialName={index === 0 ? 'select' : ''}>
|
<SegmentSection key={index} label={index === 0 ? 'SELECT' : ''} fill={true}>
|
||||||
<PartListSection
|
<PartListSection
|
||||||
parts={sel}
|
parts={sel}
|
||||||
getNewPartOptions={() => Promise.resolve(getNewSelectPartOptions())}
|
getNewPartOptions={() => Promise.resolve(getNewSelectPartOptions())}
|
||||||
@ -150,9 +146,9 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
onAppliedChange(removeSelectPart(query, partIndex, index));
|
onAppliedChange(removeSelectPart(query, partIndex, index));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SectionWrap>
|
</SegmentSection>
|
||||||
))}
|
))}
|
||||||
<SectionWrap initialName="group by">
|
<SegmentSection label="GROUP BY" fill={true}>
|
||||||
<PartListSection
|
<PartListSection
|
||||||
parts={groupByList}
|
parts={groupByList}
|
||||||
getNewPartOptions={() => getNewGroupByPartOptions(query, getTagKeys)}
|
getNewPartOptions={() => getNewGroupByPartOptions(query, getTagKeys)}
|
||||||
@ -167,8 +163,8 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
onAppliedChange(removeGroupByPart(query, partIndex));
|
onAppliedChange(removeGroupByPart(query, partIndex));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SectionWrap>
|
</SegmentSection>
|
||||||
<SectionWrap initialName="timezone">
|
<SegmentSection label="TIMEZONE" fill={true}>
|
||||||
<InputSection
|
<InputSection
|
||||||
placeholder="(optional)"
|
placeholder="(optional)"
|
||||||
value={query.tz}
|
value={query.tz}
|
||||||
@ -176,20 +172,22 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
onAppliedChange({ ...query, tz });
|
onAppliedChange({ ...query, tz });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SectionLabel name="order by time" />
|
<InlineLabel width="auto" className={styles.inlineLabel}>
|
||||||
|
ORDER BY TIME
|
||||||
|
</InlineLabel>
|
||||||
<OrderByTimeSection
|
<OrderByTimeSection
|
||||||
value={query.orderByTime === 'DESC' ? 'DESC' : 'ASC' /* FIXME: make this shared with influx_query_model */}
|
value={query.orderByTime === 'DESC' ? 'DESC' : 'ASC' /* FIXME: make this shared with influx_query_model */}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
onAppliedChange({ ...query, orderByTime: v });
|
onAppliedChange({ ...query, orderByTime: v });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SectionWrap>
|
</SegmentSection>
|
||||||
{/* query.fill is ignored in the query-editor, and it is deleted whenever
|
{/* query.fill is ignored in the query-editor, and it is deleted whenever
|
||||||
query-editor changes. the influx_query_model still handles it, but the new
|
query-editor changes. the influx_query_model still handles it, but the new
|
||||||
approach seem to be to handle "fill" inside query.groupBy. so, if you
|
approach seem to be to handle "fill" inside query.groupBy. so, if you
|
||||||
have a panel where in the json you have query.fill, it will be appled,
|
have a panel where in the json you have query.fill, it will be applied,
|
||||||
as long as you do not edit that query. */}
|
as long as you do not edit that query. */}
|
||||||
<SectionWrap initialName="limit">
|
<SegmentSection label="LIMIT" fill={true}>
|
||||||
<InputSection
|
<InputSection
|
||||||
placeholder="(optional)"
|
placeholder="(optional)"
|
||||||
value={query.limit?.toString()}
|
value={query.limit?.toString()}
|
||||||
@ -197,7 +195,9 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
onAppliedChange({ ...query, limit });
|
onAppliedChange({ ...query, limit });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SectionLabel name="slimit" />
|
<InlineLabel width="auto" className={styles.inlineLabel}>
|
||||||
|
SLIMIT
|
||||||
|
</InlineLabel>
|
||||||
<InputSection
|
<InputSection
|
||||||
placeholder="(optional)"
|
placeholder="(optional)"
|
||||||
value={query.slimit?.toString()}
|
value={query.slimit?.toString()}
|
||||||
@ -205,8 +205,8 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
onAppliedChange({ ...query, slimit });
|
onAppliedChange({ ...query, slimit });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</SectionWrap>
|
</SegmentSection>
|
||||||
<SectionWrap initialName="format as">
|
<SegmentSection label="FORMAT AS" fill={true}>
|
||||||
<FormatAsSection
|
<FormatAsSection
|
||||||
format={query.resultFormat ?? DEFAULT_RESULT_FORMAT}
|
format={query.resultFormat ?? DEFAULT_RESULT_FORMAT}
|
||||||
onChange={(format) => {
|
onChange={(format) => {
|
||||||
@ -215,7 +215,9 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
{query.resultFormat !== 'table' && (
|
{query.resultFormat !== 'table' && (
|
||||||
<>
|
<>
|
||||||
<SectionLabel name="alias" />
|
<InlineLabel width="auto" className={styles.inlineLabel}>
|
||||||
|
ALIAS
|
||||||
|
</InlineLabel>
|
||||||
<InputSection
|
<InputSection
|
||||||
isWide
|
isWide
|
||||||
placeholder="Naming pattern"
|
placeholder="Naming pattern"
|
||||||
@ -226,7 +228,15 @@ export const Editor = (props: Props): JSX.Element => {
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</SectionWrap>
|
</SegmentSection>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getStyles(theme: GrafanaTheme2) {
|
||||||
|
return {
|
||||||
|
inlineLabel: css`
|
||||||
|
color: ${theme.colors.primary.text};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { cx, css } from '@emotion/css';
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
name: string;
|
|
||||||
isInitial?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const uppercaseClass = css({
|
|
||||||
textTransform: 'uppercase',
|
|
||||||
});
|
|
||||||
|
|
||||||
export const SectionLabel = ({ name, isInitial }: Props) => (
|
|
||||||
<label className={cx('gf-form-label query-keyword', { 'width-7': isInitial ?? false }, uppercaseClass)}>{name}</label>
|
|
||||||
);
|
|
Loading…
Reference in New Issue
Block a user