mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -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)};
|
||||
`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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} />
|
||||
);
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -15,7 +15,7 @@ export type GraphiteTag = {
|
||||
value: string;
|
||||
};
|
||||
|
||||
type GraphiteTarget = {
|
||||
export type GraphiteTarget = {
|
||||
refId: string | number;
|
||||
target: string;
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user