mirror of
https://github.com/apache/superset.git
synced 2026-07-10 16:55:30 +00:00
Compare commits
1 Commits
superset-h
...
chore/ci/s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c32284d56a |
2
.github/workflows/bump-python-package.yml
vendored
2
.github/workflows/bump-python-package.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
|||||||
uses: ./.github/actions/setup-supersetbot/
|
uses: ./.github/actions/setup-supersetbot/
|
||||||
|
|
||||||
- name: Set up Python ${{ inputs.python-version }}
|
- name: Set up Python ${{ inputs.python-version }}
|
||||||
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
|
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
|
||||||
with:
|
with:
|
||||||
python-version: "3.10"
|
python-version: "3.10"
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ maintainers:
|
|||||||
- name: craig-rueda
|
- name: craig-rueda
|
||||||
email: craig@craigrueda.com
|
email: craig@craigrueda.com
|
||||||
url: https://github.com/craig-rueda
|
url: https://github.com/craig-rueda
|
||||||
version: 0.17.3 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
version: 0.17.2 # See [README](https://github.com/apache/superset/blob/master/helm/superset/README.md#versioning) for version details.
|
||||||
dependencies:
|
dependencies:
|
||||||
- name: postgresql
|
- name: postgresql
|
||||||
version: 16.7.27
|
version: 16.7.27
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ NOTE: This file is generated by helm-docs: https://github.com/norwoodj/helm-docs
|
|||||||
|
|
||||||
# superset
|
# superset
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Apache Superset is a modern, enterprise-ready business intelligence web application
|
Apache Superset is a modern, enterprise-ready business intelligence web application
|
||||||
|
|
||||||
|
|||||||
@@ -108,6 +108,8 @@ else:
|
|||||||
{{ fail (printf "Unsupported database type: %s. Please use 'postgresql' or 'mysql'." .Values.supersetNode.connections.db_type) }}
|
{{ fail (printf "Unsupported database type: %s. Please use 'postgresql' or 'mysql'." .Values.supersetNode.connections.db_type) }}
|
||||||
{{- end }}
|
{{- end }}
|
||||||
|
|
||||||
|
SQLALCHEMY_TRACK_MODIFICATIONS = True
|
||||||
|
|
||||||
class CeleryConfig:
|
class CeleryConfig:
|
||||||
imports = ("superset.sql_lab", )
|
imports = ("superset.sql_lab", )
|
||||||
broker_url = CELERY_REDIS_URL
|
broker_url = CELERY_REDIS_URL
|
||||||
|
|||||||
@@ -231,56 +231,6 @@ describe('BigNumberTotal transformProps', () => {
|
|||||||
expect(result.headerFormatter(500)).toBe('$500');
|
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', () => {
|
test('should propagate colorThresholdFormatters from getColorFormatters', () => {
|
||||||
// Override the getColorFormatters mock to return specific value
|
// Override the getColorFormatters mock to return specific value
|
||||||
const mockFormatters = [{ formatter: 'red' }];
|
const mockFormatters = [{ formatter: 'red' }];
|
||||||
|
|||||||
@@ -79,15 +79,8 @@ export default function transformProps(
|
|||||||
const formattedSubtitleFontSize = subtitle?.trim()
|
const formattedSubtitleFontSize = subtitle?.trim()
|
||||||
? (subtitleFontSize ?? PROPORTION.SUBHEADER)
|
? (subtitleFontSize ?? PROPORTION.SUBHEADER)
|
||||||
: (subheaderFontSize ?? 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 =
|
const bigNumber =
|
||||||
parsedValue === null &&
|
data.length === 0 ? null : parseMetricValue(data[0][metricName]);
|
||||||
typeof rawValue === 'string' &&
|
|
||||||
rawValue.trim() !== ''
|
|
||||||
? rawValue
|
|
||||||
: parsedValue;
|
|
||||||
|
|
||||||
let metricEntry: Metric | undefined;
|
let metricEntry: Metric | undefined;
|
||||||
if (chartProps.datasource?.metrics) {
|
if (chartProps.datasource?.metrics) {
|
||||||
|
|||||||
@@ -189,10 +189,8 @@ function BigNumberVis({
|
|||||||
text = t('No data');
|
text = t('No data');
|
||||||
} else if (typeof bigNumber === 'number') {
|
} else if (typeof bigNumber === 'number') {
|
||||||
text = headerFormatter(bigNumber);
|
text = headerFormatter(bigNumber);
|
||||||
} else if (typeof bigNumber === 'string') {
|
|
||||||
text = bigNumber;
|
|
||||||
} else {
|
} else {
|
||||||
// For boolean/Date values, convert to number if possible, else show as string
|
// For string/boolean/Date values, convert to number if possible, else show as string
|
||||||
const numValue = Number(bigNumber);
|
const numValue = Number(bigNumber);
|
||||||
text = Number.isNaN(numValue)
|
text = Number.isNaN(numValue)
|
||||||
? String(bigNumber)
|
? String(bigNumber)
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import {
|
|||||||
} from '../../../../spec/helpers/testing-library';
|
} from '../../../../spec/helpers/testing-library';
|
||||||
import { AxisType } from '@superset-ui/core';
|
import { AxisType } from '@superset-ui/core';
|
||||||
import type { EChartsCoreOption } from 'echarts/core';
|
import type { EChartsCoreOption } from 'echarts/core';
|
||||||
import type { ECElementEvent } from 'echarts/types/src/util/types';
|
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import {
|
import {
|
||||||
LegendOrientation,
|
LegendOrientation,
|
||||||
@@ -203,15 +202,11 @@ const defaultProps: TimeseriesChartTransformedProps = {
|
|||||||
onFocusedSeries: jest.fn(),
|
onFocusedSeries: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
function getLatestEchartProps() {
|
function getLatestHeight() {
|
||||||
const lastCall = mockEchart.mock.calls.at(-1);
|
const lastCall = mockEchart.mock.calls.at(-1);
|
||||||
expect(lastCall).toBeDefined();
|
expect(lastCall).toBeDefined();
|
||||||
const [props] = lastCall as [EchartsProps];
|
const [props] = lastCall as [EchartsProps];
|
||||||
return props;
|
return props.height;
|
||||||
}
|
|
||||||
|
|
||||||
function getLatestHeight() {
|
|
||||||
return getLatestEchartProps().height;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
test('observes extra control height changes when ResizeObserver is available', async () => {
|
test('observes extra control height changes when ResizeObserver is available', async () => {
|
||||||
@@ -340,7 +335,6 @@ test('emits cross-filter on X-axis value when no dimensions and categorical X-ax
|
|||||||
const clickHandler = props.eventHandlers?.click;
|
const clickHandler = props.eventHandlers?.click;
|
||||||
if (clickHandler) {
|
if (clickHandler) {
|
||||||
clickHandler({
|
clickHandler({
|
||||||
componentType: 'series',
|
|
||||||
seriesName: 'Sales', // This is the metric name
|
seriesName: 'Sales', // This is the metric name
|
||||||
data: ['Product A', 100], // X-axis value is 'Product A'
|
data: ['Product A', 100], // X-axis value is 'Product A'
|
||||||
name: 'Product A',
|
name: 'Product A',
|
||||||
@@ -367,149 +361,6 @@ 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 () => {
|
test('does not emit cross-filter when no dimensions and time-based X-axis', async () => {
|
||||||
const setDataMaskMock = jest.fn();
|
const setDataMaskMock = jest.fn();
|
||||||
|
|
||||||
@@ -534,7 +385,6 @@ test('does not emit cross-filter when no dimensions and time-based X-axis', asyn
|
|||||||
const clickHandler = props.eventHandlers?.click;
|
const clickHandler = props.eventHandlers?.click;
|
||||||
if (clickHandler) {
|
if (clickHandler) {
|
||||||
clickHandler({
|
clickHandler({
|
||||||
componentType: 'series',
|
|
||||||
seriesName: 'Sales',
|
seriesName: 'Sales',
|
||||||
data: [1609459200000, 100], // Timestamp
|
data: [1609459200000, 100], // Timestamp
|
||||||
name: '2021-01-01',
|
name: '2021-01-01',
|
||||||
@@ -557,10 +407,6 @@ test('emits cross-filter on the category value for a horizontal categorical bar'
|
|||||||
...defaultProps,
|
...defaultProps,
|
||||||
emitCrossFilters: true,
|
emitCrossFilters: true,
|
||||||
setDataMask: setDataMaskMock,
|
setDataMask: setDataMaskMock,
|
||||||
formData: {
|
|
||||||
...defaultFormData,
|
|
||||||
orientation: OrientationType.Horizontal,
|
|
||||||
},
|
|
||||||
groupby: [], // No dimensions
|
groupby: [], // No dimensions
|
||||||
xAxis: {
|
xAxis: {
|
||||||
label: 'category_column',
|
label: 'category_column',
|
||||||
@@ -577,7 +423,6 @@ test('emits cross-filter on the category value for a horizontal categorical bar'
|
|||||||
const clickHandler = props.eventHandlers?.click;
|
const clickHandler = props.eventHandlers?.click;
|
||||||
if (clickHandler) {
|
if (clickHandler) {
|
||||||
clickHandler({
|
clickHandler({
|
||||||
componentType: 'series',
|
|
||||||
seriesName: 'Sales', // This is the metric name
|
seriesName: 'Sales', // This is the metric name
|
||||||
data: [100, 'Product A'], // Horizontal: value first, category second
|
data: [100, 'Product A'], // Horizontal: value first, category second
|
||||||
name: 'Product A',
|
name: 'Product A',
|
||||||
@@ -612,10 +457,6 @@ test('context menu cross-filter uses the category value for a horizontal categor
|
|||||||
...defaultProps,
|
...defaultProps,
|
||||||
emitCrossFilters: true,
|
emitCrossFilters: true,
|
||||||
onContextMenu: onContextMenuMock,
|
onContextMenu: onContextMenuMock,
|
||||||
formData: {
|
|
||||||
...defaultFormData,
|
|
||||||
orientation: OrientationType.Horizontal,
|
|
||||||
},
|
|
||||||
groupby: [], // No dimensions
|
groupby: [], // No dimensions
|
||||||
xAxis: {
|
xAxis: {
|
||||||
label: 'category_column',
|
label: 'category_column',
|
||||||
@@ -633,7 +474,6 @@ test('context menu cross-filter uses the category value for a horizontal categor
|
|||||||
expect(contextMenuHandler).toBeDefined();
|
expect(contextMenuHandler).toBeDefined();
|
||||||
if (contextMenuHandler) {
|
if (contextMenuHandler) {
|
||||||
await contextMenuHandler({
|
await contextMenuHandler({
|
||||||
componentType: 'series',
|
|
||||||
seriesName: 'Sales', // This is the metric name
|
seriesName: 'Sales', // This is the metric name
|
||||||
data: [100, 'Product A'], // Horizontal: value first, category second
|
data: [100, 'Product A'], // Horizontal: value first, category second
|
||||||
name: 'Product A',
|
name: 'Product A',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
* specific language governing permissions and limitations
|
* specific language governing permissions and limitations
|
||||||
* under the License.
|
* under the License.
|
||||||
*/
|
*/
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
DTTM_ALIAS,
|
DTTM_ALIAS,
|
||||||
BinaryQueryObjectFilterClause,
|
BinaryQueryObjectFilterClause,
|
||||||
@@ -27,15 +27,12 @@ import {
|
|||||||
LegendState,
|
LegendState,
|
||||||
ensureIsArray,
|
ensureIsArray,
|
||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import type {
|
import type { ViewRootGroup } from 'echarts/types/src/util/types';
|
||||||
ECElementEvent,
|
|
||||||
ViewRootGroup,
|
|
||||||
} from 'echarts/types/src/util/types';
|
|
||||||
import type GlobalModel from 'echarts/types/src/model/Global';
|
import type GlobalModel from 'echarts/types/src/model/Global';
|
||||||
import type ComponentModel from 'echarts/types/src/model/Component';
|
import type ComponentModel from 'echarts/types/src/model/Component';
|
||||||
import { EchartsHandler, EventHandlers } from '../types';
|
import { EchartsHandler, EventHandlers } from '../types';
|
||||||
import Echart from '../components/Echart';
|
import Echart from '../components/Echart';
|
||||||
import { OrientationType, TimeseriesChartTransformedProps } from './types';
|
import { TimeseriesChartTransformedProps } from './types';
|
||||||
import { formatSeriesName } from '../utils/series';
|
import { formatSeriesName } from '../utils/series';
|
||||||
import { ExtraControls } from '../components/ExtraControls';
|
import { ExtraControls } from '../components/ExtraControls';
|
||||||
|
|
||||||
@@ -221,26 +218,6 @@ export default function EchartsTimeseries({
|
|||||||
// Determine if X-axis can be used for cross-filtering (categorical axis without dimensions)
|
// Determine if X-axis can be used for cross-filtering (categorical axis without dimensions)
|
||||||
const canCrossFilterByXAxis =
|
const canCrossFilterByXAxis =
|
||||||
!hasDimensions && xAxis.type === AxisType.Category;
|
!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 = {
|
const eventHandlers: EventHandlers = {
|
||||||
click: props => {
|
click: props => {
|
||||||
@@ -257,15 +234,12 @@ export default function EchartsTimeseries({
|
|||||||
// Cross-filter by dimension (original behavior)
|
// Cross-filter by dimension (original behavior)
|
||||||
const { seriesName: name } = props;
|
const { seriesName: name } = props;
|
||||||
handleChange(name);
|
handleChange(name);
|
||||||
} else if (canCrossFilterByXAxis && props.componentType === 'series') {
|
} else if (canCrossFilterByXAxis && props.name != null) {
|
||||||
// Cross-filter by X-axis value when no dimensions (issue #25334)
|
// Cross-filter by X-axis value when no dimensions (issue #25334).
|
||||||
const categoryAxisValue = getCategoryAxisValue(
|
// Use `name` (the category-axis value) instead of `data[0]`: for
|
||||||
props.data,
|
// horizontal bars the data tuple is value-first, so `data[0]` would
|
||||||
props.name,
|
// be the metric value rather than the category (issue #41102).
|
||||||
);
|
handleXAxisChange(props.name);
|
||||||
if (categoryAxisValue !== undefined) {
|
|
||||||
handleXAxisChange(categoryAxisValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, TIMER_DURATION);
|
}, TIMER_DURATION);
|
||||||
},
|
},
|
||||||
@@ -347,17 +321,10 @@ export default function EchartsTimeseries({
|
|||||||
let crossFilter;
|
let crossFilter;
|
||||||
if (hasDimensions) {
|
if (hasDimensions) {
|
||||||
crossFilter = getCrossFilterDataMask(seriesName);
|
crossFilter = getCrossFilterDataMask(seriesName);
|
||||||
} else if (
|
} else if (canCrossFilterByXAxis && eventParams.name != null) {
|
||||||
canCrossFilterByXAxis &&
|
// Use `name` (the category-axis value), not `data[0]`, so horizontal
|
||||||
eventParams.componentType === 'series'
|
// bars cross-filter on the category and not the metric (issue #41102).
|
||||||
) {
|
crossFilter = getXAxisCrossFilterDataMask(eventParams.name);
|
||||||
const categoryAxisValue = getCategoryAxisValue(
|
|
||||||
data,
|
|
||||||
eventParams.name,
|
|
||||||
);
|
|
||||||
if (categoryAxisValue !== undefined) {
|
|
||||||
crossFilter = getXAxisCrossFilterDataMask(categoryAxisValue);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
onContextMenu(pointerEvent.clientX, pointerEvent.clientY, {
|
||||||
@@ -369,33 +336,6 @@ 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 = {
|
const zrEventHandlers: EventHandlers = {
|
||||||
dblclick: params => {
|
dblclick: params => {
|
||||||
// clear single click timer
|
// clear single click timer
|
||||||
@@ -437,7 +377,6 @@ export default function EchartsTimeseries({
|
|||||||
width={width}
|
width={width}
|
||||||
echartOptions={echartOptions}
|
echartOptions={echartOptions}
|
||||||
eventHandlers={eventHandlers}
|
eventHandlers={eventHandlers}
|
||||||
queryEventHandlers={queryEventHandlers}
|
|
||||||
zrEventHandlers={zrEventHandlers}
|
zrEventHandlers={zrEventHandlers}
|
||||||
selectedValues={selectedValues}
|
selectedValues={selectedValues}
|
||||||
vizType={formData.vizType}
|
vizType={formData.vizType}
|
||||||
|
|||||||
@@ -889,10 +889,6 @@ export default function transformProps(
|
|||||||
name: xAxisTitle,
|
name: xAxisTitle,
|
||||||
nameGap: convertInteger(xAxisTitleMargin),
|
nameGap: convertInteger(xAxisTitleMargin),
|
||||||
nameLocation: 'middle',
|
nameLocation: 'middle',
|
||||||
...(xAxisType === AxisType.Category &&
|
|
||||||
groupBy.length === 0 && {
|
|
||||||
triggerEvent: true,
|
|
||||||
}),
|
|
||||||
axisLabel: {
|
axisLabel: {
|
||||||
// When rotation is applied on time axes, hideOverlap can
|
// When rotation is applied on time axes, hideOverlap can
|
||||||
// aggressively hide the last label. Rotated labels already
|
// aggressively hide the last label. Rotated labels already
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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,12 +64,7 @@ import {
|
|||||||
MarkLineComponent,
|
MarkLineComponent,
|
||||||
} from 'echarts/components';
|
} from 'echarts/components';
|
||||||
import { LabelLayout } from 'echarts/features';
|
import { LabelLayout } from 'echarts/features';
|
||||||
import {
|
import { EchartsHandler, EchartsProps, EchartsStylesProps } from '../types';
|
||||||
EchartsHandler,
|
|
||||||
EchartsProps,
|
|
||||||
EchartsStylesProps,
|
|
||||||
QueryEventHandlers,
|
|
||||||
} from '../types';
|
|
||||||
import { DEFAULT_LOCALE } from '../constants';
|
import { DEFAULT_LOCALE } from '../constants';
|
||||||
import { mergeEchartsThemeOverrides } from '../utils/themeOverrides';
|
import { mergeEchartsThemeOverrides } from '../utils/themeOverrides';
|
||||||
|
|
||||||
@@ -137,7 +132,6 @@ function Echart(
|
|||||||
height,
|
height,
|
||||||
echartOptions,
|
echartOptions,
|
||||||
eventHandlers,
|
eventHandlers,
|
||||||
queryEventHandlers,
|
|
||||||
zrEventHandlers,
|
zrEventHandlers,
|
||||||
selectedValues = {},
|
selectedValues = {},
|
||||||
refs,
|
refs,
|
||||||
@@ -153,7 +147,6 @@ function Echart(
|
|||||||
}
|
}
|
||||||
const [didMount, setDidMount] = useState(false);
|
const [didMount, setDidMount] = useState(false);
|
||||||
const chartRef = useRef<EChartsType>();
|
const chartRef = useRef<EChartsType>();
|
||||||
const previousQueryEventHandlers = useRef<QueryEventHandlers>([]);
|
|
||||||
const currentSelection = useMemo(
|
const currentSelection = useMemo(
|
||||||
() => Object.keys(selectedValues) || [],
|
() => Object.keys(selectedValues) || [],
|
||||||
[selectedValues],
|
[selectedValues],
|
||||||
@@ -203,19 +196,11 @@ function Echart(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (didMount) {
|
if (didMount) {
|
||||||
previousQueryEventHandlers.current.forEach(({ name, handler }) => {
|
|
||||||
chartRef.current?.off(name, handler);
|
|
||||||
});
|
|
||||||
Object.entries(eventHandlers || {}).forEach(([name, handler]) => {
|
Object.entries(eventHandlers || {}).forEach(([name, handler]) => {
|
||||||
chartRef.current?.off(name);
|
chartRef.current?.off(name);
|
||||||
chartRef.current?.on(name, handler);
|
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]) => {
|
Object.entries(zrEventHandlers || {}).forEach(([name, handler]) => {
|
||||||
chartRef.current?.getZr().off(name);
|
chartRef.current?.getZr().off(name);
|
||||||
chartRef.current?.getZr().on(name, handler);
|
chartRef.current?.getZr().on(name, handler);
|
||||||
@@ -351,15 +336,7 @@ function Echart(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- isDashboardRefreshing intentionally excluded to prevent extra setOption calls
|
// 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)
|
// Clear tooltip on refresh start to avoid stale content (#39247)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import {
|
|||||||
} from '@superset-ui/core';
|
} from '@superset-ui/core';
|
||||||
import type { EChartsCoreOption, EChartsType } from 'echarts/core';
|
import type { EChartsCoreOption, EChartsType } from 'echarts/core';
|
||||||
import type { TooltipMarker } from 'echarts/types/src/util/format';
|
import type { TooltipMarker } from 'echarts/types/src/util/format';
|
||||||
import type { ECElementEvent } from 'echarts/types/src/util/types';
|
|
||||||
import { StackControlsValue } from './constants';
|
import { StackControlsValue } from './constants';
|
||||||
|
|
||||||
export type EchartsStylesProps = {
|
export type EchartsStylesProps = {
|
||||||
@@ -52,7 +51,6 @@ export interface EchartsProps {
|
|||||||
width: number;
|
width: number;
|
||||||
echartOptions: EChartsCoreOption;
|
echartOptions: EChartsCoreOption;
|
||||||
eventHandlers?: EventHandlers;
|
eventHandlers?: EventHandlers;
|
||||||
queryEventHandlers?: QueryEventHandlers;
|
|
||||||
zrEventHandlers?: EventHandlers;
|
zrEventHandlers?: EventHandlers;
|
||||||
selectedValues?: Record<number, string>;
|
selectedValues?: Record<number, string>;
|
||||||
forceClear?: boolean;
|
forceClear?: boolean;
|
||||||
@@ -107,12 +105,6 @@ export type LegendFormData = {
|
|||||||
|
|
||||||
export type EventHandlers = Record<string, { (props: any): void }>;
|
export type EventHandlers = Record<string, { (props: any): void }>;
|
||||||
|
|
||||||
export type QueryEventHandlers = {
|
|
||||||
name: string;
|
|
||||||
query: string;
|
|
||||||
handler: (props: ECElementEvent) => void;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
export enum LabelPositionEnum {
|
export enum LabelPositionEnum {
|
||||||
Top = 'top',
|
Top = 'top',
|
||||||
Left = 'left',
|
Left = 'left',
|
||||||
|
|||||||
@@ -1564,13 +1564,9 @@ test('xAxisForceCategorical forces Category axis regardless of Numeric coltype',
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { echartOptions } = transformProps(chartProps);
|
const { echartOptions } = transformProps(chartProps);
|
||||||
const xAxis = echartOptions.xAxis as {
|
const xAxis = echartOptions.xAxis as { type: string };
|
||||||
triggerEvent?: boolean;
|
|
||||||
type: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
expect(xAxis.type).toBe(AxisType.Category);
|
expect(xAxis.type).toBe(AxisType.Category);
|
||||||
expect(xAxis.triggerEvent).toBe(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('temporal x coltype wires the time formatter and Time axis', () => {
|
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
|
* you may not use this file except in compliance with the License. You may obtain
|
||||||
* a copy of the License at
|
* 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
|
* 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
|
* under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
|
||||||
@@ -36,77 +36,80 @@ const mockedProps = {
|
|||||||
resourceName: 'dashboard',
|
resourceName: 'dashboard',
|
||||||
};
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
fetchMock.clearHistory().removeRoutes();
|
describe('BulkTagModal', () => {
|
||||||
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: [],
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<BulkTagModal {...mockedProps} />);
|
test('should render', () => {
|
||||||
|
const { container } = render(<BulkTagModal {...mockedProps} />);
|
||||||
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
expect(container).toBeInTheDocument();
|
||||||
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();
|
test('renders the correct title and message', () => {
|
||||||
expect(mockedProps.onHide).toHaveBeenCalled();
|
render(<BulkTagModal {...mockedProps} />);
|
||||||
});
|
expect(
|
||||||
|
screen.getByText(/you are adding tags to 2 dashboards/i),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('Bulk tag')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test('handles API errors gracefully', async () => {
|
test('renders tags input field', async () => {
|
||||||
fetchMock.post('glob:*/api/v1/tag/bulk_create', 500);
|
render(<BulkTagModal {...mockedProps} />);
|
||||||
|
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
||||||
|
expect(tagsInput).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
render(<BulkTagModal {...mockedProps} />);
|
test('calls onHide when the Cancel button is clicked', () => {
|
||||||
|
render(<BulkTagModal {...mockedProps} />);
|
||||||
|
const cancelButton = screen.getByText('Cancel');
|
||||||
|
fireEvent.click(cancelButton);
|
||||||
|
expect(mockedProps.onHide).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
test('submits the selected tags and shows success toast', async () => {
|
||||||
fireEvent.change(tagsInput, { target: { value: 'Test Tag' } });
|
fetchMock.post('glob:*/api/v1/tag/bulk_create', {
|
||||||
fireEvent.keyDown(tagsInput, { key: 'Enter', code: 'Enter' });
|
result: {
|
||||||
|
objects_tagged: [1, 2],
|
||||||
|
objects_skipped: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
fireEvent.click(screen.getByText('Save'));
|
render(<BulkTagModal {...mockedProps} />);
|
||||||
|
|
||||||
await waitFor(() => {
|
const tagsInput = await screen.findByRole('combobox', { name: /tags/i });
|
||||||
expect(mockedProps.addDangerToast).toHaveBeenCalledWith(
|
fireEvent.change(tagsInput, { target: { value: 'Test Tag' } });
|
||||||
'Failed to tag items',
|
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',
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const TestComponent = (props: ThemeSubMenuProps) => {
|
|||||||
return <Menu items={[menuItem]} />;
|
return <Menu items={[menuItem]} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
describe('useThemeMenuItems', () => {
|
describe('useThemeMenuItems', () => {
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
allowOSPreference: true,
|
allowOSPreference: true,
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
* "License"); you may not use this file except in compliance
|
* "License"); you may not use this file except in compliance
|
||||||
* with the License. You may obtain a copy of the License at
|
* 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,
|
* Unless required by applicable law or agreed to in writing,
|
||||||
* software distributed under the License is distributed on an
|
* software distributed under the License is distributed on an
|
||||||
@@ -27,127 +27,133 @@ import {
|
|||||||
ColumnDefinition,
|
ColumnDefinition,
|
||||||
} from 'src/utils/common';
|
} from 'src/utils/common';
|
||||||
|
|
||||||
test('converts values as expected', () => {
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
expect(optionFromValue(false)).toEqual({
|
describe('utils/common', () => {
|
||||||
value: false,
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
label: FALSE_STRING,
|
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' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
expect(optionFromValue(true)).toEqual({
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
value: true,
|
describe('prepareCopyToClipboardTabularData', () => {
|
||||||
label: TRUE_STRING,
|
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(null)).toEqual({
|
// eslint-disable-next-line no-restricted-globals -- TODO: Migrate from describe blocks
|
||||||
value: NULL_STRING,
|
describe('applyFormattingToTabularData', () => {
|
||||||
label: NULL_STRING,
|
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('')).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