From bd95856ff97b535326e756ef7356c93c3805e17a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Jamr=C3=B3z?= Date: Thu, 2 Sep 2021 10:18:14 +0200 Subject: [PATCH] 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 * Update public/app/plugins/datasource/graphite/state/helpers.ts Co-authored-by: Giordano Ricci * Update public/app/plugins/datasource/graphite/state/helpers.ts Co-authored-by: Giordano Ricci * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci * 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 --- public/app/core/angular_wrappers.ts | 4 - .../components/GraphiteQueryEditor.tsx | 83 ++++-- .../graphite/components/SeriesSection.tsx | 2 +- .../graphite/components/TagsSection.tsx | 8 +- .../datasource/graphite/graphite_query.ts | 2 +- .../app/plugins/datasource/graphite/module.ts | 4 +- .../plugins/datasource/graphite/query_ctrl.ts | 81 ------ .../{query_ctrl.test.ts => store.test.ts} | 241 +++++++++--------- .../datasource/graphite/state/actions.ts | 14 +- .../datasource/graphite/state/context.tsx | 83 +++++- .../datasource/graphite/state/helpers.ts | 25 +- .../datasource/graphite/state/providers.ts | 40 ++- .../datasource/graphite/state/store.ts | 61 ++--- .../app/plugins/datasource/graphite/types.ts | 18 +- 14 files changed, 341 insertions(+), 325 deletions(-) delete mode 100644 public/app/plugins/datasource/graphite/query_ctrl.ts rename public/app/plugins/datasource/graphite/specs/{query_ctrl.test.ts => store.test.ts} (57%) diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 775c4df0908..e225e7afa79 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -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']); } diff --git a/public/app/plugins/datasource/graphite/components/GraphiteQueryEditor.tsx b/public/app/plugins/datasource/graphite/components/GraphiteQueryEditor.tsx index d5f861cb43a..a06ee5f20b8 100644 --- a/public/app/plugins/datasource/graphite/components/GraphiteQueryEditor.tsx +++ b/public/app/plugins/datasource/graphite/components/GraphiteQueryEditor.tsx @@ -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 ( - - {state.target?.textEditor && } - {!state.target?.textEditor && ( - <> - - - - )} - + + + ); } + +function GraphiteQueryEditorContent() { + const dispatch = useDispatch(); + const state = useGraphiteState(); + const styles = useStyles2(getStyles); + + return ( +
+
+ {state.target?.textEditor && } + {!state.target?.textEditor && ( + <> + + + + )} +
+
+ ); +} + +function getStyles(theme: GrafanaTheme2) { + return { + container: css` + display: flex; + `, + visualEditor: css` + flex-grow: 1; + `, + toggleButton: css` + margin-left: ${theme.spacing(0.5)}; + `, + }; +} diff --git a/public/app/plugins/datasource/graphite/components/SeriesSection.tsx b/public/app/plugins/datasource/graphite/components/SeriesSection.tsx index 7ec29a4a3d4..17d5f47a598 100644 --- a/public/app/plugins/datasource/graphite/components/SeriesSection.tsx +++ b/public/app/plugins/datasource/graphite/components/SeriesSection.tsx @@ -10,7 +10,7 @@ type Props = { export function SeriesSection({ state }: Props) { const sectionContent = state.queryModel?.seriesByTagUsed ? ( - + ) : ( ); diff --git a/public/app/plugins/datasource/graphite/components/TagsSection.tsx b/public/app/plugins/datasource/graphite/components/TagsSection.tsx index 8fdf09313c2..c69e33378c2 100644 --- a/public/app/plugins/datasource/graphite/components/TagsSection.tsx +++ b/public/app/plugins/datasource/graphite/components/TagsSection.tsx @@ -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 ; })} - {newTagsOptions.length && ( + {tags.length && ( inputMinWidth={150} onChange={(value) => { diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index a4929c1659f..8b3d45bb1d2 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -15,7 +15,7 @@ export type GraphiteTag = { value: string; }; -type GraphiteTarget = { +export type GraphiteTarget = { refId: string | number; target: string; /** diff --git a/public/app/plugins/datasource/graphite/module.ts b/public/app/plugins/datasource/graphite/module.ts index 4d4e0f243cb..677be4fa678 100644 --- a/public/app/plugins/datasource/graphite/module.ts +++ b/public/app/plugins/datasource/graphite/module.ts @@ -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); diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts deleted file mode 100644 index f179d0387b1..00000000000 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ /dev/null @@ -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 component (view models are MetricSegment objects) - // It will be simplified to produce data needed by React 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()); - } -} diff --git a/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts b/public/app/plugins/datasource/graphite/specs/store.test.ts similarity index 57% rename from public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts rename to public/app/plugins/datasource/graphite/specs/store.test.ts index 081ec4114c2..68deeaeb6cd 100644 --- a/public/app/plugins/datasource/graphite/specs/query_ctrl.test.ts +++ b/public/app/plugins/datasource/graphite/specs/store.test.ts @@ -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 { - 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); }); }); }); diff --git a/public/app/plugins/datasource/graphite/state/actions.ts b/public/app/plugins/datasource/graphite/state/actions.ts index 951cc086341..555a1639b0e 100644 --- a/public/app/plugins/datasource/graphite/state/actions.ts +++ b/public/app/plugins/datasource/graphite/state/actions.ts @@ -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('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('init'); +const timeRangeChanged = createAction('time-range-changed'); +const queriesChanged = createAction('queries-changed'); +const queryChanged = createAction('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, diff --git a/public/app/plugins/datasource/graphite/state/context.tsx b/public/app/plugins/datasource/graphite/state/context.tsx index 78b06381366..6a9483e67a3 100644 --- a/public/app/plugins/datasource/graphite/state/context.tsx +++ b/public/app/plugins/datasource/graphite/state/context.tsx @@ -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; -}; +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>({} as Dispatch); +const GraphiteStateContext = createContext({} as GraphiteQueryEditorState); export const useDispatch = () => { return useContext(DispatchContext); }; -export const GraphiteContext = ({ children, dispatch }: PropsWithChildren) => { - return {children}; +export const useGraphiteState = () => { + return useContext(GraphiteStateContext); +}; + +export type GraphiteQueryEditorProps = QueryEditorProps; + +export const GraphiteQueryEditorContext = ({ + datasource, + onRunQuery, + onChange, + query, + queries, + range, + children, +}: PropsWithChildren) => { + const [state, setState] = useState(); + + 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 ( + + {children} + + ); + } }; diff --git a/public/app/plugins/datasource/graphite/state/helpers.ts b/public/app/plugins/datasource/graphite/state/helpers.ts index 589b9ea24d4..a5071fc38dc 100644 --- a/public/app/plugins/datasource/graphite/state/helpers.ts +++ b/public/app/plugins/datasource/graphite/state/helpers.ts @@ -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 { - 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); } } diff --git a/public/app/plugins/datasource/graphite/state/providers.ts b/public/app/plugins/datasource/graphite/state/providers.ts index 8def95bf3e8..ade27bd9578 100644 --- a/public/app/plugins/datasource/graphite/state/providers.ts +++ b/public/app/plugins/datasource/graphite/state/providers.ts @@ -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 = []; diff --git a/public/app/plugins/datasource/graphite/state/store.ts b/public/app/plugins/datasource/graphite/state/store.ts index 3a51bc77842..47cf379913b 100644 --- a/public/app/plugins/datasource/graphite/state/store.ts +++ b/public/app/plugins/datasource/graphite/state/store.ts @@ -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 => { 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; }; diff --git a/public/app/plugins/datasource/graphite/types.ts b/public/app/plugins/datasource/graphite/types.ts index a6a348de290..bcec810aefa 100644 --- a/public/app/plugins/datasource/graphite/types.ts +++ b/public/app/plugins/datasource/graphite/types.ts @@ -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; - -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; };