Graphite: Migrate to React (part 6: Remove AngularJS Query Editor) (#37919)

* 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

* Basic integration

* Move creating state to context.tsx

* Update tests

* Rename test

* Clean up dependencies

panel.targets is not needed for interpolation - it happens in the data source itself. It was used only to show query ref in the the dropdown for the segment.

* Update time range when it changes

* Change action name

* Simplify segments cloning

* Remove redundant variable

* Use styles instead of direct css

* Update docs

* Remove angular wrappers

* Remove redundant tests

* Section -> SegmentSection

* Simplify section styling

* Remove redundant div

* Fix unit tests

* Simplify SegmentSection component

* Use theme.spacing

* Use empty label instead of a single space label

* Remove targetFull

It was used in the past two store the query interpolated with sub-queries inside the model and send both to the backed (interpolated and not interpolated). This has been changed though - the logic has been moved away from model to the data source where interpolation happens and now only interpolated query is passed meaning targetFull is not needed anymore.

* Revert "Remove targetFull"

This reverts commit 499f8b33

* Bring back calculating targetFull

* Clean up

* Add missing dep

* Add missing dep in tests

* Fix time range synchronization

* Fix warning message

* Remove unused type

* Synchronize changes to the query

Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
Piotr Jamróz
2021-09-02 10:18:14 +02:00
committed by GitHub
parent f941390631
commit bd95856ff9
14 changed files with 341 additions and 325 deletions

View File

@@ -24,7 +24,6 @@ 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 { GraphiteQueryEditor } from '../plugins/datasource/graphite/components/GraphiteQueryEditor';
const { SecretFormField } = LegacyForms;
@@ -201,7 +200,4 @@ export function registerAngularDirectives() {
['datasource', { watchDepth: 'reference' }],
'onChange',
]);
// Temporal wrappers for Graphite migration
react2AngularDirective('graphiteQueryEditor', GraphiteQueryEditor, ['state', 'dispatch']);
}

View File

@@ -1,26 +1,73 @@
import React from 'react';
import { Dispatch } from 'redux';
import { GraphiteQueryEditorState } from '../state/store';
import { GrafanaTheme2 } from '@grafana/data';
import { actions } from '../state/actions';
import { Button, useStyles2 } from '@grafana/ui';
import { GraphiteQueryEditorContext, GraphiteQueryEditorProps, useDispatch, useGraphiteState } from '../state/context';
import { GraphiteTextEditor } from './GraphiteTextEditor';
import { SeriesSection } from './SeriesSection';
import { GraphiteContext } from '../state/context';
import { FunctionsSection } from './FunctionsSection';
import { css } from '@emotion/css';
type Props = {
state: GraphiteQueryEditorState;
dispatch: Dispatch;
};
export function GraphiteQueryEditor({ dispatch, state }: Props) {
export function GraphiteQueryEditor({
datasource,
onRunQuery,
onChange,
query,
range,
queries,
}: GraphiteQueryEditorProps) {
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>
<GraphiteQueryEditorContext
datasource={datasource}
onRunQuery={onRunQuery}
onChange={onChange}
query={query}
queries={queries}
range={range}
>
<GraphiteQueryEditorContent />
</GraphiteQueryEditorContext>
);
}
function GraphiteQueryEditorContent() {
const dispatch = useDispatch();
const state = useGraphiteState();
const styles = useStyles2(getStyles);
return (
<div className={styles.container}>
<div className={styles.visualEditor}>
{state.target?.textEditor && <GraphiteTextEditor rawQuery={state.target.target} />}
{!state.target?.textEditor && (
<>
<SeriesSection state={state} />
<FunctionsSection functions={state.queryModel?.functions} funcDefs={state.funcDefs!} />
</>
)}
</div>
<Button
className={styles.toggleButton}
icon="pen"
variant="secondary"
onClick={() => {
dispatch(actions.toggleEditorMode());
}}
/>
</div>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
container: css`
display: flex;
`,
visualEditor: css`
flex-grow: 1;
`,
toggleButton: css`
margin-left: ${theme.spacing(0.5)};
`,
};
}

View File

