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:
Piotr Jamróz 2021-08-17 18:50:31 +02:00 committed by GitHub
parent f8d7726187
commit d93d989a5a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 245 additions and 387 deletions

View File

@ -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};
`, `,
}; };
}; };

View File

@ -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>
</> </>
); );

View File

@ -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>
</> </>
); );

View File

@ -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>
</> </>
); );

View File

@ -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)};
`,
});

View File

@ -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';

View File

@ -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';

View File

@ -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']);
} }

View File

@ -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> />
); );
} }

View File

@ -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 = {

View File

@ -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';

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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};

View File

@ -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>
);
}

View File

@ -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"
/> />
</>
); );
} }

View File

@ -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 || '');

View File

@ -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';

View File

@ -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;
`,
};
}

View File

@ -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]);

View File

@ -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>
); );
} }

View File

@ -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 || '');

View File

@ -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)};
`, `,

View File

@ -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';

View File

@ -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>

View File

@ -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 {}

View 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>;
};

View File

@ -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]'
); );
}); });
}); });

View File

@ -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};
`,
};
}

View File

@ -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>
);