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-wrap: wrap;
align-content: flex-start;
row-gap: ${theme.spacing.xs};
`,
};
};

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { Segment, Icon } from '@grafana/ui';
import { Segment, Icon, SegmentSection } from '@grafana/ui';
const AddButton = (
<a className="gf-form-label query-part">
@ -17,13 +17,10 @@ const groupedOptions = [
const SegmentFrame = ({ options, children }: any) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentSection label="Segment Name">
{children}
<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 { action } from '@storybook/addon-actions';
import { SelectableValue } from '@grafana/data';
import { SegmentAsync, Icon } from '@grafana/ui';
import { SegmentAsync, Icon, SegmentSection } from '@grafana/ui';
const AddButton = (
<a className="gf-form-label query-part">
@ -21,17 +21,14 @@ const loadOptionsErr = (): Promise<Array<SelectableValue<string>>> =>
const SegmentFrame = ({ loadOptions, children }: any) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
<SegmentSection label="Segment Name">
{children}
<SegmentAsync
Component={AddButton}
onChange={(value) => action('New value added')(value)}
loadOptions={() => loadOptions(options)}
/>
</div>
</SegmentSection>
</>
);

View File

@ -1,15 +1,10 @@
import React, { useState } from 'react';
import { action } from '@storybook/addon-actions';
import { SegmentInput, Icon } from '@grafana/ui';
import { SegmentInput, Icon, SegmentSection } from '@grafana/ui';
const SegmentFrame = ({ children }: any) => (
<>
<div className="gf-form-inline">
<div className="gf-form">
<span className="gf-form-label width-8 query-keyword">Segment Name</span>
</div>
{children}
</div>
<SegmentSection label="Segment Name">{children}</SegmentSection>
</>
);

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 { SegmentSelect } from './SegmentSelect';
export { SegmentInput } from './SegmentInput';
export { SegmentSection } from './SegmentSection';
export { SegmentProps } from './types';
export { useExpandableLabel } from './useExpandableLabel';

View File

@ -152,7 +152,7 @@ export { CertificationKey } from './DataSourceSettings/CertificationKey';
export { Spinner } from './Spinner/Spinner';
export { FadeTransition } from './transitions/FadeTransition';
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 { Slider } from './Slider/Slider';
export { RangeSlider } from './Slider/RangeSlider';

View File

@ -12,12 +12,11 @@ import {
DataSourceHttpSettings,
GraphContextMenu,
Icon,
Spinner,
LegacyForms,
SeriesColorPickerPopoverWithTheme,
Spinner,
UnitPicker,
} from '@grafana/ui';
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
import { LokiAnnotationsQueryEditor } from '../plugins/datasource/loki/components/AnnotationsQueryEditor';
import { HelpModal } from './components/help/HelpModal';
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 { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
import QueryEditor from 'app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor';
import { GraphiteTextEditor } from '../plugins/datasource/graphite/components/GraphiteTextEditor';
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';
import { GraphiteQueryEditor } from '../plugins/datasource/graphite/components/GraphiteQueryEditor';
const { SecretFormField } = LegacyForms;
@ -207,10 +202,5 @@ export function registerAngularDirectives() {
]);
// Temporal wrappers for Graphite migration
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
react2AngularDirective('graphiteTextEditor', GraphiteTextEditor, ['rawQuery', 'dispatch']);
react2AngularDirective('playButton', PlayButton, ['dispatch']);
react2AngularDirective('addGraphiteFunction', AddGraphiteFunction, ['funcDefs', 'dispatch']);
react2AngularDirective('graphiteFunctionEditor', GraphiteFunctionEditor, ['func', 'dispatch']);
react2AngularDirective('seriesSection', SeriesSection, ['state', 'dispatch']);
react2AngularDirective('graphiteQueryEditor', GraphiteQueryEditor, ['state', 'dispatch']);
}

View File

@ -5,14 +5,14 @@ import { actions } from '../state/actions';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { css, cx } from '@emotion/css';
import { mapFuncDefsToSelectables } from './helpers';
import { Dispatch } from 'redux';
import { useDispatch } from '../state/context';
type Props = {
dispatch: Dispatch;
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 styles = useStyles2(getStyles);
@ -37,7 +37,7 @@ export function AddGraphiteFunction({ dispatch, funcDefs }: Props) {
options={options}
onChange={setValue}
inputMinWidth={150}
></Segment>
/>
);
}

View File

@ -1,7 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { FunctionEditor } from './FunctionEditor';
import { FuncInstance } from './gfunc';
import { FuncInstance } from '../gfunc';
function mockFunctionInstance(name: string, unknown?: boolean): FuncInstance {
const def = {

View File

@ -1,7 +1,7 @@
import React, { useRef } from 'react';
import { PopoverController, Popover, ClickOutsideWrapper, Icon, Tooltip, useStyles2 } from '@grafana/ui';
import { FunctionEditorControls, FunctionEditorControlsProps } from './FunctionEditorControls';
import { FuncInstance } from './gfunc';
import { FuncInstance } from '../gfunc';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';

View File

@ -1,6 +1,6 @@
import React, { Suspense } from 'react';
import { Icon, Tooltip } from '@grafana/ui';
import { FuncInstance } from './gfunc';
import { FuncInstance } from '../gfunc';
export interface FunctionEditorControlsProps {
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 { EditableParam, FunctionParamEditor } from './FunctionParamEditor';
import { actions } from '../state/actions';
import { FunctionEditor } from '../FunctionEditor';
import { FunctionEditor } from './FunctionEditor';
import { mapFuncInstanceToParams } from './helpers';
import { useDispatch } from '../state/context';
export type FunctionEditorProps = {
func: FuncInstance;
dispatch: (action: any) => void;
};
/**
* 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);
// 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(),
marginRight: theme.spacing(0.5),
padding: `0 ${theme.spacing(1)}`,
height: `${theme.v1.spacing.formInputHeight}px`,
}),
error: css`
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 { QueryField } from '@grafana/ui';
import { actions } from '../state/actions';
import { Dispatch } from 'redux';
import { useDispatch } from '../state/context';
type Props = {
rawQuery: string;
dispatch: Dispatch;
};
export function GraphiteTextEditor({ rawQuery, dispatch }: Props) {
export function GraphiteTextEditor({ rawQuery }: Props) {
const dispatch = useDispatch();
const updateQuery = useCallback(
(query: string) => {
dispatch(actions.updateQuery({ query }));
@ -21,15 +22,13 @@ export function GraphiteTextEditor({ rawQuery, dispatch }: Props) {
}, [dispatch]);
return (
<>
<QueryField
query={rawQuery}
onChange={updateQuery}
onBlur={runQuery}
onRunQuery={runQuery}
placeholder={'Enter a Graphite query (run with Shift+Enter)'}
portalOrigin="graphite"
/>
</>
<QueryField
query={rawQuery}
onChange={updateQuery}
onBlur={runQuery}
onRunQuery={runQuery}
placeholder={'Enter a Graphite query (run with Shift+Enter)'}
portalOrigin="graphite"
/>
);
}

View File

@ -1,17 +1,16 @@
import React, { useCallback, useMemo } from 'react';
import { SegmentAsync } from '@grafana/ui';
import { actions } from '../state/actions';
import { Dispatch } from 'redux';
import { GraphiteSegment } from '../types';
import { SelectableValue } from '@grafana/data';
import { getAltSegmentsSelectables } from '../state/providers';
import { debounce } from 'lodash';
import { GraphiteQueryEditorState } from '../state/store';
import { useDispatch } from '../state/context';
type Props = {
segment: GraphiteSegment;
metricIndex: number;
dispatch: Dispatch;
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
* 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(
(value: string | undefined) => {
return getAltSegmentsSelectables(state, metricIndex, value || '');

View File

@ -1,9 +1,9 @@
import { css, cx } from '@emotion/css';
import React, { PureComponent } from 'react';
import { MetadataInspectorProps, rangeUtil } from '@grafana/data';
import { GraphiteDatasource } from './datasource';
import { GraphiteQuery, GraphiteOptions, MetricTankSeriesMeta } from './types';
import { parseSchemaRetentions, getRollupNotice, getRuntimeConsolidationNotice } from './meta';
import { GraphiteDatasource } from '../datasource';
import { GraphiteQuery, GraphiteOptions, MetricTankSeriesMeta } from '../types';
import { parseSchemaRetentions, getRollupNotice, getRuntimeConsolidationNotice } from '../meta';
import { stylesFactory } from '@grafana/ui';
import { config } from 'app/core/config';

View File

@ -1,34 +1,19 @@
import React from 'react';
import { Dispatch } from 'redux';
import { GraphiteSegment } from '../types';
import { GraphiteQueryEditorState } from '../state/store';
import { MetricSegment } from './MetricSegment';
import { css } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
type Props = {
segments: GraphiteSegment[];
dispatch: Dispatch;
state: GraphiteQueryEditorState;
};
export function MetricsSection({ dispatch, segments = [], state }: Props) {
const styles = useStyles2(getStyles);
export function MetricsSection({ segments = [], state }: Props) {
return (
<div className={styles.container}>
<>
{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 { Button } from '@grafana/ui';
import { actions } from '../state/actions';
import { Dispatch } from 'redux';
import { useDispatch } from '../state/context';
type Props = {
rawQuery: string;
dispatch: Dispatch;
};
export function PlayButton({ dispatch }: Props) {
export function PlayButton() {
const dispatch = useDispatch();
const onClick = useCallback(() => {
dispatch(actions.unpause());
}, [dispatch]);

View File

@ -1,23 +1,23 @@
import React from 'react';
import { Dispatch } from 'redux';
import { GraphiteQueryEditorState } from '../state/store';
import { TagsSection } from './TagsSection';
import { MetricsSection } from './MetricsSection';
import { SegmentSection } from '@grafana/ui';
type Props = {
dispatch: Dispatch;
state: GraphiteQueryEditorState;
};
export function SeriesSection({ dispatch, state }: Props) {
return state.queryModel?.seriesByTagUsed ? (
<TagsSection
dispatch={dispatch}
tags={state.queryModel?.tags}
addTagSegments={state.addTagSegments}
state={state}
/>
export function SeriesSection({ state }: Props) {
const sectionContent = state.queryModel?.seriesByTagUsed ? (
<TagsSection 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 { Dispatch } from 'redux';
import { Segment, SegmentAsync } from '@grafana/ui';
import { actions } from '../state/actions';
import { GraphiteTag, GraphiteTagOperator } from '../types';
import { getTagOperatorsSelectables, getTagsSelectables, getTagValuesSelectables } from '../state/providers';
import { GraphiteQueryEditorState } from '../state/store';
import { debounce } from 'lodash';
import { useDispatch } from '../state/context';
type Props = {
tag: GraphiteTag;
tagIndex: number;
dispatch: Dispatch;
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
* (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(
(inputValue: string | undefined) => {
return getTagsSelectables(state, tagIndex, inputValue || '');

View File

@ -1,5 +1,4 @@
import React, { useCallback, useMemo } from 'react';
import { Dispatch } from 'redux';
import { GraphiteSegment } from '../types';
import { GraphiteTag } from '../graphite_query';
import { GraphiteQueryEditorState } from '../state/store';
@ -11,9 +10,10 @@ import { css } from '@emotion/css';
import { mapSegmentsToSelectables } from './helpers';
import { TagEditor } from './TagEditor';
import { debounce } from 'lodash';
import { useDispatch } from '../state/context';
import { PlayButton } from './PlayButton';
type Props = {
dispatch: Dispatch;
tags: GraphiteTag[];
addTagSegments: GraphiteSegment[];
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
* (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 newTagsOptions = mapSegmentsToSelectables(addTagSegments || []);
@ -43,9 +44,9 @@ export function TagsSection({ dispatch, tags, state, addTagSegments }: Props) {
]);
return (
<div className={styles.container}>
<>
{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 && (
<SegmentAsync<GraphiteSegment>
@ -58,16 +59,13 @@ export function TagsSection({ dispatch, tags, state, addTagSegments }: Props) {
Component={<Button icon="plus" variant="secondary" className={styles.button} />}
/>
)}
</div>
{state.paused && <PlayButton />}
</>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
container: css`
display: flex;
flex-direction: row;
`,
button: css`
margin-right: ${theme.spacing(0.5)};
`,

View File

@ -2,7 +2,7 @@ import { GraphiteDatasource } from './datasource';
import { GraphiteQueryCtrl } from './query_ctrl';
import { DataSourcePlugin } from '@grafana/data';
import { ConfigEditor } from './configuration/ConfigEditor';
import { MetricTankMetaInspector } from './MetricTankMetaInspector';
import { MetricTankMetaInspector } from './components/MetricTankMetaInspector';
class AnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';

View File

@ -1,42 +1,3 @@
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
<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>
<graphite-query-editor state="ctrl.state" dispatch="ctrl.dispatch"></graphite-query-editor>
</query-editor-row>

View File

@ -4,13 +4,7 @@ import { auto } from 'angular';
import { TemplateSrv } from '@grafana/runtime';
import { actions } from './state/actions';
import { createStore, GraphiteQueryEditorState } from './state/store';
import {
GraphiteActionDispatcher,
GraphiteQueryEditorAngularDependencies,
GraphiteSegment,
GraphiteTag,
} from './types';
import { ChangeEvent } from 'react';
import { GraphiteActionDispatcher, GraphiteQueryEditorAngularDependencies } from './types';
/**
* @deprecated Moved to state/store
@ -81,174 +75,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
this.dispatch(actions.init(deps as GraphiteQueryEditorAngularDependencies));
}
parseTarget() {
// WIP: moved to state/helpers (the same name)
}
async 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(
query,
'from[default][select measurement]where[+]' +
'select[field]([value])[mean]()[+]' +
'group by[time]([$__interval])[fill]([null])[+]' +
'timezone[(optional)]order by time[ASC]' +
'limit[(optional)]slimit[(optional)]' +
'format as[time_series]alias[Naming pattern]'
'FROM[default][select measurement]WHERE[+]' +
'SELECT[field]([value])[mean]()[+]' +
'GROUP BY[time]([$__interval])[fill]([null])[+]' +
'TIMEZONE[(optional)]ORDER BY TIME[ASC]' +
'LIMIT[(optional)]SLIMIT[(optional)]' +
'FORMAT AS[time_series]ALIAS[Naming pattern]'
);
});
it('should have the alias-field hidden when format-as-table', () => {
@ -66,12 +66,12 @@ describe('InfluxDB InfluxQL Visual Editor', () => {
};
assertEditor(
query,
'from[default][select measurement]where[+]' +
'select[field]([value])[mean]()[+]' +
'group by[time]([$__interval])[fill]([null])[+]' +
'timezone[(optional)]order by time[ASC]' +
'limit[(optional)]slimit[(optional)]' +
'format as[table]'
'FROM[default][select measurement]WHERE[+]' +
'SELECT[field]([value])[mean]()[+]' +
'GROUP BY[time]([$__interval])[fill]([null])[+]' +
'TIMEZONE[(optional)]ORDER BY TIME[ASC]' +
'LIMIT[(optional)]SLIMIT[(optional)]' +
'FORMAT AS[table]'
);
});
it('should handle complex query', () => {
@ -145,13 +145,13 @@ describe('InfluxDB InfluxQL Visual Editor', () => {
};
assertEditor(
query,
'from[default][cpu]where[cpu][=][cpu1][AND][cpu][<][cpu3][+]' +
'select[field]([usage_idle])[mean]()[+]' +
'FROM[default][cpu]WHERE[cpu][=][cpu1][AND][cpu][<][cpu3][+]' +
'SELECT[field]([usage_idle])[mean]()[+]' +
'[field]([usage_guest])[median]()[holt_winters_with_fit]([10],[2])[+]' +
'group by[time]([$__interval])[tag]([cpu])[tag]([host])[fill]([null])[+]' +
'timezone[UTC]order by time[DESC]' +
'limit[4]slimit[5]' +
'format as[logs]alias[all i as]'
'GROUP BY[time]([$__interval])[tag]([cpu])[tag]([host])[fill]([null])[+]' +
'TIMEZONE[UTC]ORDER BY TIME[DESC]' +
'LIMIT[4]SLIMIT[5]' +
'FORMAT AS[logs]ALIAS[all i as]'
);
});
});

View File

@ -24,10 +24,11 @@ import {
changeGroupByPart,
} from '../../queryUtils';
import { FormatAsSection } from './FormatAsSection';
import { SectionLabel } from './SectionLabel';
import { SectionFill } from './SectionFill';
import { DEFAULT_RESULT_FORMAT } from '../constants';
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 = {
query: InfluxQuery;
@ -51,15 +52,8 @@ function withTemplateVariableOptions(optionsPromise: Promise<string[]>): Promise
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 => {
const styles = useStyles2(getStyles);
const query = normalizeQuery(props.query);
const { datasource } = props;
const { measurement, policy } = query;
@ -112,7 +106,7 @@ export const Editor = (props: Props): JSX.Element => {
return (
<div>
<SectionWrap initialName="from">
<SegmentSection label="FROM" fill={true}>
<FromSection
policy={policy}
measurement={measurement}
@ -124,7 +118,9 @@ export const Editor = (props: Props): JSX.Element => {
}
onChange={handleFromSectionChange}
/>
<SectionLabel name="where" />
<InlineLabel width="auto" className={styles.inlineLabel}>
WHERE
</InlineLabel>
<TagsSection
tags={query.tags ?? []}
onChange={handleTagsSectionChange}
@ -133,9 +129,9 @@ export const Editor = (props: Props): JSX.Element => {
withTemplateVariableOptions(getTagValues(key, measurement, policy, query.tags ?? [], datasource))
}
/>
</SectionWrap>
</SegmentSection>
{selectLists.map((sel, index) => (
<SectionWrap key={index} initialName={index === 0 ? 'select' : ''}>
<SegmentSection key={index} label={index === 0 ? 'SELECT' : ''} fill={true}>
<PartListSection
parts={sel}
getNewPartOptions={() => Promise.resolve(getNewSelectPartOptions())}
@ -150,9 +146,9 @@ export const Editor = (props: Props): JSX.Element => {
onAppliedChange(removeSelectPart(query, partIndex, index));
}}
/>
</SectionWrap>
</SegmentSection>
))}
<SectionWrap initialName="group by">
<SegmentSection label="GROUP BY" fill={true}>
<PartListSection
parts={groupByList}
getNewPartOptions={() => getNewGroupByPartOptions(query, getTagKeys)}
@ -167,8 +163,8 @@ export const Editor = (props: Props): JSX.Element => {
onAppliedChange(removeGroupByPart(query, partIndex));
}}
/>
</SectionWrap>
<SectionWrap initialName="timezone">
</SegmentSection>
<SegmentSection label="TIMEZONE" fill={true}>
<InputSection
placeholder="(optional)"
value={query.tz}
@ -176,20 +172,22 @@ export const Editor = (props: Props): JSX.Element => {
onAppliedChange({ ...query, tz });
}}
/>
<SectionLabel name="order by time" />
<InlineLabel width="auto" className={styles.inlineLabel}>
ORDER BY TIME
</InlineLabel>
<OrderByTimeSection
value={query.orderByTime === 'DESC' ? 'DESC' : 'ASC' /* FIXME: make this shared with influx_query_model */}
onChange={(v) => {
onAppliedChange({ ...query, orderByTime: v });
}}
/>
</SectionWrap>
</SegmentSection>
{/* 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
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. */}
<SectionWrap initialName="limit">
<SegmentSection label="LIMIT" fill={true}>
<InputSection
placeholder="(optional)"
value={query.limit?.toString()}
@ -197,7 +195,9 @@ export const Editor = (props: Props): JSX.Element => {
onAppliedChange({ ...query, limit });
}}
/>
<SectionLabel name="slimit" />
<InlineLabel width="auto" className={styles.inlineLabel}>
SLIMIT
</InlineLabel>
<InputSection
placeholder="(optional)"
value={query.slimit?.toString()}
@ -205,8 +205,8 @@ export const Editor = (props: Props): JSX.Element => {
onAppliedChange({ ...query, slimit });
}}
/>
</SectionWrap>
<SectionWrap initialName="format as">
</SegmentSection>
<SegmentSection label="FORMAT AS" fill={true}>
<FormatAsSection
format={query.resultFormat ?? DEFAULT_RESULT_FORMAT}
onChange={(format) => {
@ -215,7 +215,9 @@ export const Editor = (props: Props): JSX.Element => {
/>
{query.resultFormat !== 'table' && (
<>
<SectionLabel name="alias" />
<InlineLabel width="auto" className={styles.inlineLabel}>
ALIAS
</InlineLabel>
<InputSection
isWide
placeholder="Naming pattern"
@ -226,7 +228,15 @@ export const Editor = (props: Props): JSX.Element => {
/>
</>
)}
</SectionWrap>
</SegmentSection>
</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>
);