You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by ar...@apache.org on 2024/02/12 18:10:10 UTC

(superset) branch master updated: feat(plugins): Adding colors to BigNumber with Time Comparison chart (#27052)

This is an automated email from the ASF dual-hosted git repository.

arivero pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/superset.git


The following commit(s) were added to refs/heads/master by this push:
     new e8e208dd14 feat(plugins): Adding colors to BigNumber with Time Comparison chart (#27052)
e8e208dd14 is described below

commit e8e208dd14b132339b5187b7368e86326a44e3f4
Author: Antonio Rivero <38...@users.noreply.github.com>
AuthorDate: Mon Feb 12 19:10:04 2024 +0100

    feat(plugins): Adding colors to BigNumber with Time Comparison chart (#27052)
---
 .../src/PopKPI.tsx                                 | 104 +++++++++--
 .../src/images/thumbnail.png                       | Bin 23099 -> 10434 bytes
 .../src/plugin/buildQuery.ts                       | 208 +--------------------
 .../src/plugin/controlPanel.ts                     |  12 ++
 .../src/plugin/transformProps.ts                   |   5 +-
 .../src/types.ts                                   |  19 +-
 .../src/{plugin/buildQuery.ts => utils.ts}         | 111 +++--------
 7 files changed, 148 insertions(+), 311 deletions(-)

diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
index e780e93ca4..85156ae951 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/PopKPI.tsx
@@ -16,9 +16,13 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import React, { createRef } from 'react';
+import React, { createRef, useMemo } from 'react';
 import { css, styled, useTheme } from '@superset-ui/core';
-import { PopKPIComparisonValueStyleProps, PopKPIProps } from './types';
+import {
+  PopKPIComparisonSymbolStyleProps,
+  PopKPIComparisonValueStyleProps,
+  PopKPIProps,
+} from './types';
 
 const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
   ${({ theme, subheaderFontSize }) => `
@@ -30,6 +34,17 @@ const ComparisonValue = styled.div<PopKPIComparisonValueStyleProps>`
   `}
 `;
 
+const SymbolWrapper = styled.div<PopKPIComparisonSymbolStyleProps>`
+  ${({ theme, backgroundColor, textColor }) => `
+    background-color: ${backgroundColor};
+    color: ${textColor};
+    padding: ${theme.gridUnit}px ${theme.gridUnit * 2}px;
+    border-radius: ${theme.gridUnit * 2}px;
+    display: inline-block;
+    margin-right: ${theme.gridUnit}px;
+  `}
+`;
+
 export default function PopKPI(props: PopKPIProps) {
   const {
     height,
@@ -37,9 +52,11 @@ export default function PopKPI(props: PopKPIProps) {
     bigNumber,
     prevNumber,
     valueDifference,
-    percentDifference,
+    percentDifferenceFormattedString,
     headerFontSize,
     subheaderFontSize,
+    comparisonColorEnabled,
+    percentDifferenceNumber,
   } = props;
 
   const rootElem = createRef<HTMLDivElement>();
@@ -63,9 +80,60 @@ export default function PopKPI(props: PopKPIProps) {
     text-align: center;
   `;
 
+  const getArrowIndicatorColor = () => {
+    if (!comparisonColorEnabled) return theme.colors.grayscale.base;
+    return percentDifferenceNumber > 0
+      ? theme.colors.success.base
+      : theme.colors.error.base;
+  };
+
+  const arrowIndicatorStyle = css`
+    color: ${getArrowIndicatorColor()};
+    margin-left: ${theme.gridUnit}px;
+  `;
+
+  const defaultBackgroundColor = theme.colors.grayscale.light4;
+  const defaultTextColor = theme.colors.grayscale.base;
+  const { backgroundColor, textColor } = useMemo(() => {
+    let bgColor = defaultBackgroundColor;
+    let txtColor = defaultTextColor;
+    if (percentDifferenceNumber > 0) {
+      if (comparisonColorEnabled) {
+        bgColor = theme.colors.success.light2;
+        txtColor = theme.colors.success.base;
+      }
+    } else if (percentDifferenceNumber < 0) {
+      if (comparisonColorEnabled) {
+        bgColor = theme.colors.error.light2;
+        txtColor = theme.colors.error.base;
+      }
+    }
+
+    return {
+      backgroundColor: bgColor,
+      textColor: txtColor,
+    };
+  }, [theme, comparisonColorEnabled, percentDifferenceNumber]);
+
+  const SYMBOLS_WITH_VALUES = useMemo(
+    () => [
+      ['#', prevNumber],
+      ['△', valueDifference],
+      ['%', percentDifferenceFormattedString],
+    ],
+    [prevNumber, valueDifference, percentDifferenceFormattedString],
+  );
+
   return (
     <div ref={rootElem} css={wrapperDivStyles}>
-      <div css={bigValueContainerStyles}>{bigNumber}</div>
+      <div css={bigValueContainerStyles}>
+        {bigNumber}
+        {percentDifferenceNumber !== 0 && (
+          <span css={arrowIndicatorStyle}>
+            {percentDifferenceNumber > 0 ? '↑' : '↓'}
+          </span>
+        )}
+      </div>
       <div
         css={css`
           width: 100%;
@@ -77,18 +145,22 @@ export default function PopKPI(props: PopKPIProps) {
             display: table-row;
           `}
         >
-          <ComparisonValue subheaderFontSize={subheaderFontSize}>
-            {' '}
-            #: {prevNumber}
-          </ComparisonValue>
-          <ComparisonValue subheaderFontSize={subheaderFontSize}>
-            {' '}
-            Δ: {valueDifference}
-          </ComparisonValue>
-          <ComparisonValue subheaderFontSize={subheaderFontSize}>
-            {' '}
-            %: {percentDifference}
-          </ComparisonValue>
+          {SYMBOLS_WITH_VALUES.map((symbol_with_value, index) => (
+            <ComparisonValue
+              key={`comparison-symbol-${symbol_with_value[0]}`}
+              subheaderFontSize={subheaderFontSize}
+            >
+              <SymbolWrapper
+                backgroundColor={
+                  index > 0 ? backgroundColor : defaultBackgroundColor
+                }
+                textColor={index > 0 ? textColor : defaultTextColor}
+              >
+                {symbol_with_value[0]}
+              </SymbolWrapper>
+              {symbol_with_value[1]}
+            </ComparisonValue>
+          ))}
         </div>
       </div>
     </div>
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png
index 30c9e07b0c..3be299145b 100644
Binary files a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png and b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/images/thumbnail.png differ
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
index aa0477e48f..38346007b4 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
@@ -21,7 +21,7 @@ import {
   buildQueryContext,
   QueryFormData,
 } from '@superset-ui/core';
-import moment, { Moment } from 'moment';
+import { computeQueryBComparator } from '../utils';
 
 /**
  * The buildQuery function is used to create an instance of QueryContext that's
@@ -38,184 +38,6 @@ import moment, { Moment } from 'moment';
  * if a viz needs multiple different result sets.
  */
 
-type MomentTuple = [moment.Moment | null, moment.Moment | null];
-
-function getSinceUntil(
-  timeRange: string | null = null,
-  relativeStart: string | null = null,
-  relativeEnd: string | null = null,
-): MomentTuple {
-  const separator = ' : ';
-  const effectiveRelativeStart = relativeStart || 'today';
-  const effectiveRelativeEnd = relativeEnd || 'today';
-
-  if (!timeRange) {
-    return [null, null];
-  }
-
-  let modTimeRange: string | null = timeRange;
-
-  if (timeRange === 'NO_TIME_RANGE' || timeRange === '_(NO_TIME_RANGE)') {
-    return [null, null];
-  }
-
-  if (timeRange?.startsWith('last') && !timeRange.includes(separator)) {
-    modTimeRange = timeRange + separator + effectiveRelativeEnd;
-  }
-
-  if (timeRange?.startsWith('next') && !timeRange.includes(separator)) {
-    modTimeRange = effectiveRelativeStart + separator + timeRange;
-  }
-
-  if (
-    timeRange?.startsWith('previous calendar week') &&
-    !timeRange.includes(separator)
-  ) {
-    return [
-      moment().subtract(1, 'week').startOf('week'),
-      moment().startOf('week'),
-    ];
-  }
-
-  if (
-    timeRange?.startsWith('previous calendar month') &&
-    !timeRange.includes(separator)
-  ) {
-    return [
-      moment().subtract(1, 'month').startOf('month'),
-      moment().startOf('month'),
-    ];
-  }
-
-  if (
-    timeRange?.startsWith('previous calendar year') &&
-    !timeRange.includes(separator)
-  ) {
-    return [
-      moment().subtract(1, 'year').startOf('year'),
-      moment().startOf('year'),
-    ];
-  }
-
-  const timeRangeLookup: Array<[RegExp, (...args: string[]) => Moment]> = [
-    [
-      /^last\s+(day|week|month|quarter|year)$/i,
-      (unit: string) =>
-        moment().subtract(1, unit as moment.unitOfTime.DurationConstructor),
-    ],
-    [
-      /^last\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
-      (delta: string, unit: string) =>
-        moment().subtract(delta, unit as moment.unitOfTime.DurationConstructor),
-    ],
-    [
-      /^next\s+([0-9]+)\s+(second|minute|hour|day|week|month|year)s?$/i,
-      (delta: string, unit: string) =>
-        moment().add(delta, unit as moment.unitOfTime.DurationConstructor),
-    ],
-    [
-      // eslint-disable-next-line no-useless-escape
-      /DATEADD\(DATETIME\("([^"]+)"\),\s*(-?\d+),\s*([^\)]+)\)/i,
-      (timePart: string, delta: string, unit: string) => {
-        if (timePart === 'now') {
-          return moment().add(
-            delta,
-            unit as moment.unitOfTime.DurationConstructor,
-          );
-        }
-        if (moment(timePart.toUpperCase(), true).isValid()) {
-          return moment(timePart).add(
-            delta,
-            unit as moment.unitOfTime.DurationConstructor,
-          );
-        }
-        return moment();
-      },
-    ],
-  ];
-
-  const sinceAndUntilPartition = modTimeRange
-    .split(separator, 2)
-    .map(part => part.trim());
-
-  const sinceAndUntil: (Moment | null)[] = sinceAndUntilPartition.map(part => {
-    if (!part) {
-      return null;
-    }
-
-    let transformedValue: Moment | null = null;
-    // Matching time_range_lookup
-    const matched = timeRangeLookup.some(([pattern, fn]) => {
-      const result = part.match(pattern);
-      if (result) {
-        transformedValue = fn(...result.slice(1));
-        return true;
-      }
-
-      if (part === 'today') {
-        transformedValue = moment().startOf('day');
-        return true;
-      }
-
-      if (part === 'now') {
-        transformedValue = moment();
-        return true;
-      }
-      return false;
-    });
-
-    if (matched && transformedValue !== null) {
-      // Handle the transformed value
-    } else {
-      // Handle the case when there was no match
-      transformedValue = moment(`${part}`);
-    }
-
-    return transformedValue;
-  });
-
-  const [_since, _until] = sinceAndUntil;
-
-  if (_since && _until && _since.isAfter(_until)) {
-    throw new Error('From date cannot be larger than to date');
-  }
-
-  return [_since, _until];
-}
-
-function calculatePrev(
-  startDate: Moment | null,
-  endDate: Moment | null,
-  calcType: String,
-) {
-  if (!startDate || !endDate) {
-    return [null, null];
-  }
-
-  const daysBetween = endDate.diff(startDate, 'days');
-
-  let startDatePrev = moment();
-  let endDatePrev = moment();
-  if (calcType === 'y') {
-    startDatePrev = startDate.subtract(1, 'year');
-    endDatePrev = endDate.subtract(1, 'year');
-  } else if (calcType === 'w') {
-    startDatePrev = startDate.subtract(1, 'week');
-    endDatePrev = endDate.subtract(1, 'week');
-  } else if (calcType === 'm') {
-    startDatePrev = startDate.subtract(1, 'month');
-    endDatePrev = endDate.subtract(1, 'month');
-  } else if (calcType === 'r') {
-    startDatePrev = startDate.clone().subtract(daysBetween.valueOf(), 'day');
-    endDatePrev = startDate;
-  } else {
-    startDatePrev = startDate.subtract(1, 'year');
-    endDatePrev = endDate.subtract(1, 'year');
-  }
-
-  return [startDatePrev, endDatePrev];
-}
-
 export default function buildQuery(formData: QueryFormData) {
   const {
     cols: groupby,
@@ -240,37 +62,19 @@ export default function buildQuery(formData: QueryFormData) {
       ? formData.adhoc_filters[timeFilterIndex]
       : null;
 
-  let testSince = null;
-  let testUntil = null;
-
-  if (
-    timeFilter &&
-    'comparator' in timeFilter &&
-    typeof timeFilter.comparator === 'string'
-  ) {
-    let timeRange = timeFilter.comparator.toLocaleLowerCase();
-    if (extraFormData?.time_range) {
-      timeRange = extraFormData.time_range;
-    }
-    [testSince, testUntil] = getSinceUntil(timeRange);
-  }
-
   let formDataB: QueryFormData;
+  let queryBComparator = null;
 
   if (timeComparison !== 'c') {
-    const [prevStartDateMoment, prevEndDateMoment] = calculatePrev(
-      testSince,
-      testUntil,
+    queryBComparator = computeQueryBComparator(
+      formData.adhoc_filters || [],
       timeComparison,
+      extraFormData,
     );
 
-    const queryBComparator = `${prevStartDateMoment?.format(
-      'YYYY-MM-DDTHH:mm:ss',
-    )} : ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`;
-
     const queryBFilter: any = {
       ...timeFilter,
-      comparator: queryBComparator.replace(/Z/g, ''),
+      comparator: queryBComparator,
     };
 
     const otherFilters = formData.adhoc_filters?.filter(
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
index 89afdb4835..3d2504f639 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/controlPanel.ts
@@ -181,6 +181,18 @@ const config: ControlPanelConfig = {
             },
           },
         ],
+        [
+          {
+            name: 'comparison_color_enabled',
+            config: {
+              type: 'CheckboxControl',
+              label: t('Add color for positive/negative change'),
+              renderTrigger: true,
+              default: false,
+              description: t('Add color for positive/negative change'),
+            },
+          },
+        ],
       ],
     },
   ],
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
index 80737f6032..e5de882f6d 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/transformProps.ts
@@ -81,6 +81,7 @@ export default function transformProps(chartProps: ChartProps) {
     yAxisFormat,
     currencyFormat,
     subheaderFontSize,
+    comparisonColorEnabled,
   } = formData;
   const { data: dataA = [] } = queriesData[0];
   const { data: dataB = [] } = queriesData[1];
@@ -138,11 +139,13 @@ export default function transformProps(chartProps: ChartProps) {
     bigNumber,
     prevNumber,
     valueDifference,
-    percentDifference,
+    percentDifferenceFormattedString: percentDifference,
     boldText,
     headerFontSize,
     subheaderFontSize,
     headerText,
     compType,
+    comparisonColorEnabled,
+    percentDifferenceNumber: percentDifferenceNum,
   };
 }
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
index b13f2115ef..a239a29593 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/types.ts
@@ -29,6 +29,7 @@ export interface PopKPIStylesProps {
   headerFontSize: keyof typeof supersetTheme.typography.sizes;
   subheaderFontSize: keyof typeof supersetTheme.typography.sizes;
   boldText: boolean;
+  comparisonColorEnabled: boolean;
 }
 
 interface PopKPICustomizeProps {
@@ -39,6 +40,11 @@ export interface PopKPIComparisonValueStyleProps {
   subheaderFontSize?: keyof typeof supersetTheme.typography.sizes;
 }
 
+export interface PopKPIComparisonSymbolStyleProps {
+  backgroundColor: string;
+  textColor: string;
+}
+
 export type PopKPIQueryFormData = QueryFormData &
   PopKPIStylesProps &
   PopKPICustomizeProps;
@@ -47,10 +53,11 @@ export type PopKPIProps = PopKPIStylesProps &
   PopKPICustomizeProps & {
     data: TimeseriesDataRecord[];
     metrics: Metric[];
-    metricName: String;
-    bigNumber: Number;
-    prevNumber: Number;
-    valueDifference: Number;
-    percentDifference: Number;
-    compType: String;
+    metricName: string;
+    bigNumber: string;
+    prevNumber: string;
+    valueDifference: string;
+    percentDifferenceFormattedString: string;
+    compType: string;
+    percentDifferenceNumber: number;
   };
diff --git a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts
similarity index 70%
copy from superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
copy to superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts
index aa0477e48f..4ce2ff1e4c 100644
--- a/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/plugin/buildQuery.ts
+++ b/superset-frontend/plugins/plugin-chart-period-over-period-kpi/src/utils.ts
@@ -16,35 +16,17 @@
  * specific language governing permissions and limitations
  * under the License.
  */
-import {
-  AdhocFilter,
-  buildQueryContext,
-  QueryFormData,
-} from '@superset-ui/core';
-import moment, { Moment } from 'moment';
 
-/**
- * The buildQuery function is used to create an instance of QueryContext that's
- * sent to the chart data endpoint. In addition to containing information of which
- * datasource to use, it specifies the type (e.g. full payload, samples, query) and
- * format (e.g. CSV or JSON) of the result and whether or not to force refresh the data from
- * the datasource as opposed to using a cached copy of the data, if available.
- *
- * More importantly though, QueryContext contains a property `queries`, which is an array of
- * QueryObjects specifying individual data requests to be made. A QueryObject specifies which
- * columns, metrics and filters, among others, to use during the query. Usually it will be enough
- * to specify just one query based on the baseQueryObject, but for some more advanced use cases
- * it is possible to define post processing operations in the QueryObject, or multiple queries
- * if a viz needs multiple different result sets.
- */
+import { AdhocFilter } from '@superset-ui/core';
+import moment, { Moment } from 'moment';
 
 type MomentTuple = [moment.Moment | null, moment.Moment | null];
 
-function getSinceUntil(
+const getSinceUntil = (
   timeRange: string | null = null,
   relativeStart: string | null = null,
   relativeEnd: string | null = null,
-): MomentTuple {
+): MomentTuple => {
   const separator = ' : ';
   const effectiveRelativeStart = relativeStart || 'today';
   const effectiveRelativeEnd = relativeEnd || 'today';
@@ -181,13 +163,13 @@ function getSinceUntil(
   }
 
   return [_since, _until];
-}
+};
 
-function calculatePrev(
+const calculatePrev = (
   startDate: Moment | null,
   endDate: Moment | null,
   calcType: String,
-) {
+) => {
   if (!startDate || !endDate) {
     return [null, null];
   }
@@ -214,31 +196,21 @@ function calculatePrev(
   }
 
   return [startDatePrev, endDatePrev];
-}
-
-export default function buildQuery(formData: QueryFormData) {
-  const {
-    cols: groupby,
-    time_comparison: timeComparison,
-    extra_form_data: extraFormData,
-  } = formData;
-
-  const queryContextA = buildQueryContext(formData, baseQueryObject => [
-    {
-      ...baseQueryObject,
-      groupby,
-    },
-  ]);
-
-  const timeFilterIndex: number =
-    formData.adhoc_filters?.findIndex(
+};
+
+export const computeQueryBComparator = (
+  adhocFilters: AdhocFilter[],
+  timeComparison: string,
+  extraFormData: any,
+  join = ':',
+) => {
+  const timeFilterIndex =
+    adhocFilters?.findIndex(
       filter => 'operator' in filter && filter.operator === 'TEMPORAL_RANGE',
     ) ?? -1;
 
-  const timeFilter: AdhocFilter | null =
-    timeFilterIndex !== -1 && formData.adhoc_filters
-      ? formData.adhoc_filters[timeFilterIndex]
-      : null;
+  const timeFilter =
+    timeFilterIndex !== -1 ? adhocFilters[timeFilterIndex] : null;
 
   let testSince = null;
   let testUntil = null;
@@ -255,8 +227,6 @@ export default function buildQuery(formData: QueryFormData) {
     [testSince, testUntil] = getSinceUntil(timeRange);
   }
 
-  let formDataB: QueryFormData;
-
   if (timeComparison !== 'c') {
     const [prevStartDateMoment, prevEndDateMoment] = calculatePrev(
       testSince,
@@ -264,44 +234,13 @@ export default function buildQuery(formData: QueryFormData) {
       timeComparison,
     );
 
-    const queryBComparator = `${prevStartDateMoment?.format(
+    return `${prevStartDateMoment?.format(
       'YYYY-MM-DDTHH:mm:ss',
-    )} : ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`;
-
-    const queryBFilter: any = {
-      ...timeFilter,
-      comparator: queryBComparator.replace(/Z/g, ''),
-    };
-
-    const otherFilters = formData.adhoc_filters?.filter(
-      (_value: any, index: number) => timeFilterIndex !== index,
+    )} ${join} ${prevEndDateMoment?.format('YYYY-MM-DDTHH:mm:ss')}`.replace(
+      /Z/g,
+      '',
     );
-    const queryBFilters = otherFilters
-      ? [queryBFilter, ...otherFilters]
-      : [queryBFilter];
-
-    formDataB = {
-      ...formData,
-      adhoc_filters: queryBFilters,
-      extra_form_data: {},
-    };
-  } else {
-    formDataB = {
-      ...formData,
-      adhoc_filters: formData.adhoc_custom,
-      extra_form_data: {},
-    };
   }
 
-  const queryContextB = buildQueryContext(formDataB, baseQueryObject => [
-    {
-      ...baseQueryObject,
-      groupby,
-    },
-  ]);
-
-  return {
-    ...queryContextA,
-    queries: [...queryContextA.queries, ...queryContextB.queries],
-  };
-}
+  return null;
+};