mirror of
https://github.com/apache/superset.git
synced 2026-06-24 17:09:20 +00:00
Compare commits
4 Commits
chore/ci/s
...
superset-h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d9d395bde1 | ||
|
|
584d41759b | ||
|
|
8f22b71898 | ||
|
|
1ea3584dcb |
@@ -29,7 +29,7 @@ maintainers:
|
||||
- name: craig-rueda
|
||||
email: craig@craigrueda.com
|
||||
url: https://github.com/craig-rueda
|
||||
version: 0.17.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
version: 0.17.3 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||
dependencies:
|
||||
- name: postgresql
|
||||
version: 16.7.27
|
||||
|
||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
||||
|
||||
# superset
|
||||
|
||||

|
||||

|
||||
|
||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||
|
||||
|
||||
@@ -108,8 +108,6 @@ else:
|
||||
{{ fail (printf "Unsupported database type: %s. Please use 'postgresql' or 'mysql'." .Values.supersetNode.connections.db_type) }}
|
||||
{{- end }}
|
||||
|
||||
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||
|
||||
class CeleryConfig:
|
||||
imports = ("superset.sql_lab", )
|
||||
broker_url = CELERY_REDIS_URL
|
||||
|
||||
@@ -231,6 +231,56 @@ describe('BigNumberTotal transformProps', () => {
|
||||
expect(result.headerFormatter(500)).toBe('$500');
|
||||
});
|
||||
|
||||
test('should pass through non-numeric raw string when parseMetricValue returns null (e.g. VARCHAR MAX)', () => {
|
||||
const { parseMetricValue } = jest.requireMock('../utils');
|
||||
parseMetricValue.mockReturnValueOnce(null);
|
||||
|
||||
const chartProps = {
|
||||
width: 400,
|
||||
height: 300,
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ value: 'some-varchar-result' }],
|
||||
coltypes: [GenericDataType.String],
|
||||
},
|
||||
],
|
||||
formData: baseFormData,
|
||||
rawFormData: baseRawFormData,
|
||||
hooks: baseHooks,
|
||||
datasource: baseDatasource,
|
||||
};
|
||||
|
||||
const result = transformProps(
|
||||
chartProps as unknown as BigNumberTotalChartProps,
|
||||
);
|
||||
expect(result.bigNumber).toBe('some-varchar-result');
|
||||
});
|
||||
|
||||
test('should pass through numeric-looking VARCHAR string literally (e.g. "123")', () => {
|
||||
const { parseMetricValue } = jest.requireMock('../utils');
|
||||
parseMetricValue.mockReturnValueOnce(null);
|
||||
|
||||
const chartProps = {
|
||||
width: 400,
|
||||
height: 300,
|
||||
queriesData: [
|
||||
{
|
||||
data: [{ value: '123' }],
|
||||
coltypes: [GenericDataType.String],
|
||||
},
|
||||
],
|
||||
formData: baseFormData,
|
||||
rawFormData: baseRawFormData,
|
||||
hooks: baseHooks,
|
||||
datasource: baseDatasource,
|
||||
};
|
||||
|
||||
const result = transformProps(
|
||||
chartProps as unknown as BigNumberTotalChartProps,
|
||||
);
|
||||
expect(result.bigNumber).toBe('123');
|
||||
});
|
||||
|
||||
test('should propagate colorThresholdFormatters from getColorFormatters', () => {
|
||||
// Override the getColorFormatters mock to return specific value
|
||||
const mockFormatters = [{ formatter: 'red' }];
|
||||
|
||||
@@ -79,8 +79,15 @@ export default function transformProps(
|
||||
const formattedSubtitleFontSize = subtitle?.trim()
|
||||
? (subtitleFontSize ?? PROPORTION.SUBHEADER)
|
||||
: (subheaderFontSize ?? subtitleFontSize ?? PROPORTION.SUBHEADER);
|
||||
const rawValue = data.length === 0 ? null : data[0][metricName];
|
||||
const parsedValue = rawValue == null ? null : parseMetricValue(rawValue);
|
||||
|
||||
const bigNumber =
|
||||
data.length === 0 ? null : parseMetricValue(data[0][metricName]);
|
||||
parsedValue === null &&
|
||||
typeof rawValue === 'string' &&
|
||||
rawValue.trim() !== ''
|
||||
? rawValue
|
||||
: parsedValue;
|
||||
|
||||
let metricEntry: Metric | undefined;
|
||||
if (chartProps.datasource?.metrics) {
|
||||
|
||||
@@ -189,8 +189,10 @@ function BigNumberVis({
|
||||
text = t('No data');
|
||||
} else if (typeof bigNumber === 'number') {
|
||||
text = headerFormatter(bigNumber);
|
||||
} else if (typeof bigNumber === 'string') {
|
||||
text = bigNumber;
|
||||
} else {
|
||||
// For string/boolean/Date values, convert to number if possible, else show as string
|
||||
// For boolean/Date values, convert to number if possible, else show as string
|
||||
const numValue = Number(bigNumber);
|
||||
text = Number.isNaN(numValue)
|
||||
? String(bigNumber)
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from '../../../../spec/helpers/testing-library';
|
||||
import { AxisType } from '@superset-ui/core';
|
||||
import type { EChartsCoreOption } from 'echarts/core';
|
||||
import type { ECElementEvent } from 'echarts/types/src/util/types';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
LegendOrientation,
|
||||
@@ -202,11 +203,15 @@ const defaultProps: TimeseriesChartTransformedProps = {
|
||||
onFocusedSeries: jest.fn(),
|
||||
};
|
||||
|
||||
function getLatestHeight() {
|
||||
function getLatestEchartProps() {
|
||||
const lastCall = mockEchart.mock.calls.at(-1);
|
||||
expect(lastCall).toBeDefined();
|
||||
const [props] = lastCall as [EchartsProps];
|
||||
return props.height;
|
||||
return props;
|
||||
}
|
||||
|
||||
function getLatestHeight() {
|
||||
return getLatestEchartProps().height;
|
||||
}
|
||||
|
||||
test('observes extra control height changes when ResizeObserver is available', async () => {
|
||||
@@ -335,6 +340,7 @@ test('emits cross-filter on X-axis value when no dimensions and categorical X-ax
|
||||
const clickHandler = props.eventHandlers?.click;
|
||||
if (clickHandler) {
|
||||
clickHandler({
|
||||
componentType: 'series',
|
||||
seriesName: 'Sales', // This is the metric name
|
||||
data: ['Product A', 100], // X-axis value is 'Product A'
|
||||
name: 'Product A',
|
||||
@@ -361,6 +367,149 @@ test('emits cross-filter on X-axis value when no dimensions and categorical X-ax
|
||||
}
|
||||
});
|
||||
|
||||
test('emits cross-filter on category value for horizontal bar clicks', async () => {
|
||||
const setDataMaskMock = jest.fn();
|
||||
|
||||
render(
|
||||
<EchartsTimeseries
|
||||
{...defaultProps}
|
||||
emitCrossFilters
|
||||
setDataMask={setDataMaskMock}
|
||||
formData={{
|
||||
...defaultFormData,
|
||||
orientation: OrientationType.Horizontal,
|
||||
}}
|
||||
xAxis={{
|
||||
label: 'category_column',
|
||||
type: AxisType.Category,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const clickHandler = getLatestEchartProps().eventHandlers?.click;
|
||||
expect(clickHandler).toBeDefined();
|
||||
clickHandler?.({
|
||||
componentType: 'series',
|
||||
seriesName: 'Sales',
|
||||
data: [100, 'Product A'],
|
||||
name: 'Product A',
|
||||
dataIndex: 0,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(setDataMaskMock).toHaveBeenCalled();
|
||||
},
|
||||
{ timeout: 500 },
|
||||
);
|
||||
|
||||
expect(setDataMaskMock.mock.calls[0][0].extraFormData.filters).toEqual([
|
||||
{
|
||||
col: 'category_column',
|
||||
op: 'IN',
|
||||
val: ['Product A'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('uses rendered categorical axis for query event handlers', () => {
|
||||
render(
|
||||
<EchartsTimeseries
|
||||
{...defaultProps}
|
||||
xAxis={{
|
||||
label: 'category_column',
|
||||
type: AxisType.Category,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getLatestEchartProps().queryEventHandlers?.[0].query).toBe(
|
||||
'xAxis.category',
|
||||
);
|
||||
|
||||
cleanup();
|
||||
mockEchart.mockReset();
|
||||
|
||||
render(
|
||||
<EchartsTimeseries
|
||||
{...defaultProps}
|
||||
formData={{
|
||||
...defaultFormData,
|
||||
orientation: OrientationType.Horizontal,
|
||||
}}
|
||||
xAxis={{
|
||||
label: 'category_column',
|
||||
type: AxisType.Category,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(getLatestEchartProps().queryEventHandlers?.[0].query).toBe(
|
||||
'yAxis.category',
|
||||
);
|
||||
});
|
||||
|
||||
test('emits cross-filter from horizontal categorical axis label clicks', () => {
|
||||
const setDataMaskMock = jest.fn();
|
||||
|
||||
render(
|
||||
<EchartsTimeseries
|
||||
{...defaultProps}
|
||||
emitCrossFilters
|
||||
setDataMask={setDataMaskMock}
|
||||
formData={{
|
||||
...defaultFormData,
|
||||
orientation: OrientationType.Horizontal,
|
||||
}}
|
||||
xAxis={{
|
||||
label: 'category_column',
|
||||
type: AxisType.Category,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const labelClickHandler =
|
||||
getLatestEchartProps().queryEventHandlers?.[0].handler;
|
||||
expect(labelClickHandler).toBeDefined();
|
||||
labelClickHandler?.({
|
||||
value: 'Product A',
|
||||
} as ECElementEvent);
|
||||
|
||||
expect(setDataMaskMock.mock.calls[0][0].extraFormData.filters).toEqual([
|
||||
{
|
||||
col: 'category_column',
|
||||
op: 'IN',
|
||||
val: ['Product A'],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('does not emit duplicate cross-filter for generic axis label clicks', async () => {
|
||||
const setDataMaskMock = jest.fn();
|
||||
|
||||
render(
|
||||
<EchartsTimeseries
|
||||
{...defaultProps}
|
||||
emitCrossFilters
|
||||
setDataMask={setDataMaskMock}
|
||||
xAxis={{
|
||||
label: 'category_column',
|
||||
type: AxisType.Category,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
|
||||
const clickHandler = getLatestEchartProps().eventHandlers?.click;
|
||||
expect(clickHandler).toBeDefined();
|
||||
clickHandler?.({
|
||||
componentType: 'xAxis',
|
||||
name: 'Product A',
|
||||
});
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 400));
|
||||
expect(setDataMaskMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('does not emit cross-filter when no dimensions and time-based X-axis', async () => {
|
||||
const setDataMaskMock = jest.fn();
|
||||
|
||||
@@ -385,6 +534,7 @@ test('does not emit cross-filter when no dimensions and time-based X-axis', asyn
|
||||
const clickHandler = props.eventHandlers?.click;
|
||||
if (clickHandler) {
|
||||
clickHandler({
|
||||
componentType: 'series',
|
||||
seriesName: 'Sales',
|
||||
data: [1609459200000, 100], // Timestamp
|
||||
name: '2021-01-01',
|
||||
@@ -407,6 +557,10 @@ test('emits cross-filter on the category value for a horizontal categorical bar'
|
||||
...defaultProps,
|
||||
emitCrossFilters: true,
|
||||
setDataMask: setDataMaskMock,
|
||||
formData: {
|
||||
...defaultFormData,
|
||||
orientation: OrientationType.Horizontal,
|
||||
},
|
||||
groupby: [], // No dimensions
|
||||
xAxis: {
|
||||
label: 'category_column',
|
||||
@@ -423,6 +577,7 @@ test('emits cross-filter on the category value for a horizontal categorical bar'
|
||||
const clickHandler = props.eventHandlers?.click;
|
||||
if (clickHandler) {
|
||||
clickHandler({
|
||||
componentType: 'series',
|
||||
seriesName: 'Sales', // This is the metric name
|
||||
data: [100, 'Product A'], // Horizontal: value first, category second
|
||||
name: 'Product A',
|
||||
@@ -457,6 +612,10 @@ test('context menu cross-filter uses the category value for a horizontal categor
|
||||
...defaultProps,
|
||||
emitCrossFilters: true,
|
||||
onContextMenu: onContextMenuMock,
|
||||
formData: {
|
||||
...defaultFormData,
|
||||
orientation: OrientationType.Horizontal,
|
||||
},
|
||||
groupby: [], // No dimensions
|
||||
xAxis: {
|
||||
label: 'category_column',
|
||||
@@ -474,6 +633,7 @@ test('context menu cross-filter uses the category value for a horizontal categor
|
||||
expect(contextMenuHandler).toBeDefined();
|
||||
if (contextMenuHandler) {
|
||||
await contextMenuHandler({
|
||||
componentType: 'series',
|
||||
seriesName: 'Sales', // This is the metric name
|
||||
data: [100, 'Product A'], // Horizontal: value first, category second
|
||||
name: 'Product A',
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
DTTM_ALIAS,
|
||||
BinaryQueryObjectFilterClause,
|
||||
@@ -27,12 +27,15 @@ import {
|
||||
LegendState,
|
||||
ensureIsArray,
|
||||
} from '@superset-ui/core';
|
||||
import type { ViewRootGroup } from 'echarts/types/src/util/types';
|
||||
import type {
|
||||
ECElementEvent,
|
||||
ViewRootGroup,
|
||||
} from 'echarts/types/src/util/types';
|
||||
import type GlobalModel from 'echarts/types/src/model/Global';
|
||||
import type ComponentModel from 'echarts/types/src/model/Component';
|
||||
import { EchartsHandler, EventHandlers } from '../types';
|
||||
import Echart from '../components/Echart';
|
||||
import { TimeseriesChartTransformedProps } from './types';
|
||||
import { OrientationType, TimeseriesChartTransformedProps } from './types';
|
||||
import { formatSeriesName } from '../utils/series';
|
||||
import { ExtraControls } from '../components/ExtraControls';
|
||||
|
||||
@@ -218,6 +221,26 @@ export default function EchartsTimeseries({
|
||||
// Determine if X-axis can be used for cross-filtering (categorical axis without dimensions)
|
||||
const canCrossFilterByXAxis =
|
||||
!hasDimensions && xAxis.type === AxisType.Category;
|
||||
const categoryAxisValueIndex =
|
||||
formData.orientation === OrientationType.Horizontal ? 1 : 0;
|
||||
const getCategoryAxisValue = useCallback(
|
||||
(data: unknown, name: unknown) => {
|
||||
if (Array.isArray(data)) {
|
||||
const categoryAxisValue = data[categoryAxisValueIndex];
|
||||
if (
|
||||
typeof categoryAxisValue === 'string' ||
|
||||
typeof categoryAxisValue === 'number'
|
||||
) {
|
||||
return categoryAxisValue;
|
||||
}
|
||||
}
|
||||
if (typeof name === 'string' || typeof name === 'number') {
|
||||
return name;
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
[categoryAxisValueIndex],
|
||||
);
|
||||
|
||||
const eventHandlers: EventHandlers = {
|
||||
click: props => {
|
||||
@@ -234,12 +257,15 @@ export default function EchartsTimeseries({
|
||||
// Cross-filter by dimension (original behavior)
|
||||
const { seriesName: name } = props;
|
||||
handleChange(name);
|
||||
} else if (canCrossFilterByXAxis && props.name != null) {
|
||||
// Cross-filter by X-axis value when no dimensions (issue #25334).
|
||||
// Use `name` (the category-axis value) instead of `data[0]`: for
|
||||
// horizontal bars the data tuple is value-first, so `data[0]` would
|
||||
// be the metric value rather than the category (issue #41102).
|
||||
handleXAxisChange(props.name);
|
||||
} else if (canCrossFilterByXAxis && props.componentType === 'series') {
|
||||
// Cross-filter by X-axis value when no dimensions (issue #25334)
|
||||
const categoryAxisValue = getCategoryAxisValue(
|
||||
props.data,
|
||||
props.name,
|
||||
);
|
||||
if (categoryAxisValue !== undefined) {
|
||||
handleXAxisChange(categoryAxisValue);
|
||||
}
|
||||
}
|
||||
}, TIMER_DURATION);
|
||||
},
|
||||
@@ -321,10 +347,17 @@ export default function EchartsTimeseries({
|
||||
let crossFilter;
|
||||
if (hasDimensions) {
|
||||
crossFilter = getCrossFilterDataMask(seriesName);
|
||||
} else if (canCrossFilterByXAxis && eventParams.name != null) {
|
||||
// Use `name` (the category-axis value), not `data[0]`, so horizontal
|
||||
// bars cross-filter on the category and not the metric (issue #41102).
|
||||
crossFilter = getXAxisCrossFilterDataMask(eventParams.name);
|
||||
} else if (
|
||||
canCrossFilterByXAxis &&
|
||||
eventParams.componentType === 'series'
|
||||
) {
|
||||
const categoryAxisValue = getCategoryAxisValue(
|
||||
data,
|
||||
eventParams.name,
|
||||
);
|
||||
if (categoryAxisValue !== undefined) {
|
||||
crossFilter = getXAxisCrossFilterDataMask(categoryAxisValue);
|
||||
}
|
||||
}
|
||||
|
||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||
@@ -336,6 +369,33 @@ export default function EchartsTimeseries({
|
||||
},
|
||||
};
|
||||
|
||||
const handleXAxisLabelClick = useCallback(
|
||||
(event: ECElementEvent) => {
|
||||
const { value } = event;
|
||||
if (
|
||||
canCrossFilterByXAxis &&
|
||||
(typeof value === 'string' || typeof value === 'number')
|
||||
) {
|
||||
handleXAxisChange(value);
|
||||
}
|
||||
},
|
||||
[canCrossFilterByXAxis, handleXAxisChange],
|
||||
);
|
||||
|
||||
const categoryAxis =
|
||||
formData.orientation === OrientationType.Horizontal ? 'yAxis' : 'xAxis';
|
||||
|
||||
const queryEventHandlers = useMemo(
|
||||
() => [
|
||||
{
|
||||
name: 'click',
|
||||
query: `${categoryAxis}.category`,
|
||||
handler: handleXAxisLabelClick,
|
||||
},
|
||||
],
|
||||
[categoryAxis, handleXAxisLabelClick],
|
||||
);
|
||||
|
||||
const zrEventHandlers: EventHandlers = {
|
||||
dblclick: params => {
|
||||
// clear single click timer
|
||||
@@ -377,6 +437,7 @@ export default function EchartsTimeseries({
|
||||
width={width}
|
||||
echartOptions={echartOptions}
|
||||
eventHandlers={eventHandlers}
|
||||
queryEventHandlers={queryEventHandlers}
|
||||
zrEventHandlers={zrEventHandlers}
|
||||
selectedValues={selectedValues}
|
||||
vizType={formData.vizType}
|
||||
|
||||
@@ -889,6 +889,10 @@ export default function transformProps(
|
||||
name: xAxisTitle,
|
||||
nameGap: convertInteger(xAxisTitleMargin),
|
||||
nameLocation: 'middle',
|
||||
...(xAxisType === AxisType.Category &&
|
||||
groupBy.length === 0 && {
|
||||
triggerEvent: true,
|
||||
}),
|
||||
axisLabel: {
|
||||
// When rotation is applied on time axes, hideOverlap can
|
||||
// aggressively hide the last label. Rotated labels already
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
import { render, waitFor } from '../../../../spec/helpers/testing-library';
|
||||
import type { EChartsCoreOption } from 'echarts/core';
|
||||
import Echart from './Echart';
|
||||
import type { EchartsProps } from '../types';
|
||||
|
||||
type Handler = (params: unknown) => void;
|
||||
type Listener = {
|
||||
query?: string;
|
||||
handler: Handler;
|
||||
};
|
||||
|
||||
const listeners: Record<string, Listener[]> = {};
|
||||
|
||||
const mockChart = {
|
||||
dispatchAction: jest.fn(),
|
||||
dispose: jest.fn(),
|
||||
getOption: jest.fn(() => ({})),
|
||||
getZr: jest.fn(() => ({
|
||||
off: jest.fn(),
|
||||
on: jest.fn(),
|
||||
})),
|
||||
off: jest.fn((name: string, handler?: Handler) => {
|
||||
if (!handler) {
|
||||
delete listeners[name];
|
||||
return;
|
||||
}
|
||||
listeners[name] = (listeners[name] || []).filter(
|
||||
listener => listener.handler !== handler,
|
||||
);
|
||||
}),
|
||||
on: jest.fn(
|
||||
(name: string, queryOrHandler: string | Handler, handler?: Handler) => {
|
||||
listeners[name] = listeners[name] || [];
|
||||
listeners[name].push(
|
||||
handler
|
||||
? { query: queryOrHandler as string, handler }
|
||||
: { handler: queryOrHandler as Handler },
|
||||
);
|
||||
},
|
||||
),
|
||||
resize: jest.fn(),
|
||||
setOption: jest.fn(),
|
||||
};
|
||||
|
||||
jest.mock('echarts/core', () => ({
|
||||
init: jest.fn(() => mockChart),
|
||||
registerLocale: jest.fn(),
|
||||
use: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('echarts/charts', () => ({
|
||||
BarChart: 'BarChart',
|
||||
BoxplotChart: 'BoxplotChart',
|
||||
CustomChart: 'CustomChart',
|
||||
FunnelChart: 'FunnelChart',
|
||||
GaugeChart: 'GaugeChart',
|
||||
GraphChart: 'GraphChart',
|
||||
HeatmapChart: 'HeatmapChart',
|
||||
LineChart: 'LineChart',
|
||||
PieChart: 'PieChart',
|
||||
RadarChart: 'RadarChart',
|
||||
SankeyChart: 'SankeyChart',
|
||||
ScatterChart: 'ScatterChart',
|
||||
SunburstChart: 'SunburstChart',
|
||||
TreeChart: 'TreeChart',
|
||||
TreemapChart: 'TreemapChart',
|
||||
}));
|
||||
|
||||
jest.mock('echarts/components', () => ({
|
||||
AriaComponent: 'AriaComponent',
|
||||
DataZoomComponent: 'DataZoomComponent',
|
||||
GraphicComponent: 'GraphicComponent',
|
||||
GridComponent: 'GridComponent',
|
||||
LegendComponent: 'LegendComponent',
|
||||
MarkAreaComponent: 'MarkAreaComponent',
|
||||
MarkLineComponent: 'MarkLineComponent',
|
||||
TitleComponent: 'TitleComponent',
|
||||
ToolboxComponent: 'ToolboxComponent',
|
||||
TooltipComponent: 'TooltipComponent',
|
||||
VisualMapComponent: 'VisualMapComponent',
|
||||
}));
|
||||
|
||||
jest.mock('echarts/features', () => ({
|
||||
LabelLayout: 'LabelLayout',
|
||||
}));
|
||||
|
||||
jest.mock('echarts/renderers', () => ({
|
||||
CanvasRenderer: 'CanvasRenderer',
|
||||
}));
|
||||
|
||||
const initialState = {
|
||||
common: {
|
||||
locale: 'en',
|
||||
},
|
||||
dashboardState: {
|
||||
isRefreshing: false,
|
||||
},
|
||||
};
|
||||
|
||||
const defaultProps: EchartsProps = {
|
||||
echartOptions: { series: [] } as EChartsCoreOption,
|
||||
height: 100,
|
||||
refs: {},
|
||||
width: 100,
|
||||
};
|
||||
|
||||
const renderEchart = (props: Partial<EchartsProps> = {}) => (
|
||||
<Echart {...defaultProps} {...props} />
|
||||
);
|
||||
|
||||
const trigger = (name: string) => {
|
||||
(listeners[name] || []).forEach(listener => listener.handler({}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.keys(listeners).forEach(name => {
|
||||
delete listeners[name];
|
||||
});
|
||||
Object.values(mockChart).forEach(value => {
|
||||
if (jest.isMockFunction(value)) {
|
||||
value.mockClear();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test('replaces stale query event handlers without clearing regular event handlers', async () => {
|
||||
const regularClickHandler = jest.fn();
|
||||
const firstQueryHandler = jest.fn();
|
||||
const secondQueryHandler = jest.fn();
|
||||
|
||||
const { rerender } = render(
|
||||
renderEchart({
|
||||
eventHandlers: {
|
||||
click: regularClickHandler,
|
||||
},
|
||||
queryEventHandlers: [
|
||||
{
|
||||
handler: firstQueryHandler,
|
||||
name: 'click',
|
||||
query: 'xAxis.category',
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ initialState, useRedux: true },
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockChart.on).toHaveBeenCalledWith(
|
||||
'click',
|
||||
'xAxis.category',
|
||||
firstQueryHandler,
|
||||
),
|
||||
);
|
||||
|
||||
rerender(
|
||||
renderEchart({
|
||||
eventHandlers: {
|
||||
click: regularClickHandler,
|
||||
},
|
||||
queryEventHandlers: [
|
||||
{
|
||||
handler: secondQueryHandler,
|
||||
name: 'click',
|
||||
query: 'xAxis.category',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockChart.on).toHaveBeenCalledWith(
|
||||
'click',
|
||||
'xAxis.category',
|
||||
secondQueryHandler,
|
||||
),
|
||||
);
|
||||
|
||||
trigger('click');
|
||||
|
||||
expect(regularClickHandler).toHaveBeenCalledTimes(1);
|
||||
expect(firstQueryHandler).not.toHaveBeenCalled();
|
||||
expect(secondQueryHandler).toHaveBeenCalledTimes(1);
|
||||
|
||||
regularClickHandler.mockClear();
|
||||
secondQueryHandler.mockClear();
|
||||
|
||||
rerender(
|
||||
renderEchart({
|
||||
eventHandlers: {
|
||||
click: regularClickHandler,
|
||||
},
|
||||
queryEventHandlers: [],
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(mockChart.off).toHaveBeenCalledWith('click', secondQueryHandler),
|
||||
);
|
||||
|
||||
trigger('click');
|
||||
|
||||
expect(regularClickHandler).toHaveBeenCalledTimes(1);
|
||||
expect(firstQueryHandler).not.toHaveBeenCalled();
|
||||
expect(secondQueryHandler).not.toHaveBeenCalled();
|
||||
});
|
||||
@@ -64,7 +64,12 @@ import {
|
||||
MarkLineComponent,
|
||||
} from 'echarts/components';
|
||||
import { LabelLayout } from 'echarts/features';
|
||||
import { EchartsHandler, EchartsProps, EchartsStylesProps } from '../types';
|
||||
import {
|
||||
EchartsHandler,
|
||||
EchartsProps,
|
||||
EchartsStylesProps,
|
||||
QueryEventHandlers,
|
||||
} from '../types';
|
||||
import { DEFAULT_LOCALE } from '../constants';
|
||||
import { mergeEchartsThemeOverrides } from '../utils/themeOverrides';
|
||||
|
||||
@@ -132,6 +137,7 @@ function Echart(
|
||||
height,
|
||||
echartOptions,
|
||||
eventHandlers,
|
||||
queryEventHandlers,
|
||||
zrEventHandlers,
|
||||
selectedValues = {},
|
||||
refs,
|
||||
@@ -147,6 +153,7 @@ function Echart(
|
||||
}
|
||||
const [didMount, setDidMount] = useState(false);
|
||||
const chartRef = useRef<EChartsType>();
|
||||
const previousQueryEventHandlers = useRef<QueryEventHandlers>([]);
|
||||
const currentSelection = useMemo(
|
||||
() => Object.keys(selectedValues) || [],
|
||||
[selectedValues],
|
||||
@@ -196,11 +203,19 @@ function Echart(
|
||||
|
||||
useEffect(() => {
|
||||
if (didMount) {
|
||||
previousQueryEventHandlers.current.forEach(({ name, handler }) => {
|
||||
chartRef.current?.off(name, handler);
|
||||
});
|
||||
Object.entries(eventHandlers || {}).forEach(([name, handler]) => {
|
||||
chartRef.current?.off(name);
|
||||
chartRef.current?.on(name, handler);
|
||||
});
|
||||
|
||||
(queryEventHandlers || []).forEach(({ name, query, handler }) => {
|
||||
chartRef.current?.on(name, query, handler);
|
||||
});
|
||||
previousQueryEventHandlers.current = queryEventHandlers || [];
|
||||
|
||||
Object.entries(zrEventHandlers || {}).forEach(([name, handler]) => {
|
||||
chartRef.current?.getZr().off(name);
|
||||
chartRef.current?.getZr().on(name, handler);
|
||||
@@ -336,7 +351,15 @@ function Echart(
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- isDashboardRefreshing intentionally excluded to prevent extra setOption calls
|
||||
}, [didMount, echartOptions, eventHandlers, zrEventHandlers, theme, vizType]);
|
||||
}, [
|
||||
didMount,
|
||||
echartOptions,
|
||||
eventHandlers,
|
||||
queryEventHandlers,
|
||||
zrEventHandlers,
|
||||
theme,
|
||||
vizType,
|
||||
]);
|
||||
|
||||
// Clear tooltip on refresh start to avoid stale content (#39247)
|
||||
useEffect(() => {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {
|
||||
} from '@superset-ui/core';
|
||||
import type { EChartsCoreOption, EChartsType } from 'echarts/core';
|
||||
import type { TooltipMarker } from 'echarts/types/src/util/format';
|
||||
import type { ECElementEvent } from 'echarts/types/src/util/types';
|
||||
import { StackControlsValue } from './constants';
|
||||
|
||||
export type EchartsStylesProps = {
|
||||
@@ -51,6 +52,7 @@ export interface EchartsProps {
|
||||
width: number;
|
||||
echartOptions: EChartsCoreOption;
|
||||
eventHandlers?: EventHandlers;
|
||||
queryEventHandlers?: QueryEventHandlers;
|
||||
zrEventHandlers?: EventHandlers;
|
||||
selectedValues?: Record<number, string>;
|
||||
forceClear?: boolean;
|
||||
@@ -105,6 +107,12 @@ export type LegendFormData = {
|
||||
|
||||
export type EventHandlers = Record<string, { (props: any): void }>;
|
||||
|
||||
export type QueryEventHandlers = {
|
||||
name: string;
|
||||
query: string;
|
||||
handler: (props: ECElementEvent) => void;
|
||||
}[];
|
||||
|
||||
export enum LabelPositionEnum {
|
||||
Top = 'top',
|
||||
Left = 'left',
|
||||
|
||||
@@ -1564,9 +1564,13 @@ test('xAxisForceCategorical forces Category axis regardless of Numeric coltype',
|
||||
});
|
||||
|
||||
const { echartOptions } = transformProps(chartProps);
|
||||
const xAxis = echartOptions.xAxis as { type: string };
|
||||
const xAxis = echartOptions.xAxis as {
|
||||
triggerEvent?: boolean;
|
||||
type: string;
|
||||
};
|
||||
|
||||
expect(xAxis.type).toBe(AxisType.Category);
|
||||
expect(xAxis.triggerEvent).toBe(true);
|
||||
});
|
||||
|
||||
test('temporal x coltype wires the time formatter and Time axis', () => {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* you may not use this file except in compliance with the License. You may obtain
|
||||
* a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software distributed
|
||||
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||
@@ -36,80 +36,77 @@ const mockedProps = {
|
||||
resourceName: 'dashboard',
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('BulkTagModal', () => {
|
||||
afterEach(() => {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
jest.clearAllMocks();
|
||||
afterEach(() => {
|
||||
fetchMock.clearHistory().removeRoutes();
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = render(<BulkTagModal {...mockedProps} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders the correct title and message', () => {
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
expect(
|
||||
screen.getByText(/you are adding tags to 2 dashboards/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Bulk tag')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('renders tags input field', async () => {
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
||||
expect(tagsInput).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('calls onHide when the Cancel button is clicked', () => {
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
const cancelButton = screen.getByText('Cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
expect(mockedProps.onHide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('submits the selected tags and shows success toast', async () => {
|
||||
fetchMock.post('glob:*/api/v1/tag/bulk_create', {
|
||||
result: {
|
||||
objects_tagged: [1, 2],
|
||||
objects_skipped: [],
|
||||
},
|
||||
});
|
||||
|
||||
test('should render', () => {
|
||||
const { container } = render(<BulkTagModal {...mockedProps} />);
|
||||
expect(container).toBeInTheDocument();
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
|
||||
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
||||
fireEvent.change(tagsInput, { target: { value: 'Test Tag' } });
|
||||
fireEvent.keyDown(tagsInput, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
fireEvent.click(screen.getByText('Save'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedProps.addSuccessToast).toHaveBeenCalledWith(
|
||||
'Tagged 2 dashboards',
|
||||
);
|
||||
});
|
||||
|
||||
test('renders the correct title and message', () => {
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
expect(
|
||||
screen.getByText(/you are adding tags to 2 dashboards/i),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Bulk tag')).toBeInTheDocument();
|
||||
});
|
||||
expect(mockedProps.refreshData).toHaveBeenCalled();
|
||||
expect(mockedProps.onHide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('renders tags input field', async () => {
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
||||
expect(tagsInput).toBeInTheDocument();
|
||||
});
|
||||
test('handles API errors gracefully', async () => {
|
||||
fetchMock.post('glob:*/api/v1/tag/bulk_create', 500);
|
||||
|
||||
test('calls onHide when the Cancel button is clicked', () => {
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
const cancelButton = screen.getByText('Cancel');
|
||||
fireEvent.click(cancelButton);
|
||||
expect(mockedProps.onHide).toHaveBeenCalled();
|
||||
});
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
|
||||
test('submits the selected tags and shows success toast', async () => {
|
||||
fetchMock.post('glob:*/api/v1/tag/bulk_create', {
|
||||
result: {
|
||||
objects_tagged: [1, 2],
|
||||
objects_skipped: [],
|
||||
},
|
||||
});
|
||||
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
||||
fireEvent.change(tagsInput, { target: { value: 'Test Tag' } });
|
||||
fireEvent.keyDown(tagsInput, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
fireEvent.click(screen.getByText('Save'));
|
||||
|
||||
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
||||
fireEvent.change(tagsInput, { target: { value: 'Test Tag' } });
|
||||
fireEvent.keyDown(tagsInput, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
fireEvent.click(screen.getByText('Save'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedProps.addSuccessToast).toHaveBeenCalledWith(
|
||||
'Tagged 2 dashboards',
|
||||
);
|
||||
});
|
||||
|
||||
expect(mockedProps.refreshData).toHaveBeenCalled();
|
||||
expect(mockedProps.onHide).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('handles API errors gracefully', async () => {
|
||||
fetchMock.post('glob:*/api/v1/tag/bulk_create', 500);
|
||||
|
||||
render(<BulkTagModal {...mockedProps} />);
|
||||
|
||||
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
||||
fireEvent.change(tagsInput, { target: { value: 'Test Tag' } });
|
||||
fireEvent.keyDown(tagsInput, { key: 'Enter', code: 'Enter' });
|
||||
|
||||
fireEvent.click(screen.getByText('Save'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockedProps.addDangerToast).toHaveBeenCalledWith(
|
||||
'Failed to tag items',
|
||||
);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockedProps.addDangerToast).toHaveBeenCalledWith(
|
||||
'Failed to tag items',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -38,7 +38,6 @@ const TestComponent = (props: ThemeSubMenuProps) => {
|
||||
return <Menu items={[menuItem]} />;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('useThemeMenuItems', () => {
|
||||
const defaultProps = {
|
||||
allowOSPreference: true,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
@@ -27,133 +27,127 @@ import {
|
||||
ColumnDefinition,
|
||||
} from 'src/utils/common';
|
||||
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('utils/common', () => {
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('optionFromValue', () => {
|
||||
test('converts values as expected', () => {
|
||||
expect(optionFromValue(false)).toEqual({
|
||||
value: false,
|
||||
label: FALSE_STRING,
|
||||
});
|
||||
expect(optionFromValue(true)).toEqual({
|
||||
value: true,
|
||||
label: TRUE_STRING,
|
||||
});
|
||||
expect(optionFromValue(null)).toEqual({
|
||||
value: NULL_STRING,
|
||||
label: NULL_STRING,
|
||||
});
|
||||
expect(optionFromValue('')).toEqual({
|
||||
value: '',
|
||||
label: '<empty string>',
|
||||
});
|
||||
expect(optionFromValue('foo')).toEqual({ value: 'foo', label: 'foo' });
|
||||
expect(optionFromValue(5)).toEqual({ value: 5, label: '5' });
|
||||
});
|
||||
test('converts values as expected', () => {
|
||||
expect(optionFromValue(false)).toEqual({
|
||||
value: false,
|
||||
label: FALSE_STRING,
|
||||
});
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('prepareCopyToClipboardTabularData', () => {
|
||||
test('converts empty array', () => {
|
||||
const data: TabularDataRow[] = [];
|
||||
const columns: string[] = [];
|
||||
expect(prepareCopyToClipboardTabularData(data, columns)).toEqual('');
|
||||
});
|
||||
test('converts non empty array', () => {
|
||||
const data: TabularDataRow[] = [
|
||||
{ column1: 'lorem', column2: 'ipsum' },
|
||||
{ column1: 'dolor', column2: 'sit', column3: 'amet' },
|
||||
];
|
||||
const columns: string[] = ['column1', 'column2', 'column3'];
|
||||
expect(prepareCopyToClipboardTabularData(data, columns)).toEqual(
|
||||
'column1\tcolumn2\tcolumn3\nlorem\tipsum\t\ndolor\tsit\tamet\n',
|
||||
);
|
||||
});
|
||||
test('includes 0 values and handle column objects', () => {
|
||||
const data: TabularDataRow[] = [
|
||||
{ column1: 0, column2: 0 },
|
||||
{ column1: 1, column2: -1, 0: 0 },
|
||||
];
|
||||
const columns: ColumnDefinition[] = [
|
||||
{ name: 'column1' },
|
||||
{ name: 'column2' },
|
||||
{ name: '0' },
|
||||
];
|
||||
expect(prepareCopyToClipboardTabularData(data, columns)).toEqual(
|
||||
'column1\tcolumn2\t0\n0\t0\t\n1\t-1\t0\n',
|
||||
);
|
||||
});
|
||||
expect(optionFromValue(true)).toEqual({
|
||||
value: true,
|
||||
label: TRUE_STRING,
|
||||
});
|
||||
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||
describe('applyFormattingToTabularData', () => {
|
||||
test('does not mutate empty array', () => {
|
||||
const data: TabularDataRow[] = [];
|
||||
expect(applyFormattingToTabularData(data, [])).toEqual(data);
|
||||
});
|
||||
test('does not mutate array without temporal column', () => {
|
||||
const data: TabularDataRow[] = [
|
||||
{ column1: 'lorem', column2: 'ipsum' },
|
||||
{ column1: 'dolor', column2: 'sit', column3: 'amet' },
|
||||
];
|
||||
expect(applyFormattingToTabularData(data, [])).toEqual(data);
|
||||
});
|
||||
test('changes formatting of columns selected for formatting', () => {
|
||||
const originalData: TabularDataRow[] = [
|
||||
{
|
||||
__timestamp: null,
|
||||
column1: 'lorem',
|
||||
column2: 1590014060000,
|
||||
column3: 1507680000000,
|
||||
},
|
||||
{
|
||||
__timestamp: 0,
|
||||
column1: 'ipsum',
|
||||
column2: 1590075817000,
|
||||
column3: 1513641600000,
|
||||
},
|
||||
{
|
||||
__timestamp: 1594285437771,
|
||||
column1: 'dolor',
|
||||
column2: 1591062977000,
|
||||
column3: 1516924800000,
|
||||
},
|
||||
{
|
||||
__timestamp: 1594285441675,
|
||||
column1: 'sit',
|
||||
column2: 1591397351000,
|
||||
column3: 1518566400000,
|
||||
},
|
||||
];
|
||||
const timeFormattedColumns: string[] = ['__timestamp', 'column3'];
|
||||
const expectedData: TabularDataRow[] = [
|
||||
{
|
||||
__timestamp: null,
|
||||
column1: 'lorem',
|
||||
column2: 1590014060000,
|
||||
column3: '2017-10-11 00:00:00',
|
||||
},
|
||||
{
|
||||
__timestamp: '1970-01-01 00:00:00',
|
||||
column1: 'ipsum',
|
||||
column2: 1590075817000,
|
||||
column3: '2017-12-19 00:00:00',
|
||||
},
|
||||
{
|
||||
__timestamp: '2020-07-09 09:03:57',
|
||||
column1: 'dolor',
|
||||
column2: 1591062977000,
|
||||
column3: '2018-01-26 00:00:00',
|
||||
},
|
||||
{
|
||||
__timestamp: '2020-07-09 09:04:01',
|
||||
column1: 'sit',
|
||||
column2: 1591397351000,
|
||||
column3: '2018-02-14 00:00:00',
|
||||
},
|
||||
];
|
||||
expect(
|
||||
applyFormattingToTabularData(originalData, timeFormattedColumns),
|
||||
).toEqual(expectedData);
|
||||
});
|
||||
expect(optionFromValue(null)).toEqual({
|
||||
value: NULL_STRING,
|
||||
label: NULL_STRING,
|
||||
});
|
||||
expect(optionFromValue('')).toEqual({
|
||||
value: '',
|
||||
label: '<empty string>',
|
||||
});
|
||||
expect(optionFromValue('foo')).toEqual({ value: 'foo', label: 'foo' });
|
||||
expect(optionFromValue(5)).toEqual({ value: 5, label: '5' });
|
||||
});
|
||||
|
||||
test('converts empty array', () => {
|
||||
const data: TabularDataRow[] = [];
|
||||
const columns: string[] = [];
|
||||
expect(prepareCopyToClipboardTabularData(data, columns)).toEqual('');
|
||||
});
|
||||
|
||||
test('converts non empty array', () => {
|
||||
const data: TabularDataRow[] = [
|
||||
{ column1: 'lorem', column2: 'ipsum' },
|
||||
{ column1: 'dolor', column2: 'sit', column3: 'amet' },
|
||||
];
|
||||
const columns: string[] = ['column1', 'column2', 'column3'];
|
||||
expect(prepareCopyToClipboardTabularData(data, columns)).toEqual(
|
||||
'column1\tcolumn2\tcolumn3\nlorem\tipsum\t\ndolor\tsit\tamet\n',
|
||||
);
|
||||
});
|
||||
|
||||
test('includes 0 values and handle column objects', () => {
|
||||
const data: TabularDataRow[] = [
|
||||
{ column1: 0, column2: 0 },
|
||||
{ column1: 1, column2: -1, 0: 0 },
|
||||
];
|
||||
const columns: ColumnDefinition[] = [
|
||||
{ name: 'column1' },
|
||||
{ name: 'column2' },
|
||||
{ name: '0' },
|
||||
];
|
||||
expect(prepareCopyToClipboardTabularData(data, columns)).toEqual(
|
||||
'column1\tcolumn2\t0\n0\t0\t\n1\t-1\t0\n',
|
||||
);
|
||||
});
|
||||
|
||||
test('does not mutate empty array', () => {
|
||||
const data: TabularDataRow[] = [];
|
||||
expect(applyFormattingToTabularData(data, [])).toEqual(data);
|
||||
});
|
||||
|
||||
test('does not mutate array without temporal column', () => {
|
||||
const data: TabularDataRow[] = [
|
||||
{ column1: 'lorem', column2: 'ipsum' },
|
||||
{ column1: 'dolor', column2: 'sit', column3: 'amet' },
|
||||
];
|
||||
expect(applyFormattingToTabularData(data, [])).toEqual(data);
|
||||
});
|
||||
|
||||
test('changes formatting of columns selected for formatting', () => {
|
||||
const originalData: TabularDataRow[] = [
|
||||
{
|
||||
__timestamp: null,
|
||||
column1: 'lorem',
|
||||
column2: 1590014060000,
|
||||
column3: 1507680000000,
|
||||
},
|
||||
{
|
||||
__timestamp: 0,
|
||||
column1: 'ipsum',
|
||||
column2: 1590075817000,
|
||||
column3: 1513641600000,
|
||||
},
|
||||
{
|
||||
__timestamp: 1594285437771,
|
||||
column1: 'dolor',
|
||||
column2: 1591062977000,
|
||||
column3: 1516924800000,
|
||||
},
|
||||
{
|
||||
__timestamp: 1594285441675,
|
||||
column1: 'sit',
|
||||
column2: 1591397351000,
|
||||
column3: 1518566400000,
|
||||
},
|
||||
];
|
||||
const timeFormattedColumns: string[] = ['__timestamp', 'column3'];
|
||||
const expectedData: TabularDataRow[] = [
|
||||
{
|
||||
__timestamp: null,
|
||||
column1: 'lorem',
|
||||
column2: 1590014060000,
|
||||
column3: '2017-10-11 00:00:00',
|
||||
},
|
||||
{
|
||||
__timestamp: '1970-01-01 00:00:00',
|
||||
column1: 'ipsum',
|
||||
column2: 1590075817000,
|
||||
column3: '2017-12-19 00:00:00',
|
||||
},
|
||||
{
|
||||
__timestamp: '2020-07-09 09:03:57',
|
||||
column1: 'dolor',
|
||||
column2: 1591062977000,
|
||||
column3: '2018-01-26 00:00:00',
|
||||
},
|
||||
{
|
||||
__timestamp: '2020-07-09 09:04:01',
|
||||
column1: 'sit',
|
||||
column2: 1591397351000,
|
||||
column3: '2018-02-14 00:00:00',
|
||||
},
|
||||
];
|
||||
expect(
|
||||
applyFormattingToTabularData(originalData, timeFormattedColumns),
|
||||
).toEqual(expectedData);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user