From 01df00fd0f509c486a84d9e5ec6a006708955bd9 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Tue, 22 Feb 2022 22:17:35 -0500 Subject: [PATCH] DataFrame: Add cached response notice for X-Cache: HIT header (#45564) --- .../src/utils/queryResponse.test.ts | 51 ++++++++++++++++++- .../src/utils/queryResponse.ts | 31 ++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/packages/grafana-runtime/src/utils/queryResponse.test.ts b/packages/grafana-runtime/src/utils/queryResponse.test.ts index 0f64a32a1bd..36bdcdf953e 100644 --- a/packages/grafana-runtime/src/utils/queryResponse.test.ts +++ b/packages/grafana-runtime/src/utils/queryResponse.test.ts @@ -1,6 +1,6 @@ import { DataQuery, toDataFrameDTO, DataFrame } from '@grafana/data'; import { FetchError, FetchResponse } from 'src/services'; -import { BackendDataSourceResponse, toDataQueryResponse, toTestingStatus } from './queryResponse'; +import { BackendDataSourceResponse, cachedResponseNotice, toDataQueryResponse, toTestingStatus } from './queryResponse'; const resp = { data: { @@ -277,6 +277,55 @@ describe('Query Response parser', () => { expect(ids).toEqual(['A', 'B']); }); + describe('Cache notice', () => { + let resp: any; + + beforeEach(() => { + resp = { + url: '', + type: 'basic', + config: { url: '' }, + status: 200, + statusText: 'OK', + ok: true, + redirected: false, + headers: new Headers(), + data: { + results: { + A: { frames: [{ schema: { fields: [] } }] }, + }, + }, + }; + }); + + test('adds notice for responses with X-Cache: HIT header', () => { + const queries: DataQuery[] = [{ refId: 'A' }]; + resp.headers.set('X-Cache', 'HIT'); + expect(toDataQueryResponse(resp, queries).data[0].meta.notices).toStrictEqual([cachedResponseNotice]); + }); + + test('does not remove existing notices', () => { + const queries: DataQuery[] = [{ refId: 'A' }]; + resp.headers.set('X-Cache', 'HIT'); + resp.data.results.A.frames[0].schema.meta = { notices: [{ severity: 'info', text: 'Example' }] }; + expect(toDataQueryResponse(resp, queries).data[0].meta.notices).toStrictEqual([ + { severity: 'info', text: 'Example' }, + cachedResponseNotice, + ]); + }); + + test('does not add notice for responses with X-Cache: MISS header', () => { + const queries: DataQuery[] = [{ refId: 'A' }]; + resp.headers.set('X-Cache', 'MISS'); + expect(toDataQueryResponse(resp, queries).data[0].meta?.notices).toBeUndefined(); + }); + + test('does not add notice for responses without X-Cache header', () => { + const queries: DataQuery[] = [{ refId: 'A' }]; + expect(toDataQueryResponse(resp, queries).data[0].meta?.notices).toBeUndefined(); + }); + }); + test('resultWithError', () => { // Generated from: // qdr.Responses[q.GetRefID()] = backend.DataResponse{ diff --git a/packages/grafana-runtime/src/utils/queryResponse.ts b/packages/grafana-runtime/src/utils/queryResponse.ts index 55c12a20703..748efa4d57d 100644 --- a/packages/grafana-runtime/src/utils/queryResponse.ts +++ b/packages/grafana-runtime/src/utils/queryResponse.ts @@ -12,10 +12,13 @@ import { DataQuery, DataFrameJSON, dataFrameFromJSON, + QueryResultMetaNotice, } from '@grafana/data'; import { FetchError, FetchResponse } from '../services'; import { toDataQueryError } from './toDataQueryError'; +export const cachedResponseNotice: QueryResultMetaNotice = { severity: 'info', text: 'Cached response' }; + /** * Single response object from a backend data source. Properties are optional but response should contain at least * an error or a some data (but can contain both). Main way to send data is with dataframes attribute as series and @@ -62,6 +65,7 @@ export function toDataQueryResponse( if ((res as FetchResponse).data?.results) { const results = (res as FetchResponse).data.results; const refIDs = queries?.length ? queries.map((q) => q.refId) : Object.keys(results); + const cachedResponse = isCachedResponse(res as FetchResponse); const data: DataResponse[] = []; for (const refId of refIDs) { @@ -85,7 +89,10 @@ export function toDataQueryResponse( } if (dr.frames?.length) { - for (const js of dr.frames) { + for (let js of dr.frames) { + if (cachedResponse) { + js = addCacheNotice(js); + } const df = dataFrameFromJSON(js); if (!df.refId) { df.refId = dr.refId; @@ -128,6 +135,28 @@ export function toDataQueryResponse( return rsp; } +function isCachedResponse(res: FetchResponse): boolean { + const headers = res?.headers; + if (!headers || !headers.get) { + return false; + } + return headers.get('X-Cache') === 'HIT'; +} + +function addCacheNotice(frame: DataFrameJSON): DataFrameJSON { + return { + ...frame, + schema: { + ...frame.schema, + fields: [...(frame.schema?.fields ?? [])], + meta: { + ...frame.schema?.meta, + notices: [...(frame.schema?.meta?.notices ?? []), cachedResponseNotice], + }, + }, + }; +} + /** * Data sources using api/ds/query to test data sources can use this function to * handle errors and convert them to TestingStatus object.