123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- import { TemplateSrvStub } from 'test/specs/helpers';
- import { dispatch } from 'app/store/store';
- import gfunc from '../gfunc';
- import { actions } from '../state/actions';
- import {
- getAltSegmentsSelectables,
- getTagsSelectables,
- getTagsAsSegmentsSelectables,
- getTagValuesSelectables,
- } from '../state/providers';
- import { createStore } from '../state/store';
- import { GraphiteSegment } from '../types';
- jest.mock('app/angular/promiseToDigest', () => ({
- promiseToDigest: (scope: any) => {
- return (p: Promise<any>) => p;
- },
- }));
- jest.mock('app/store/store', () => ({
- dispatch: jest.fn(),
- }));
- 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.dispatch(actions.toggleEditorMode());
- await ctx.dispatch(actions.updateQuery({ query: target }));
- await ctx.dispatch(actions.runQuery());
- await ctx.dispatch(actions.toggleEditorMode());
- }
- describe('Graphite actions', () => {
- const ctx = {
- datasource: {
- metricFindQuery: jest.fn(() => Promise.resolve([])),
- getFuncDefs: jest.fn(() => Promise.resolve(gfunc.getFuncDefs('1.0'))),
- getFuncDef: gfunc.getFuncDef,
- waitForFuncDefsLoaded: jest.fn(() => Promise.resolve(null)),
- createFuncInstance: gfunc.createFuncInstance,
- getTagsAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
- getTagValuesAutoComplete: jest.fn().mockReturnValue(Promise.resolve([])),
- },
- target: { target: 'aliasByNode(scaleToSeconds(test.prod.*,1),2)' },
- } as any;
- beforeEach(async () => {
- jest.clearAllMocks();
- 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(),
- })
- );
- });
- describe('init', () => {
- it('should validate metric key exists', () => {
- expect(ctx.datasource.metricFindQuery.mock.calls[0][0]).toBe('test.prod.*');
- });
- it('should not delete last segment if no metrics are found', () => {
- 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.state.queryModel.functions.length).toBe(2);
- });
- });
- describe('when toggling edit mode to raw and back again', () => {
- beforeEach(async () => {
- await ctx.dispatch(actions.toggleEditorMode());
- await ctx.dispatch(actions.toggleEditorMode());
- });
- it('should validate metric key exists', () => {
- const lastCallIndex = ctx.datasource.metricFindQuery.mock.calls.length - 1;
- expect(ctx.datasource.metricFindQuery.mock.calls[lastCallIndex][0]).toBe('test.prod.*');
- });
- it('should delete last segment if no metrics are found', () => {
- 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.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.dispatch(actions.segmentValueChanged({ segment: segment, index: 1 }));
- });
- it('should validate metric key exists', () => {
- const lastCallIndex = ctx.datasource.metricFindQuery.mock.calls.length - 1;
- expect(ctx.datasource.metricFindQuery.mock.calls[lastCallIndex][0]).toBe('test.test.*');
- });
- it('should delete last segment if no metrics are found', () => {
- 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.state.queryModel.functions.length).toBe(2);
- });
- });
- describe('when adding function', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- await changeTarget(ctx, 'test.prod.*.count');
- await ctx.dispatch(actions.addFunction({ name: 'aliasByNode' }));
- });
- it('should add function with correct node number', () => {
- expect(ctx.state.queryModel.functions[0].params[0]).toBe(2);
- });
- it('should update target', () => {
- expect(ctx.state.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
- });
- it('should call refresh', () => {
- expect(ctx.state.refresh).toHaveBeenCalled();
- });
- });
- describe('when adding function before any metric segment', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
- await changeTarget(ctx, '');
- await ctx.dispatch(actions.addFunction({ name: 'asPercent' }));
- });
- it('should add function and remove select metric link', () => {
- expect(ctx.state.segments.length).toBe(0);
- });
- });
- describe('when initializing a target with single param func using variable', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([]);
- await changeTarget(ctx, 'movingAverage(prod.count, $var)');
- });
- it('should add 2 segments', () => {
- expect(ctx.state.segments.length).toBe(2);
- });
- it('should add function param', () => {
- 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.state.datasource.metricFindQuery = () => Promise.resolve([]);
- await changeTarget(ctx, 'asPercent(metric.node.count, #A)');
- });
- it('should add segments', () => {
- expect(ctx.state.segments.length).toBe(3);
- });
- it('should have correct func params', () => {
- expect(ctx.state.queryModel.functions[0].params.length).toBe(1);
- });
- });
- describe('when getting altSegments and metricFindQuery returns empty array', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([]);
- await changeTarget(ctx, 'test.count');
- ctx.altSegments = await getAltSegmentsSelectables(ctx.state, 1, '');
- });
- it('should have no segments', () => {
- expect(ctx.altSegments.length).toBe(0);
- });
- });
- it('current time range and limit is passed when getting list of tags when editing', async () => {
- const currentRange = { from: 0, to: 1 };
- ctx.state.range = currentRange;
- await getTagsSelectables(ctx.state, 0, 'any');
- expect(ctx.state.datasource.getTagsAutoComplete).toBeCalledWith([], 'any', { range: currentRange, limit: 5000 });
- });
- it('current time range and limit is passed when getting list of tags for adding', async () => {
- const currentRange = { from: 0, to: 1 };
- ctx.state.range = currentRange;
- await getTagsAsSegmentsSelectables(ctx.state, 'any');
- expect(ctx.state.datasource.getTagsAutoComplete).toBeCalledWith([], 'any', { range: currentRange, limit: 5000 });
- });
- it('limit is passed when getting list of tag values', async () => {
- await getTagValuesSelectables(ctx.state, { key: 'key', operator: '=', value: 'value' }, 1, 'test');
- expect(ctx.state.datasource.getTagValuesAutoComplete).toBeCalledWith([], 'key', 'test', { limit: 5000 });
- });
- describe('when autocomplete for metric names is not available', () => {
- beforeEach(() => {
- ctx.state.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
- ctx.state.datasource.metricFindQuery = jest.fn().mockReturnValue(
- new Promise(() => {
- throw new Error();
- })
- );
- });
- it('getAltSegmentsSelectables should handle autocomplete errors', async () => {
- await expect(async () => {
- await getAltSegmentsSelectables(ctx.state, 0, 'any');
- expect(mockDispatch).toBeCalledWith(
- expect.objectContaining({
- type: 'appNotifications/notifyApp',
- })
- );
- }).not.toThrow();
- });
- it('getAltSegmentsSelectables should display the error message only once', async () => {
- await getAltSegmentsSelectables(ctx.state, 0, 'any');
- expect(mockDispatch.mock.calls.length).toBe(1);
- await getAltSegmentsSelectables(ctx.state, 0, 'any');
- expect(mockDispatch.mock.calls.length).toBe(1);
- });
- });
- describe('when autocomplete for tags is not available', () => {
- beforeEach(() => {
- ctx.datasource.metricFindQuery = jest.fn().mockReturnValue(Promise.resolve([]));
- ctx.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(
- new Promise(() => {
- throw new Error();
- })
- );
- });
- it('getTagsSelectables should handle autocomplete errors', async () => {
- await expect(async () => {
- await getTagsSelectables(ctx.state, 0, 'any');
- expect(mockDispatch).toBeCalledWith(
- expect.objectContaining({
- type: 'appNotifications/notifyApp',
- })
- );
- }).not.toThrow();
- });
- it('getTagsSelectables should display the error message only once', async () => {
- await getTagsSelectables(ctx.state, 0, 'any');
- expect(mockDispatch.mock.calls.length).toBe(1);
- 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.state, 'any');
- expect(mockDispatch).toBeCalledWith(
- expect.objectContaining({
- type: 'appNotifications/notifyApp',
- })
- );
- }).not.toThrow();
- });
- it('getTagsAsSegmentsSelectables should display the error message only once', async () => {
- await getTagsAsSegmentsSelectables(ctx.state, 'any');
- expect(mockDispatch.mock.calls.length).toBe(1);
- await getTagsAsSegmentsSelectables(ctx.state, 'any');
- expect(mockDispatch.mock.calls.length).toBe(1);
- });
- });
- describe('targetChanged', () => {
- beforeEach(async () => {
- const newQuery = 'aliasByNode(scaleToSeconds(test.prod.*, 1), 2)';
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- await changeTarget(ctx, newQuery);
- });
- it('should rebuild target after expression model', () => {
- expect(ctx.state.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
- });
- it('should call refresh', () => {
- expect(ctx.state.refresh).toHaveBeenCalled();
- });
- });
- describe('when updating targets with nested query', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- await changeTarget(ctx, 'scaleToSeconds(#A, 60)');
- });
- it('should add function params', () => {
- expect(ctx.state.queryModel.segments.length).toBe(1);
- expect(ctx.state.queryModel.segments[0].value).toBe('#A');
- 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.state.target.target).toBe('scaleToSeconds(#A, 60)');
- });
- it('targetFull should include nested queries', async () => {
- await ctx.dispatch(
- actions.queriesChanged([
- {
- target: 'nested.query.count',
- refId: 'A',
- },
- ])
- );
- expect(ctx.state.target.target).toBe('scaleToSeconds(#A, 60)');
- expect(ctx.state.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
- });
- });
- describe('target interpolation', () => {
- beforeEach(async () => {
- ctx.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- ctx.state.target.refId = 'A';
- await changeTarget(ctx, 'sumSeries(#B)');
- });
- it('when updating target used in other query, targetFull of other query should update', async () => {
- ctx.state.queries = [ctx.state.target, { target: 'metrics.foo.count', refId: 'B' }];
- await changeTarget(ctx, 'sumSeries(#B)');
- expect(ctx.state.queryModel.target.targetFull).toBe('sumSeries(metrics.foo.count)');
- });
- it('when updating target from a query from other data source, targetFull of other query should not update', async () => {
- ctx.state.queries = [ctx.state.target, { someOtherProperty: 'metrics.foo.count', refId: 'B' }];
- await changeTarget(ctx, 'sumSeries(#B)');
- expect(ctx.state.queryModel.target.targetFull).toBeUndefined();
- });
- });
- describe('when adding seriesByTag function', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- await changeTarget(ctx, '');
- await ctx.dispatch(actions.addFunction({ name: 'seriesByTag' }));
- });
- it('should update functions', () => {
- expect(ctx.state.queryModel.getSeriesByTagFuncIndex()).toBe(0);
- });
- it('should update seriesByTagUsed flag', () => {
- expect(ctx.state.queryModel.seriesByTagUsed).toBe(true);
- });
- it('should update target', () => {
- expect(ctx.state.target.target).toBe('seriesByTag()');
- });
- it('should call refresh', () => {
- expect(ctx.state.refresh).toHaveBeenCalled();
- });
- });
- describe('when parsing seriesByTag function', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
- });
- it('should add tags', () => {
- const expected = [
- { key: 'tag1', operator: '=', value: 'value1' },
- { key: 'tag2', operator: '!=~', value: 'value2' },
- ];
- expect(ctx.state.queryModel.tags).toEqual(expected);
- });
- });
- describe('when tag added', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- await changeTarget(ctx, 'seriesByTag()');
- await ctx.dispatch(actions.addNewTag({ segment: { value: 'tag1' } }));
- });
- it('should update tags with default value', () => {
- const expected = [{ key: 'tag1', operator: '=', value: '' }];
- expect(ctx.state.queryModel.tags).toEqual(expected);
- });
- it('should update target', () => {
- const expected = "seriesByTag('tag1=')";
- expect(ctx.state.target.target).toEqual(expected);
- });
- });
- describe('when tag changed', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
- await ctx.dispatch(actions.tagChanged({ tag: { key: 'tag1', operator: '=', value: 'new_value' }, index: 0 }));
- });
- it('should update tags', () => {
- const expected = [
- { key: 'tag1', operator: '=', value: 'new_value' },
- { key: 'tag2', operator: '!=~', value: 'value2' },
- ];
- expect(ctx.state.queryModel.tags).toEqual(expected);
- });
- it('should update target', () => {
- const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
- expect(ctx.state.target.target).toEqual(expected);
- });
- });
- describe('when tag removed', () => {
- beforeEach(async () => {
- ctx.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
- await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
- 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.state.queryModel.tags).toEqual(expected);
- });
- it('should update target', () => {
- const expected = "seriesByTag('tag2!=~value2')";
- expect(ctx.state.target.target).toEqual(expected);
- });
- });
- describe('when auto-completing over a large set of tags and metrics', () => {
- const manyMetrics: Array<{ text: string }> = [],
- max = 20000;
- beforeEach(() => {
- for (let i = 0; i < max; i++) {
- manyMetrics.push({ text: `metric${i}` });
- }
- ctx.state.datasource.metricFindQuery = jest.fn().mockReturnValue(Promise.resolve(manyMetrics));
- ctx.state.datasource.getTagsAutoComplete = jest.fn((_tag, _prefix, { limit }) => {
- const tags = [];
- for (let i = 0; i < limit; i++) {
- tags.push({ text: `tag${i}` });
- }
- return tags;
- });
- });
- it('uses limited metrics and tags list', async () => {
- ctx.state.supportsTags = true;
- const segments = await getAltSegmentsSelectables(ctx.state, 0, '');
- expect(segments).toHaveLength(10000);
- expect(segments[0].value!.value).toBe('*'); // * - is a fixed metric name, always added at the top
- expect(segments[4999].value!.value).toBe('metric4998');
- expect(segments[5000].value!.value).toBe('tag: tag0');
- expect(segments[9999].value!.value).toBe('tag: tag4999');
- });
- it('uses correct limit for metrics and tags list when tags are not supported', async () => {
- ctx.state.supportsTags = false;
- const segments = await getAltSegmentsSelectables(ctx.state, 0, '');
- expect(segments).toHaveLength(5000);
- expect(segments[0].value!.value).toBe('*'); // * - is a fixed metric name, always added at the top
- expect(segments[4999].value!.value).toBe('metric4998');
- });
- it('uses limited metrics when adding more metrics', async () => {
- const segments = await getAltSegmentsSelectables(ctx.state, 1, '');
- expect(segments).toHaveLength(5000);
- });
- });
- });
|