@@ -10,7 +10,7 @@ type Props = {
export function SeriesSection({ state }: Props) {
const sectionContent = state.queryModel?.seriesByTagUsed ? (
<TagsSection tags={state.queryModel?.tags} addTagSegments={state.addTagSegments} state={state} />
<TagsSection tags={state.queryModel?.tags} state={state} />
) : (
<MetricsSection segments={state.segments} state={state} />
);

View File

@@ -7,7 +7,6 @@ import { Button, SegmentAsync, useStyles2 } from '@grafana/ui';
import { actions } from '../state/actions';
import { GrafanaTheme2 } from '@grafana/data';
import { css } from '@emotion/css';
import { mapSegmentsToSelectables } from './helpers';
import { TagEditor } from './TagEditor';
import { debounce } from 'lodash';
import { useDispatch } from '../state/context';
@@ -15,7 +14,6 @@ import { PlayButton } from './PlayButton';
type Props = {
tags: GraphiteTag[];
addTagSegments: GraphiteSegment[];
state: GraphiteQueryEditorState;
};
@@ -25,12 +23,10 @@ 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({ tags, state, addTagSegments }: Props) {
export function TagsSection({ tags, state }: Props) {
const dispatch = useDispatch();
const styles = useStyles2(getStyles);
const newTagsOptions = mapSegmentsToSelectables(addTagSegments || []);
// Options 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)
const getTagsAsSegmentsOptions = useCallback(
@@ -48,7 +44,7 @@ export function TagsSection({ tags, state, addTagSegments }: Props) {
{tags.map((tag, index) => {
return <TagEditor key={index} tagIndex={index} tag={tag} state={state} />;
})}
{newTagsOptions.length && (
{tags.length && (
<SegmentAsync<GraphiteSegment>
inputMinWidth={150}
onChange={(value) => {

View File

@@ -15,7 +15,7 @@ export type GraphiteTag = {
value: string;
};
type GraphiteTarget = {
export type GraphiteTarget = {
refId: string | number;
target: string;
/**

View File

@@ -1,15 +1,15 @@
import { GraphiteDatasource } from './datasource';
import { GraphiteQueryCtrl } from './query_ctrl';
import { DataSourcePlugin } from '@grafana/data';
import { ConfigEditor } from './configuration/ConfigEditor';
import { MetricTankMetaInspector } from './components/MetricTankMetaInspector';
import { GraphiteQueryEditor } from './components/GraphiteQueryEditor';
class AnnotationsQueryCtrl {
static templateUrl = 'partials/annotations.editor.html';
}
export const plugin = new DataSourcePlugin(GraphiteDatasource)
.setQueryCtrl(GraphiteQueryCtrl)
.setQueryEditor(GraphiteQueryEditor)
.setConfigEditor(ConfigEditor)
.setMetadataInspector(MetricTankMetaInspector)
.setAnnotationQueryCtrl(AnnotationsQueryCtrl);

View File

@@ -1,81 +0,0 @@
import GraphiteQuery from './graphite_query';
import { QueryCtrl } from 'app/plugins/sdk';
import { auto } from 'angular';
import { TemplateSrv } from '@grafana/runtime';
import { actions } from './state/actions';
import { createStore, GraphiteQueryEditorState } from './state/store';
import { GraphiteActionDispatcher, GraphiteQueryEditorAngularDependencies } from './types';
/**
* @deprecated Moved to state/store
*
* Note: methods marked with WIP are kept for easier diffing with previous changes. They will be removed when
* GraphiteQueryCtrl is replaced with a react component.
*/
export class GraphiteQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
declare queryModel: GraphiteQuery;
segments: any[] = [];
addTagSegments: any[] = [];
declare removeTagValue: string;
supportsTags = false;
paused = false;
state: GraphiteQueryEditorState;
readonly dispatch: GraphiteActionDispatcher;
/** @ngInject */
constructor(
$scope: any,
$injector: auto.IInjectorService,
private uiSegmentSrv: any,
private templateSrv: TemplateSrv
) {
super($scope, $injector);
// This controller will be removed once it's root partial (query.editor.html) renders only React components.
// All component will be wrapped in ReactQueryEditor receiving DataSourceApi in QueryRow.renderQueryEditor
// The init() action will be removed and the store will be created in ReactQueryEditor. Note that properties
// passed to React component in QueryRow.renderQueryEditor are different than properties passed to Angular editor
// and will be mapped/provided in a way described below:
const deps = {
// WIP: to be removed. It's not passed to ReactQueryEditor but it's used only to:
// - get refId of the query (refId be passed in query property),
// - and to refresh changes (this will be handled by onChange passed to ReactQueryEditor)
// - it's needed to get other targets to interpolate the query (this will be added in QueryRow)
panelCtrl: this.panelCtrl,
// WIP: to be replaced with query property passed to ReactQueryEditor
target: this.target,
// WIP: same object will be passed to ReactQueryEditor
datasource: this.datasource,
// This is used to create view models for Angular <metric-segment> component (view models are MetricSegment objects)
// It will be simplified to produce data needed by React <SegmentAsync/> component
uiSegmentSrv: this.uiSegmentSrv,
// WIP: will be replaced with:
// import { getTemplateSrv } from 'app/features/templating/template_srv';
templateSrv: this.templateSrv,
};
const [dispatch, state] = createStore((state) => {
this.state = state;
// HACK: inefficient but not invoked frequently. It's needed to inform angular watcher about state changes
// for state shared between React/AngularJS. Actions invoked from React component will not mark the scope
// as dirty and the view won't be updated. It has to happen manually on each state change.
this.$scope.$digest();
});
this.state = state;
this.dispatch = dispatch;
this.dispatch(actions.init(deps as GraphiteQueryEditorAngularDependencies));
}
async toggleEditorMode() {
await this.dispatch(actions.toggleEditorMode());
}
}

View File

@@ -1,12 +1,11 @@
import { dispatch } from 'app/store/store';
import { uiSegmentSrv } from 'app/core/services/segment_srv';
import gfunc from '../gfunc';
import { GraphiteQueryCtrl } from '../query_ctrl';
import { TemplateSrvStub } from 'test/specs/helpers';
import { silenceConsoleOutput } from 'test/core/utils/silenceConsoleOutput';
import { actions } from '../state/actions';
import { getAltSegmentsSelectables, getTagsSelectables, getTagsAsSegmentsSelectables } from '../state/providers';
import { GraphiteSegment } from '../types';
import { createStore } from '../state/store';
jest.mock('app/core/utils/promiseToDigest', () => ({
promiseToDigest: (scope: any) => {
@@ -23,13 +22,13 @@ const mockDispatch = dispatch as jest.Mock;
* Simulate switching to text editor, changing the query and switching back to visual editor
*/
async function changeTarget(ctx: any, target: string): Promise<void> {
await ctx.ctrl.toggleEditorMode();
await ctx.ctrl.dispatch(actions.updateQuery({ query: target }));
await ctx.ctrl.dispatch(actions.runQuery());
await ctx.ctrl.toggleEditorMode();
await ctx.dispatch(actions.toggleEditorMode());
await ctx.dispatch(actions.updateQuery({ query: target }));
await ctx.dispatch(actions.runQuery());
await ctx.dispatch(actions.toggleEditorMode());
}
describe('GraphiteQueryCtrl', () => {
describe('Graphite actions', async () => {
const ctx = {
datasource: {
metricFindQuery: jest.fn(() => Promise.resolve([])),
@@ -40,32 +39,26 @@ describe('GraphiteQueryCtrl', () => {
getTagsAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
},
target: { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' },
panelCtrl: {
refresh: jest.fn(),
},
} as any;
ctx.panelCtrl.panel = {
targets: [ctx.target],
};
beforeEach(async () => {
jest.clearAllMocks();
GraphiteQueryCtrl.prototype.target = ctx.target;
GraphiteQueryCtrl.prototype.datasource = ctx.datasource;
GraphiteQueryCtrl.prototype.panelCtrl = ctx.panelCtrl;
ctx.ctrl = new GraphiteQueryCtrl(
{ $digest: jest.fn() },
{} as any,
//@ts-ignore
new uiSegmentSrv({ trustAsHtml: (html) => html }, { highlightVariablesAsHtml: () => {} }),
//@ts-ignore
new TemplateSrvStub()
ctx.state = null;
ctx.dispatch = createStore((state) => {
ctx.state = state;
});
await ctx.dispatch(
actions.init({
datasource: ctx.datasource,
target: ctx.target,
refresh: jest.fn(),
queries: [],
//@ts-ignore
templateSrv: new TemplateSrvStub(),
})
);
// resolve async code called by the constructor
await Promise.resolve();
});
describe('init', () => {
@@ -74,19 +67,19 @@ describe('GraphiteQueryCtrl', () => {
});
it('should not delete last segment if no metrics are found', () => {
expect(ctx.ctrl.state.segments[2].value).not.toBe('select metric');
expect(ctx.ctrl.state.segments[2].value).toBe('*');
expect(ctx.state.segments[2].value).not.toBe('select metric');
expect(ctx.state.segments[2].value).toBe('*');
});
it('should parse expression and build function model', () => {
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
expect(ctx.state.queryModel.functions.length).toBe(2);
});
});
describe('when toggling edit mode to raw and back again', () => {
beforeEach(async () => {
await ctx.ctrl.toggleEditorMode();
await ctx.ctrl.toggleEditorMode();
await ctx.dispatch(actions.toggleEditorMode());
await ctx.dispatch(actions.toggleEditorMode());
});
it('should validate metric key exists', () => {
@@ -95,20 +88,20 @@ describe('GraphiteQueryCtrl', () => {
});
it('should delete last segment if no metrics are found', () => {
expect(ctx.ctrl.state.segments[0].value).toBe('test');
expect(ctx.ctrl.state.segments[1].value).toBe('prod');
expect(ctx.ctrl.state.segments[2].value).toBe('select metric');
expect(ctx.state.segments[0].value).toBe('test');
expect(ctx.state.segments[1].value).toBe('prod');
expect(ctx.state.segments[2].value).toBe('select metric');
});
it('should parse expression and build function model', () => {
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
expect(ctx.state.queryModel.functions.length).toBe(2);
});
});
describe('when middle segment value of test.prod.* is changed', () => {
beforeEach(async () => {
const segment: GraphiteSegment = { type: 'metric', value: 'test', expandable: true };
await ctx.ctrl.dispatch(actions.segmentValueChanged({ segment: segment, index: 1 }));
await ctx.dispatch(actions.segmentValueChanged({ segment: segment, index: 1 }));
});
it('should validate metric key exists', () => {
@@ -117,83 +110,99 @@ describe('GraphiteQueryCtrl', () => {
});
it('should delete last segment if no metrics are found', () => {
expect(ctx.ctrl.state.segments[0].value).toBe('test');
expect(ctx.ctrl.state.segments[1].value).toBe('test');
expect(ctx.ctrl.state.segments[2].value).toBe('select metric');
expect(ctx.state.segments[0].value).toBe('test');
expect(ctx.state.segments[1].value).toBe('test');
expect(ctx.state.segments[2].value).toBe('select metric');
});
it('should parse expression and build function model', () => {
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
expect(ctx.state.queryModel.functions.length).toBe(2);
});
});
describe('when adding function', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, 'test.prod.*.count');
await ctx.ctrl.dispatch(actions.addFunction({ name: 'aliasByNode' }));
await ctx.dispatch(actions.addFunction({ name: 'aliasByNode' }));
});
it('should add function with correct node number', () => {
expect(ctx.ctrl.state.queryModel.functions[0].params[0]).toBe(2);
expect(ctx.state.queryModel.functions[0].params[0]).toBe(2);
});
it('should update target', () => {
expect(ctx.ctrl.state.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
expect(ctx.state.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
});
it('should call refresh', () => {
expect(ctx.panelCtrl.refresh).toHaveBeenCalled();
expect(ctx.state.refresh).toHaveBeenCalled();
});
});
describe('when adding function before any metric segment', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
await changeTarget(ctx, '');
await ctx.ctrl.dispatch(actions.addFunction({ name: 'asPercent' }));
await ctx.dispatch(actions.addFunction({ name: 'asPercent' }));
});
it('should add function and remove select metric link', () => {
expect(ctx.ctrl.state.segments.length).toBe(0);
expect(ctx.state.segments.length).toBe(0);
});
});
describe('when initializing a target with single param func using variable', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([]);
await changeTarget(ctx, 'movingAverage(prod.count, $var)');
});
it('should add 2 segments', () => {
expect(ctx.ctrl.state.segments.length).toBe(2);
expect(ctx.state.segments.length).toBe(2);
});
it('should add function param', () => {
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
expect(ctx.state.queryModel.functions[0].params.length).toBe(1);
});
});
describe('when changing the query from the outside', () => {
it('should update the model', async () => {
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ text: '*' }]);
await changeTarget(ctx, 'my.query.*');
expect(ctx.state.target.target).toBe('my.query.*');
expect(ctx.state.segments[0].value).toBe('my');
expect(ctx.state.segments[1].value).toBe('query');
await ctx.dispatch(actions.queryChanged({ target: 'new.metrics.*', refId: 'A' }));
expect(ctx.state.target.target).toBe('new.metrics.*');
expect(ctx.state.segments[0].value).toBe('new');
expect(ctx.state.segments[1].value).toBe('metrics');
});
});
describe('when initializing target without metric expression and function with series-ref', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([]);
await changeTarget(ctx, 'asPercent(metric.node.count, #A)');
});
it('should add segments', () => {
expect(ctx.ctrl.state.segments.length).toBe(3);
expect(ctx.state.segments.length).toBe(3);
});
it('should have correct func params', () => {
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
expect(ctx.state.queryModel.functions[0].params.length).toBe(1);
});
});
describe('when getting altSegments and metricFindQuery returns empty array', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([]);
await changeTarget(ctx, 'test.count');
ctx.altSegments = await getAltSegmentsSelectables(ctx.ctrl.state, 1, '');
ctx.altSegments = await getAltSegmentsSelectables(ctx.state, 1, '');
});
it('should have no segments', () => {
@@ -204,8 +213,8 @@ describe('GraphiteQueryCtrl', () => {
describe('when autocomplete for metric names is not available', () => {
silenceConsoleOutput();
beforeEach(() => {
ctx.ctrl.state.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
ctx.ctrl.state.datasource.metricFindQuery = jest.fn().mockReturnValue(
ctx.state.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
ctx.state.datasource.metricFindQuery = jest.fn().mockReturnValue(
new Promise(() => {
throw new Error();
})
@@ -214,7 +223,7 @@ describe('GraphiteQueryCtrl', () => {
it('getAltSegmentsSelectables should handle autocomplete errors', async () => {
await expect(async () => {
await getAltSegmentsSelectables(ctx.ctrl.state, 0, 'any');
await getAltSegmentsSelectables(ctx.state, 0, 'any');
expect(mockDispatch).toBeCalledWith(
expect.objectContaining({
type: 'appNotifications/notifyApp',
@@ -224,10 +233,10 @@ describe('GraphiteQueryCtrl', () => {
});
it('getAltSegmentsSelectables should display the error message only once', async () => {
await getAltSegmentsSelectables(ctx.ctrl.state, 0, 'any');
await getAltSegmentsSelectables(ctx.state, 0, 'any');
expect(mockDispatch.mock.calls.length).toBe(1);
await getAltSegmentsSelectables(ctx.ctrl.state, 0, 'any');
await getAltSegmentsSelectables(ctx.state, 0, 'any');
expect(mockDispatch.mock.calls.length).toBe(1);
});
});
@@ -245,7 +254,7 @@ describe('GraphiteQueryCtrl', () => {
it('getTagsSelectables should handle autocomplete errors', async () => {
await expect(async () => {
await getTagsSelectables(ctx.ctrl.state, 0, 'any');
await getTagsSelectables(ctx.state, 0, 'any');
expect(mockDispatch).toBeCalledWith(
expect.objectContaining({
type: 'appNotifications/notifyApp',
@@ -255,16 +264,16 @@ describe('GraphiteQueryCtrl', () => {
});
it('getTagsSelectables should display the error message only once', async () => {
await getTagsSelectables(ctx.ctrl.state, 0, 'any');
await getTagsSelectables(ctx.state, 0, 'any');
expect(mockDispatch.mock.calls.length).toBe(1);
await getTagsSelectables(ctx.ctrl.state, 0, 'any');
await getTagsSelectables(ctx.state, 0, 'any');
expect(mockDispatch.mock.calls.length).toBe(1);
});
it('getTagsAsSegmentsSelectables should handle autocomplete errors', async () => {
await expect(async () => {
await getTagsAsSegmentsSelectables(ctx.ctrl.state, 'any');
await getTagsAsSegmentsSelectables(ctx.state, 'any');
expect(mockDispatch).toBeCalledWith(
expect.objectContaining({
type: 'appNotifications/notifyApp',
@@ -274,10 +283,10 @@ describe('GraphiteQueryCtrl', () => {
});
it('getTagsAsSegmentsSelectables should display the error message only once', async () => {
await getTagsAsSegmentsSelectables(ctx.ctrl.state, 'any');
await getTagsAsSegmentsSelectables(ctx.state, 'any');
expect(mockDispatch.mock.calls.length).toBe(1);
await getTagsAsSegmentsSelectables(ctx.ctrl.state, 'any');
await getTagsAsSegmentsSelectables(ctx.state, 'any');
expect(mockDispatch.mock.calls.length).toBe(1);
});
});
@@ -285,96 +294,96 @@ describe('GraphiteQueryCtrl', () => {
describe('targetChanged', () => {
beforeEach(async () => {
const newQuery = 'aliasByNode(scaleToSeconds(test.prod.*, 1), 2)';
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, newQuery);
});
it('should rebuild target after expression model', () => {
expect(ctx.ctrl.state.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
expect(ctx.state.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
});
it('should call panelCtrl.refresh', () => {
expect(ctx.panelCtrl.refresh).toHaveBeenCalled();
it('should call refresh', () => {
expect(ctx.state.refresh).toHaveBeenCalled();
});
});
describe('when updating targets with nested query', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, 'scaleToSeconds(#A, 60)');
});
it('should add function params', () => {
expect(ctx.ctrl.state.queryModel.segments.length).toBe(1);
expect(ctx.ctrl.state.queryModel.segments[0].value).toBe('#A');
expect(ctx.state.queryModel.segments.length).toBe(1);
expect(ctx.state.queryModel.segments[0].value).toBe('#A');
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
expect(ctx.ctrl.state.queryModel.functions[0].params[0]).toBe(60);
expect(ctx.state.queryModel.functions[0].params.length).toBe(1);
expect(ctx.state.queryModel.functions[0].params[0]).toBe(60);
});
it('target should remain the same', () => {
expect(ctx.ctrl.state.target.target).toBe('scaleToSeconds(#A, 60)');
expect(ctx.state.target.target).toBe('scaleToSeconds(#A, 60)');
});
it('targetFull should include nested queries', async () => {
ctx.ctrl.state.panelCtrl.panel.targets = [
{
target: 'nested.query.count',
refId: 'A',
},
];
await ctx.dispatch(
actions.queriesChanged([
{
target: 'nested.query.count',
refId: 'A',
},
])
);
await changeTarget(ctx, ctx.target.target);
expect(ctx.state.target.target).toBe('scaleToSeconds(#A, 60)');
expect(ctx.ctrl.state.target.target).toBe('scaleToSeconds(#A, 60)');
expect(ctx.ctrl.state.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
expect(ctx.state.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
});
});
describe('when updating target used in other query', () => {
beforeEach(async () => {
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.ctrl.target.refId = 'A';
ctx.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.target.refId = 'A';
await changeTarget(ctx, 'metrics.foo.count');
ctx.ctrl.state.panelCtrl.panel.targets = [ctx.ctrl.target, { target: 'sumSeries(#A)', refId: 'B' }];
ctx.state.queries = [ctx.state.target, { target: 'sumSeries(#A)', refId: 'B' }];
await changeTarget(ctx, 'metrics.bar.count');
});
it('targetFull of other query should update', () => {
expect(ctx.ctrl.state.panelCtrl.panel.targets[1].targetFull).toBe('sumSeries(metrics.bar.count)');
expect(ctx.state.queries[1].targetFull).toBe('sumSeries(metrics.bar.count)');
});
});
describe('when adding seriesByTag function', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, '');
await ctx.ctrl.dispatch(actions.addFunction({ name: 'seriesByTag' }));
await ctx.dispatch(actions.addFunction({ name: 'seriesByTag' }));
});
it('should update functions', () => {
expect(ctx.ctrl.state.queryModel.getSeriesByTagFuncIndex()).toBe(0);
expect(ctx.state.queryModel.getSeriesByTagFuncIndex()).toBe(0);
});
it('should update seriesByTagUsed flag', () => {
expect(ctx.ctrl.state.queryModel.seriesByTagUsed).toBe(true);
expect(ctx.state.queryModel.seriesByTagUsed).toBe(true);
});
it('should update target', () => {
expect(ctx.ctrl.state.target.target).toBe('seriesByTag()');
expect(ctx.state.target.target).toBe('seriesByTag()');
});
it('should call refresh', () => {
expect(ctx.panelCtrl.refresh).toHaveBeenCalled();
expect(ctx.state.refresh).toHaveBeenCalled();
});
});
describe('when parsing seriesByTag function', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
});
@@ -383,39 +392,33 @@ describe('GraphiteQueryCtrl', () => {
{ key: 'tag1', operator: '=', value: 'value1' },
{ key: 'tag2', operator: '!=~', value: 'value2' },
];
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
});
it('should add plus button', () => {
expect(ctx.ctrl.state.addTagSegments.length).toBe(1);
expect(ctx.state.queryModel.tags).toEqual(expected);
});
});
describe('when tag added', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, 'seriesByTag()');
await ctx.ctrl.dispatch(actions.addNewTag({ segment: { value: 'tag1' } }));
await ctx.dispatch(actions.addNewTag({ segment: { value: 'tag1' } }));
});
it('should update tags with default value', () => {
const expected = [{ key: 'tag1', operator: '=', value: '' }];
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
expect(ctx.state.queryModel.tags).toEqual(expected);
});
it('should update target', () => {
const expected = "seriesByTag('tag1=')";
expect(ctx.ctrl.state.target.target).toEqual(expected);
expect(ctx.state.target.target).toEqual(expected);
});
});
describe('when tag changed', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
await ctx.ctrl.dispatch(
actions.tagChanged({ tag: { key: 'tag1', operator: '=', value: 'new_value' }, index: 0 })
);
await ctx.dispatch(actions.tagChanged({ tag: { key: 'tag1', operator: '=', value: 'new_value' }, index: 0 }));
});
it('should update tags', () => {
@@ -423,32 +426,32 @@ describe('GraphiteQueryCtrl', () => {
{ key: 'tag1', operator: '=', value: 'new_value' },
{ key: 'tag2', operator: '!=~', value: 'value2' },
];
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
expect(ctx.state.queryModel.tags).toEqual(expected);
});
it('should update target', () => {
const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
expect(ctx.ctrl.state.target.target).toEqual(expected);
expect(ctx.state.target.target).toEqual(expected);
});
});
describe('when tag removed', () => {
beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
await ctx.ctrl.dispatch(
actions.tagChanged({ tag: { key: ctx.ctrl.state.removeTagValue, operator: '=', value: '' }, index: 0 })
await ctx.dispatch(
actions.tagChanged({ tag: { key: ctx.state.removeTagValue, operator: '=', value: '' }, index: 0 })
);
});
it('should update tags', () => {
const expected = [{ key: 'tag2', operator: '!=~', value: 'value2' }];
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
expect(ctx.state.queryModel.tags).toEqual(expected);
});
it('should update target', () => {
const expected = "seriesByTag('tag2!=~value2')";
expect(ctx.ctrl.state.target.target).toEqual(expected);
expect(ctx.state.target.target).toEqual(expected);
});
});
});

View File

@@ -1,15 +1,20 @@
import { GraphiteQueryEditorAngularDependencies, GraphiteSegment, GraphiteTag } from '../types';
import { GraphiteQuery, GraphiteQueryEditorDependencies, GraphiteSegment, GraphiteTag } from '../types';
import { createAction } from '@reduxjs/toolkit';
import { FuncInstance } from '../gfunc';
import { TimeRange } from '@grafana/data';
/**
* List of possible actions changing the state of QueryEditor
*/
const init = createAction<GraphiteQueryEditorDependencies>('init');
/**
* This is used only during the transition to react. It will be removed after migrating all components.
* Synchronise editor dependencies with internal state.
*/
const init = createAction<GraphiteQueryEditorAngularDependencies>('init');
const timeRangeChanged = createAction<TimeRange | undefined>('time-range-changed');
const queriesChanged = createAction<GraphiteQuery[] | undefined>('queries-changed');
const queryChanged = createAction<GraphiteQuery>('query-changed');
// Metrics & Tags
const segmentValueChanged = createAction<{ segment: GraphiteSegment | string; index: number }>('segment-value-changed');
@@ -32,6 +37,9 @@ const toggleEditorMode = createAction('toggle-editor');
export const actions = {
init,
timeRangeChanged,
queriesChanged,
queryChanged,
segmentValueChanged,
tagChanged,
addNewTag,

View File

@@ -1,16 +1,85 @@
import React, { createContext, Dispatch, PropsWithChildren, useContext } from 'react';
import React, { createContext, Dispatch, PropsWithChildren, useContext, useEffect, useMemo, useState } from 'react';
import { AnyAction } from '@reduxjs/toolkit';
type Props = {
dispatch: Dispatch<AnyAction>;
};
import { QueryEditorProps } from '@grafana/data';
import { GraphiteDatasource } from '../datasource';
import { GraphiteOptions, GraphiteQuery } from '../types';
import { createStore, GraphiteQueryEditorState } from './store';
import { getTemplateSrv } from 'app/features/templating/template_srv';
import { actions } from './actions';
import { usePrevious } from 'react-use';
const DispatchContext = createContext<Dispatch<AnyAction>>({} as Dispatch<AnyAction>);
const GraphiteStateContext = createContext<GraphiteQueryEditorState>({} as GraphiteQueryEditorState);
export const useDispatch = () => {
return useContext(DispatchContext);
};
export const GraphiteContext = ({ children, dispatch }: PropsWithChildren<Props>) => {
return <DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider>;
export const useGraphiteState = () => {
return useContext(GraphiteStateContext);
};
export type GraphiteQueryEditorProps = QueryEditorProps<GraphiteDatasource, GraphiteQuery, GraphiteOptions>;
export const GraphiteQueryEditorContext = ({
datasource,
onRunQuery,
onChange,
query,
queries,
range,
children,
}: PropsWithChildren<GraphiteQueryEditorProps>) => {
const [state, setState] = useState<GraphiteQueryEditorState>();
const dispatch = useMemo(() => {
return createStore((state) => {
setState(state);
});
}, []);
// synchronise changes provided in props with editor's state
const previousRange = usePrevious(range);
useEffect(() => {
if (previousRange?.raw !== range?.raw) {
dispatch(actions.timeRangeChanged(range));
}
}, [dispatch, range, previousRange]);
useEffect(() => {
if (state) {
dispatch(actions.queriesChanged(queries));
}
}, [dispatch, queries]);
useEffect(() => {
if (state && state.target?.target !== query.target) {
dispatch(actions.queryChanged(query));
}
}, [dispatch, query]);
if (!state) {
dispatch(
actions.init({
target: query,
datasource: datasource,
range: range,
templateSrv: getTemplateSrv(),
// list of queries is passed only when the editor is in Dashboards. This is to allow interpolation
// of sub-queries which are stored in "targetFull" property used by alerting in the backend.
queries: queries || [],
refresh: (target: string) => {
onChange({ ...query, target: target });
onRunQuery();
},
})
);
return null;
} else {
return (
<GraphiteStateContext.Provider value={state}>
<DispatchContext.Provider value={dispatch}>{children}</DispatchContext.Provider>
</GraphiteStateContext.Provider>
);
}
};

View File

@@ -1,5 +1,5 @@
import { GraphiteQueryEditorState } from './store';
import { map } from 'lodash';
import { clone } from 'lodash';
import { dispatch } from '../../../../store/store';
import { notifyApp } from '../../../../core/reducers/appNotification';
import { createErrorNotification } from '../../../../core/copy/appNotification';
@@ -31,17 +31,12 @@ export async function parseTarget(state: GraphiteQueryEditorState): Promise<void
* Create segments out of the current metric path + add "select metrics" if it's possible to add more to the path
*/
export async function buildSegments(state: GraphiteQueryEditorState, modifyLastSegment = true): Promise<void> {
state.segments = map(state.queryModel.segments, (segment) => {
return state.uiSegmentSrv.newSegment(segment);
});
// Start with a shallow copy from the model, then check if "select metric" segment should be added at the end
state.segments = clone(state.queryModel.segments);
const checkOtherSegmentsIndex = state.queryModel.checkOtherSegmentsIndex || 0;
await checkOtherSegments(state, checkOtherSegmentsIndex, modifyLastSegment);
if (state.queryModel.seriesByTagUsed) {
fixTagSegments(state);
}
}
/**
@@ -49,7 +44,7 @@ export async function buildSegments(state: GraphiteQueryEditorState, modifyLastS
*/
export function addSelectMetricSegment(state: GraphiteQueryEditorState): void {
state.queryModel.addSelectMetricSegment();
state.segments.push(state.uiSegmentSrv.newSelectMetric());
state.segments.push({ value: 'select metric', fake: true });
}
/**
@@ -139,14 +134,6 @@ export function smartlyHandleNewAliasByNode(state: GraphiteQueryEditorState, fun
}
}
/**
* Add "+" button for adding tags once at least one tag is selected
*/
export function fixTagSegments(state: GraphiteQueryEditorState): void {
// Adding tag with the same name as just removed works incorrectly if single segment is used (instead of array)
state.addTagSegments = [state.uiSegmentSrv.newPlusButton()];
}
/**
* Pauses running the query to allow selecting tag value. This is to prevent getting errors if the query is run
* for a tag with no selected value.
@@ -165,10 +152,10 @@ export function handleTargetChanged(state: GraphiteQueryEditorState): void {
}
const oldTarget = state.queryModel.target.target;
state.queryModel.updateModelTarget(state.panelCtrl.panel.targets);
state.queryModel.updateModelTarget(state.queries);
if (state.queryModel.target.target !== oldTarget && !state.paused) {
state.panelCtrl.refresh();
state.refresh(state.target.target);
}
}

View File

@@ -31,17 +31,17 @@ async function getAltSegments(
query = state.queryModel.getSegmentPathUpTo(index) + '.' + query;
}
const options = {
range: state.panelCtrl.range,
range: state.range,
requestId: 'get-alt-segments',
};
try {
const segments = await state.datasource.metricFindQuery(query, options);
const altSegments = map(segments, (segment) => {
return state.uiSegmentSrv.newSegment({
const altSegments: GraphiteSegment[] = map(segments, (segment) => {
return {
value: segment.text,
expandable: segment.expandable,
});
};
});
if (index > 0 && altSegments.length === 0) {
@@ -50,34 +50,30 @@ async function getAltSegments(
// add query references
if (index === 0) {
eachRight(state.panelCtrl.panel.targets, (target) => {
eachRight(state.queries, (target) => {
if (target.refId === state.queryModel.target.refId) {
return;
}
altSegments.unshift(
state.uiSegmentSrv.newSegment({
type: 'series-ref',
value: '#' + target.refId,
expandable: false,
})
);
altSegments.unshift({
type: 'series-ref',
value: '#' + target.refId,
expandable: false,
});
});
}
// add template variables
eachRight(state.templateSrv.getVariables(), (variable) => {
altSegments.unshift(
state.uiSegmentSrv.newSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
})
);
altSegments.unshift({
type: 'template',
value: '$' + variable.name,
expandable: true,
});
});
// add wildcard option
altSegments.unshift(state.uiSegmentSrv.newSegment('*'));
altSegments.unshift({ value: '*', expandable: true });
if (state.supportsTags && index === 0) {
removeTaggedEntry(altSegments);
@@ -140,11 +136,11 @@ async function getTagsAsSegments(state: GraphiteQueryEditorState, tagPrefix: str
const tagExpressions = state.queryModel.renderTagExpressions();
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix);
tagsAsSegments = map(values, (val) => {
return state.uiSegmentSrv.newSegment({
return {
value: val.text,
type: 'tag',
expandable: false,
});
};
});
} catch (err) {
tagsAsSegments = [];

View File

@@ -1,5 +1,5 @@
import GraphiteQuery from '../graphite_query';
import { GraphiteActionDispatcher, GraphiteSegment, GraphiteTagOperator } from '../types';
import GraphiteQuery, { GraphiteTarget } from '../graphite_query';
import { GraphiteSegment, GraphiteTagOperator } from '../types';
import { GraphiteDatasource } from '../datasource';
import { TemplateSrv } from '../../../../features/templating/template_srv';
import { actions } from './actions';
@@ -9,7 +9,6 @@ import {
buildSegments,
checkOtherSegments,
emptySegments,
fixTagSegments,
handleTargetChanged,
parseTarget,
pause,
@@ -17,34 +16,28 @@ import {
smartlyHandleNewAliasByNode,
spliceSegments,
} from './helpers';
import { Action } from 'redux';
import { Action, Dispatch } from 'redux';
import { FuncDefs } from '../gfunc';
import { AnyAction } from '@reduxjs/toolkit';
import { DataQuery, TimeRange } from '@grafana/data';
export type GraphiteQueryEditorState = {
/**
* Extra segment with plus button when tags are rendered
*/
addTagSegments: GraphiteSegment[];
// external dependencies
datasource: GraphiteDatasource;
target: GraphiteTarget;
refresh: (target: string) => void;
queries?: DataQuery[];
templateSrv: TemplateSrv;
range?: TimeRange;
// internal
supportsTags: boolean;
paused: boolean;
removeTagValue: string;
datasource: GraphiteDatasource;
uiSegmentSrv: any;
templateSrv: TemplateSrv;
panelCtrl: any;
target: { target: string; textEditor: boolean };
funcDefs: FuncDefs | null;
segments: GraphiteSegment[];
queryModel: GraphiteQuery;
error: Error | null;
tagsAutoCompleteErrorShown: boolean;
metricAutoCompleteErrorShown: boolean;
};
@@ -66,10 +59,23 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
paused: false,
removeTagValue: '-- remove tag --',
funcDefs: deps.datasource.funcDefs,
queries: deps.queries,
};
await buildSegments(state, false);
}
if (actions.timeRangeChanged.match(action)) {
state.range = action.payload;
}
if (actions.queriesChanged.match(action)) {
state.queries = action.payload;
handleTargetChanged(state);
}
if (actions.queryChanged.match(action)) {
state.target.target = action.payload.target || '';
await parseTarget(state);
handleTargetChanged(state);
}
if (actions.segmentValueChanged.match(action)) {
const { segment: segmentOrString, index: segmentIndex } = action.payload;
@@ -125,11 +131,10 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' };
state.queryModel.addTag(newTag);
handleTargetChanged(state);
fixTagSegments(state);
}
if (actions.unpause.match(action)) {
state.paused = false;
state.panelCtrl.refresh();
state.refresh(state.target.target);
}
if (actions.addFunction.match(action)) {
const newFunc = state.datasource.createFuncInstance(action.payload.name, {
@@ -170,9 +175,7 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
handleTargetChanged(state);
}
if (actions.runQuery.match(action)) {
// handleTargetChanged() builds target from segments/tags/functions only,
// it doesn't handle refresh when target is change explicitly
state.panelCtrl.refresh();
state.refresh(state.target.target);
}
if (actions.toggleEditorMode.match(action)) {
state.target.textEditor = !state.target.textEditor;
@@ -182,15 +185,13 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
return { ...state };
};
export const createStore = (
onChange: (state: GraphiteQueryEditorState) => void
): [GraphiteActionDispatcher, GraphiteQueryEditorState] => {
export const createStore = (onChange: (state: GraphiteQueryEditorState) => void): Dispatch<AnyAction> => {
let state = {} as GraphiteQueryEditorState;
const dispatch = async (action: Action) => {
const dispatch = async (action: AnyAction) => {
state = await reducer(action, state);
onChange(state);
};
return [dispatch, state];
return dispatch as Dispatch<AnyAction>;
};

View File

@@ -1,4 +1,4 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { DataQuery, DataSourceJsonData, TimeRange } from '@grafana/data';
import { GraphiteDatasource } from './datasource';
import { TemplateSrv } from '../../../features/templating/template_srv';
@@ -58,7 +58,7 @@ export type GraphiteMetricLokiMatcher = {
export type GraphiteSegment = {
value: string;
type?: 'tag' | 'metric' | 'series-ref';
type?: 'tag' | 'metric' | 'series-ref' | 'template';
expandable?: boolean;
fake?: boolean;
};
@@ -71,17 +71,11 @@ export type GraphiteTag = {
value: string;
};
export type GraphiteActionDispatcher = (action: any) => Promise<void>;
export type GraphiteQueryEditorAngularDependencies = {
panelCtrl: any;
export type GraphiteQueryEditorDependencies = {
target: any;
datasource: GraphiteDatasource;
uiSegmentSrv: any;
range?: TimeRange;
templateSrv: TemplateSrv;
};
export type AngularDropdownOptions = {
text: string;
value: string;
queries: DataQuery[];
refresh: (target: string) => void;
};