Compare commits

...

12 Commits

Author SHA1 Message Date
Claude Code
ead906f93a test(preset-chart-deckgl): prove legend clicks no longer scroll to top (#33576)
Adds an RTL regression test for the deck.gl Legend component asserting
that a plain click on a legend item both calls preventDefault() on the
href="#" anchor's click event (so the browser never navigates to "#"
and scrolls the window to the top) and fires the toggleCategory
callback for the clicked category.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 16:40:49 -07:00
luca-vercelli
9e50d9eca6 fix: Italian translation (#36114)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 14:21:20 -07:00
yousoph
033b7bc385 fix(databases): update broken database documentation links to /user-docs namespace (#41557)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Đỗ Trọng Hải <41283691+hainenber@users.noreply.github.com>
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-06 13:02:02 -07:00
yousoph
c0b0a2fdaf feat(tags): add favorites filter to Tags list view (#41461)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 12:55:01 -07:00
Ville Brofeldt
44c1ad5c71 fix: delint frontend files (#41802) 2026-07-06 21:54:33 +02:00
amaannawab923
fb496e158a fix(ag-grid): persist "None" value aggregation selection (#41386)
Co-authored-by: Enzo Martellucci <52219496+EnxDev@users.noreply.github.com>
Co-authored-by: Enzo Martellucci <enzomartellucci@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-06 21:51:26 +02:00
Đỗ Trọng Hải
69ce27f173 feat(sec): resolve all vuln alerts in superset-embedded-sdk, superset-frontend and docs (#41776)
Signed-off-by: hainenber <dotronghai96@gmail.com>
Co-authored-by: Claude Code <noreply@anthropic.com>
2026-07-06 12:14:34 -07:00
Richard Fogaca Nienkotter
51e708da6b fix(mcp): accept Superset vocabulary in chart configs and clarify query_dataset metric errors (#40972) 2026-07-06 16:01:14 -03:00
Evan Rusackas
21b4185819 fix(ace-editor): caret drift from late font loads and emoji widths (#41697)
Co-authored-by: Claude Code <noreply@anthropic.com>
Co-authored-by: Amin Ghadersohi <amin.ghadersohi@gmail.com>
2026-07-06 11:54:11 -07:00
Đỗ Trọng Hải
689b4f623e docs(admin/config): resolve misconfigured aws-iam Markdown file (#41748)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-06 11:51:15 -07:00
Đỗ Trọng Hải
ad8e6d4140 build(deps): upgrade react-markdown to v9 and remark-gfm to v4 (#41778)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2026-07-06 11:50:09 -07:00
marun
83d93b8b42 fix(view-query): Add ownState parameter to ViewQueryModal for query consistency (#35208)
Co-authored-by: Evan Rusackas <evan@preset.io>
2026-07-06 11:16:55 -07:00
151 changed files with 9698 additions and 14323 deletions

View File

@@ -10,11 +10,7 @@ updates:
- package-ecosystem: "npm"
ignore:
- dependency-name: "react-error-boundary"
- dependency-name: "@rjsf/*"
# remark-gfm v4+ requires react-markdown v9+, which needs React 18
- dependency-name: "remark-gfm"
- dependency-name: "react-markdown"
# TODO: remove below entries until React >= 19.0.0
- dependency-name: "react-icons"
# JSDOM v30 doesn't play well with Jest v30
@@ -78,8 +74,6 @@ updates:
- package-ecosystem: "npm"
directory: "/docs/"
ignore:
- dependency-name: "react-error-boundary"
schedule:
interval: "daily"
open-pull-requests-limit: 10
@@ -376,11 +370,6 @@ updates:
- package-ecosystem: "npm"
directory: "/superset-frontend/packages/superset-ui-core/"
ignore:
# not until React >= 18.0.0
- dependency-name: "react-markdown"
- dependency-name: "remark-gfm"
- dependency-name: "react-error-boundary"
schedule:
interval: "daily"
labels:

View File

@@ -1,25 +1,7 @@
{/*
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.
*/}
---
title: AWS IAM Authentication
sidebar_label: AWS IAM Authentication
version: 1
sidebar_position: 15
---

View File

@@ -67,7 +67,6 @@
"docusaurus-plugin-openapi-docs": "^5.1.0",
"docusaurus-theme-openapi-docs": "^5.1.0",
"js-yaml": "^5.2.0",
"js-yaml-loader": "^1.2.2",
"json-bigint": "^1.0.0",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
@@ -118,14 +117,16 @@
"react-redux": "^9.2.0",
"@reduxjs/toolkit": "^2.5.0",
"baseline-browser-mapping": "^2.9.19",
"swagger-client": "3.37.3",
"lodash": "4.18.1",
"lodash-es": "4.18.1",
"yaml": "1.10.3",
"uuid": "11.1.1",
"serialize-javascript": "7.0.5",
"d3-color": "3.1.0",
"ws": "^8.21.0"
"ws": "^8.21.0",
"@docusaurus/core/@docusaurus/utils/gray-matter/js-yaml": "^3.15.0",
"@docusaurus/core/**/js-yaml": "^4.3.0",
"docusaurus-plugin-openapi-docs/**/js-yaml": "^4.3.0"
},
"packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610"
}

View File

@@ -176,7 +176,7 @@ export interface DatabaseInfo {
expressions_in_orderby?: boolean;
// Platform features
limit_method?: string;
limit_method?: number;
limit_clause?: boolean;
max_column_name?: number;
supports_file_upload?: boolean;

View File

@@ -19,6 +19,7 @@
import Layout from '@theme/Layout';
import { Avatar, Card, Col, Collapse, Row, Typography } from 'antd';
import { load } from 'js-yaml';
import BlurredSection from '../components/BlurredSection';
import SectionHeader from '../components/SectionHeader';
import DataSet from '../../../RESOURCES/INTHEWILD.yaml';
@@ -36,7 +37,7 @@ interface DataSetType {
categories: Record<string, Organization[]>;
}
const typedDataSet = DataSet as DataSetType;
const typedDataSet = load(DataSet) as DataSetType;
const ContributorAvatars = ({ contributors }: { contributors?: string[] }) => {
if (!contributors?.length) return null;

View File

@@ -22,6 +22,7 @@ import Link from '@docusaurus/Link';
import { Card, Carousel, Flex } from 'antd';
import styled from '@emotion/styled';
import GitHubButton from 'react-github-btn';
import { load } from 'js-yaml';
import { mq } from '../utils';
import SectionHeader from '../components/SectionHeader';
import databaseData from '../data/databases.json';
@@ -60,7 +61,7 @@ interface DataSetType {
categories: Record<string, Organization[]>;
}
const typedDataSet = DataSet as DataSetType;
const typedDataSet = load(DataSet) as DataSetType;
// Extract all organizations with logos for the carousel
const companiesWithLogos = Object.values(typedDataSet.categories)

View File

@@ -18,11 +18,11 @@
*/
declare module '*.yaml' {
const content: unknown;
const content: string;
export default content;
}
declare module '*.yml' {
const content: unknown;
const content: string;
export default content;
}

View File

@@ -61,10 +61,10 @@ export default function webpackExtendPlugin(): Plugin<void> {
);
});
// Add YAML loader rule directly to existing rules
// Load YAML file as raw text string
config.module?.rules?.push({
test: /\.ya?ml$/,
use: 'js-yaml-loader',
type: 'asset/source',
});
// Add swc-loader rule for superset-frontend files

View File

@@ -30,7 +30,6 @@
"exclude": [
"node_modules",
"../superset-frontend/**/*",
"src/webpack.extend.ts",
"src/shims/**"
]
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -37,10 +37,10 @@
"jwt-decode": "^4.0.0"
},
"devDependencies": {
"@babel/cli": "^7.25.6",
"@babel/core": "^7.25.2",
"@babel/preset-env": "^7.25.4",
"@babel/preset-typescript": "^7.24.7",
"@babel/cli": "^7.29.7",
"@babel/core": "^7.29.7",
"@babel/preset-env": "^7.29.7",
"@babel/preset-typescript": "7.29.7",
"@types/node": "^25.4.0",
"babel-loader": "^9.1.3",
"tscw-config": "^1.1.2",

File diff suppressed because it is too large Load Diff

View File

@@ -393,7 +393,6 @@
"http-proxy-middleware": "^2.0.10",
"tar": "^7.5.16",
"puppeteer": "^22.4.1",
"remark-gfm": "^3.0.1",
"underscore": "^1.13.7",
"jspdf": "^4.2.0",
"nwsapi": "^2.2.13",
@@ -428,6 +427,9 @@
},
"eslint-plugin-jest-dom": {
"eslint": "$eslint"
},
"lerna": {
"js-yaml": "^4.3.0"
}
},
"readme": "ERROR: No README data found!",

View File

@@ -330,8 +330,7 @@ export interface SelectControlConfig<
optionRenderer?: (option: O) => ReactNode;
valueRenderer?: (option: O) => ReactNode;
filterOption?:
| ((option: FilterOption<O>, rawInput: string) => boolean)
| null;
((option: FilterOption<O>, rawInput: string) => boolean) | null;
}
export type SharedControlConfig<
@@ -396,9 +395,7 @@ export const isCustomControlItem = (obj: unknown): obj is CustomControlItem =>
export type ExpandedControlItem = CustomControlItem | ReactElement | null;
export type ControlSetItem =
| SharedControlAlias
| OverrideSharedControlItem
| ExpandedControlItem;
SharedControlAlias | OverrideSharedControlItem | ExpandedControlItem;
export type ControlSetRow = ControlSetItem[];
@@ -702,10 +699,7 @@ export interface DataColumnMeta {
originalLabel?: string;
dataType: GenericDataType;
formatter?:
| TimeFormatter
| NumberFormatter
| CustomFormatter
| CurrencyFormatter;
TimeFormatter | NumberFormatter | CustomFormatter | CurrencyFormatter;
isMetric?: boolean;
isPercentMetric?: boolean;
isNumeric?: boolean;

View File

@@ -53,16 +53,16 @@
"re-resizable": "^6.11.2",
"react-ace": "^14.0.1",
"react-draggable": "^4.7.0",
"react-error-boundary": "6.0.0",
"react-error-boundary": "^6.1.2",
"react-js-cron": "^5.2.0",
"react-markdown": "^8.0.7",
"react-markdown": "^9.0.7",
"react-resize-detector": "^7.1.2",
"react-syntax-highlighter": "^16.1.1",
"react-ultimate-pagination": "^1.3.2",
"regenerator-runtime": "^0.14.1",
"rehype-raw": "^7.0.0",
"rehype-sanitize": "^6.0.0",
"remark-gfm": "^3.0.1",
"remark-gfm": "^4.0.1",
"reselect": "^5.2.0",
"rison": "^0.1.1",
"seedrandom": "^3.0.5",

View File

@@ -29,13 +29,7 @@ import {
import type { AsyncAceEditorOptions } from './types';
type EditorType =
| 'sql'
| 'full-sql'
| 'markdown'
| 'text-area'
| 'css'
| 'json'
| 'config';
'sql' | 'full-sql' | 'markdown' | 'text-area' | 'css' | 'json' | 'config';
const editorTypes: EditorType[] = [
'sql',

View File

@@ -70,6 +70,199 @@ test('SQLEditor uses fontFamilyCode from theme', async () => {
expect(fontFamily).toMatch(/mono|courier|consolas/i);
});
test('re-measures Ace font metrics on load and preserves a consumer onLoad (#41664)', async () => {
// Ace caches glyph width at construction; if the editor font settles later,
// the caret drifts. The editor forces a re-measure on load and again once
// `document.fonts.ready` resolves. Mock `document.fonts` so the re-measure
// path is deterministic regardless of the jsdom FontFaceSet implementation.
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
const fontsReady = Promise.resolve();
Object.defineProperty(document, 'fonts', {
configurable: true,
value: { ready: fontsReady },
});
const ref = createRef<AceEditor>();
let updateFontSizeSpy: jest.SpyInstance | undefined;
// The spy is installed from inside the consumer onLoad, so it captures the
// asynchronous (post-fonts-ready) re-measure that runs after this callback.
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
// Cast to a minimal shape so `jest.spyOn` resolves cleanly; it is the
// same renderer instance the component re-measures, so the spy still
// observes the production calls.
const renderer = editor.renderer as unknown as {
updateFontSize: () => void;
};
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
});
try {
const { container } = render(
<SQLEditor
ref={ref as React.Ref<never>}
onLoad={consumerOnLoad as never}
/>,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
// The wrapper must call through to the consumer's onLoad with the editor.
expect(consumerOnLoad).toHaveBeenCalledTimes(1);
expect(consumerOnLoad).toHaveBeenCalledWith(ref.current?.editor);
// Once fonts settle, the editor re-measures (Ace itself resizes and
// re-renders when the measured character size changed) so the caret
// realigns.
await fontsReady;
await waitFor(() => {
expect(updateFontSizeSpy).toHaveBeenCalled();
});
} finally {
updateFontSizeSpy?.mockRestore();
if (originalFonts) {
Object.defineProperty(document, 'fonts', originalFonts);
} else {
delete (document as { fonts?: unknown }).fonts;
}
}
});
test('does not crash when the Font Loading API is unavailable (#41697)', async () => {
// jsdom has no `document.fonts` by default; some embedded webviews don't
// either. The load-time re-measure must still run and the editor must mount.
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
delete (document as { fonts?: unknown }).fonts;
const consumerOnLoad = jest.fn();
try {
const { container } = render(
<SQLEditor onLoad={consumerOnLoad as never} />,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
expect(consumerOnLoad).toHaveBeenCalledTimes(1);
} finally {
if (originalFonts) {
Object.defineProperty(document, 'fonts', originalFonts);
}
}
});
test('explicitly loads the editor font and re-measures when it resolves (#41664)', async () => {
// `fonts.ready` can settle BEFORE a lazily-referenced editor font even
// starts loading, so the fix must request the font itself via
// `fonts.load()` and re-measure when that specific load resolves.
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
let resolveFontLoad: () => void = () => {};
const fontLoad = new Promise<void>(resolve => {
resolveFontLoad = resolve;
});
const load = jest.fn((_font: string) => fontLoad);
Object.defineProperty(document, 'fonts', {
configurable: true,
// `ready` already settled: the pre-fix code would never re-measure.
value: {
ready: Promise.resolve(),
load,
addEventListener: jest.fn(),
removeEventListener: jest.fn(),
},
});
let updateFontSizeSpy: jest.SpyInstance | undefined;
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
const renderer = editor.renderer as unknown as {
updateFontSize: () => void;
};
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
});
try {
const { container } = render(
<SQLEditor onLoad={consumerOnLoad as never} />,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
// The editor font itself is requested (family from the theme's code font).
expect(load).toHaveBeenCalledTimes(1);
expect(load.mock.calls[0][0]).toMatch(/^12px /);
const callsBefore = updateFontSizeSpy?.mock.calls.length ?? 0;
resolveFontLoad();
await waitFor(() => {
expect(updateFontSizeSpy!.mock.calls.length).toBeGreaterThan(callsBefore);
});
} finally {
updateFontSizeSpy?.mockRestore();
if (originalFonts) {
Object.defineProperty(document, 'fonts', originalFonts);
} else {
delete (document as { fonts?: unknown }).fonts;
}
}
});
test('re-measures on later loadingdone events and unsubscribes on unmount (#41664)', async () => {
const originalFonts = Object.getOwnPropertyDescriptor(document, 'fonts');
const listeners: Record<string, (() => void)[]> = {};
const addEventListener = jest.fn((event: string, handler: () => void) => {
(listeners[event] ??= []).push(handler);
});
const removeEventListener = jest.fn((event: string, handler: () => void) => {
listeners[event] = (listeners[event] ?? []).filter(h => h !== handler);
});
Object.defineProperty(document, 'fonts', {
configurable: true,
value: {
ready: Promise.resolve(),
load: jest.fn(() => Promise.resolve()),
addEventListener,
removeEventListener,
},
});
let updateFontSizeSpy: jest.SpyInstance | undefined;
const consumerOnLoad = jest.fn((editor: AceEditor['editor']) => {
const renderer = editor.renderer as unknown as {
updateFontSize: () => void;
};
updateFontSizeSpy = jest.spyOn(renderer, 'updateFontSize');
});
try {
const { container, unmount } = render(
<SQLEditor onLoad={consumerOnLoad as never} />,
);
await waitFor(() => {
expect(container.querySelector(selector)).toBeInTheDocument();
});
await waitFor(() => {
expect(listeners.loadingdone?.length ?? 0).toBeGreaterThan(0);
});
// A font finishing later (e.g. a user-config font) re-measures again.
const callsBefore = updateFontSizeSpy?.mock.calls.length ?? 0;
listeners.loadingdone.forEach(handler => handler());
expect(updateFontSizeSpy!.mock.calls.length).toBeGreaterThan(callsBefore);
// Unmount removes the listener so destroyed editors are not re-measured.
unmount();
expect(listeners.loadingdone?.length ?? 0).toBe(0);
} finally {
updateFontSizeSpy?.mockRestore();
if (originalFonts) {
Object.defineProperty(document, 'fonts', originalFonts);
} else {
delete (document as { fonts?: unknown }).fonts;
}
}
});
test('renders FullSQLEditor', async () => {
const { container } = render(<FullSQLEditor />);

View File

@@ -0,0 +1,190 @@
/**
* 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 ace from 'ace-builds/src-noconflict/ace';
import {
emojiWidth,
isBmpEmojiPresentation,
patchAceEmojiWidths,
} from './emojiWidthPatch';
const { EditSession } = ace.require('ace/edit_session');
const { Text: TextLayer } = ace.require('ace/layer/text');
beforeAll(() => {
patchAceEmojiWidths(EditSession, TextLayer);
});
test('classifies BMP emoji-presentation codepoints and nothing else', () => {
expect(isBmpEmojiPresentation('✨'.charCodeAt(0))).toBe(true); // U+2728
expect(isBmpEmojiPresentation('⭐'.charCodeAt(0))).toBe(true); // U+2B50
expect(isBmpEmojiPresentation('❤'.charCodeAt(0))).toBe(false); // text-default
expect(isBmpEmojiPresentation('A'.charCodeAt(0))).toBe(false);
expect(isBmpEmojiPresentation('中'.charCodeAt(0))).toBe(false); // CJK, ace's own tables
expect(isBmpEmojiPresentation(0xd83d)).toBe(false); // lone surrogate
});
test('emojiWidth: VS16 tops its base up to 2 columns total', () => {
const VS16 = 0xfe0f;
expect(emojiWidth('✨'.charCodeAt(0), 0)).toBe(2);
// ❤️ = U+2764 (1 by default rules) + VS16 (1) = 2 total
expect(emojiWidth('❤'.charCodeAt(0), 0)).toBe(null);
expect(emojiWidth(VS16, '❤'.charCodeAt(0))).toBe(1);
// Redundant ✨️ = ✨ (2) + VS16 (0) = 2 total
expect(emojiWidth(VS16, '✨'.charCodeAt(0))).toBe(0);
expect(emojiWidth('A'.charCodeAt(0), 0)).toBe(null);
});
test('caret math counts emoji as two columns (issue #41664 repro line)', () => {
const session = new EditSession('### ✨Header 3');
// Before "H" (doc column 5): "### " = 4 + ✨ = 2 → screen column 6.
expect(session.documentToScreenColumn(0, 5)).toBe(6);
// End of line: 4 + 2 + "Header 3".length (8) = 14.
expect(session.documentToScreenColumn(0, 13)).toBe(14);
});
test('astral emoji keep their pre-existing two-column accounting', () => {
const session = new EditSession('💡X');
// 💡 is two code units (doc columns 0-1) and two screen columns.
expect(session.documentToScreenColumn(0, 2)).toBe(2);
});
test('$getStringScreenWidth counts emoji, VS16 pairs, and plain text', () => {
const session = new EditSession('');
expect(session.$getStringScreenWidth('✨')[0]).toBe(2);
expect(session.$getStringScreenWidth('❤️')[0]).toBe(2);
expect(session.$getStringScreenWidth('✨️')[0]).toBe(2);
expect(session.$getStringScreenWidth('abc')[0]).toBe(3);
expect(session.$getStringScreenWidth('中')[0]).toBe(2); // ace's own CJK path
});
test('renderer draws emoji in a forced 2×charWidth box like CJK', () => {
const container = document.createElement('div');
const layer = new TextLayer(container);
layer.config = { characterWidth: 10 };
const parent = document.createElement('div');
const screenColumn = layer.$renderToken(
parent,
0,
{ type: 'text', value: '# ✨Header' },
'# ✨Header',
);
const box = parent.querySelector<HTMLElement>('.ace_cjk');
expect(box).not.toBeNull();
expect(box?.textContent).toBe('✨');
expect(box?.style.width).toBe('20px');
// "# " (2) + ✨ (2) + "Header" (6) = 10 columns.
expect(screenColumn).toBe(10);
// Text around the emoji is preserved in order.
expect(parent.textContent).toBe('# ✨Header');
});
test('renderer handles a leading emoji (empty split part) cleanly', () => {
const container = document.createElement('div');
const layer = new TextLayer(container);
layer.config = { characterWidth: 10 };
const parent = document.createElement('div');
const screenColumn = layer.$renderToken(
parent,
0,
{ type: 'text', value: '✨x' },
'✨x',
);
expect(parent.querySelector('.ace_cjk')?.textContent).toBe('✨');
expect(parent.textContent).toBe('✨x');
expect(screenColumn).toBe(3);
});
test('renderer leaves emoji-free tokens entirely to the original path', () => {
const container = document.createElement('div');
const layer = new TextLayer(container);
layer.config = { characterWidth: 10 };
const parent = document.createElement('div');
const screenColumn = layer.$renderToken(
parent,
0,
{ type: 'text', value: 'SELECT 1' },
'SELECT 1',
);
expect(parent.querySelector('.ace_cjk')).toBeNull();
expect(parent.textContent).toBe('SELECT 1');
expect(screenColumn).toBe(8);
});
test('patch is idempotent', () => {
const before = EditSession.prototype.$getStringScreenWidth;
patchAceEmojiWidths(EditSession, TextLayer);
expect(EditSession.prototype.$getStringScreenWidth).toBe(before);
});
// --- copied-internals parity ------------------------------------------------
// The two overridden EditSession methods are verbatim copies of the
// ace-builds internals plus the emoji branch; these pin the non-emoji
// behavior so a future ace upgrade that drifts is caught here.
test('$getStringScreenWidth keeps tab, early-return, and break semantics', () => {
const session = new EditSession('');
// Tab expands to the next tab stop (default tab size 4).
expect(session.$getStringScreenWidth('\ta')[0]).toBe(5);
// maxScreenColumn === 0 short-circuits.
expect(session.$getStringScreenWidth('abc', 0)).toEqual([0, 0]);
// Stops consuming once past maxScreenColumn.
const [screen, column] = session.$getStringScreenWidth('abcdef', 3);
expect(screen).toBe(4);
expect(column).toBe(3);
});
test('$getDisplayTokens keeps tab, space, punctuation, and CJK tokens', () => {
const session = new EditSession('');
const CHAR = 1;
const CHAR_EXT = 2;
const PUNCTUATION = 9;
const SPACE = 10;
const TAB = 11;
const TAB_SPACE = 12;
// Tab at offset 0 with default tab size 4: TAB + 3 TAB_SPACEs.
expect(session.$getDisplayTokens('\t', 0)).toEqual([
TAB,
TAB_SPACE,
TAB_SPACE,
TAB_SPACE,
]);
expect(session.$getDisplayTokens(' a', 0)).toEqual([SPACE, CHAR]);
expect(session.$getDisplayTokens('*:', 0)).toEqual([
PUNCTUATION,
PUNCTUATION,
]);
expect(session.$getDisplayTokens('中', 0)).toEqual([CHAR, CHAR_EXT]);
expect(session.$getDisplayTokens('✨', 0)).toEqual([CHAR, CHAR_EXT]);
// VS16 keeps one entry per code unit so wrap offsets stay aligned.
expect(session.$getDisplayTokens('✨️', 0)).toEqual([
CHAR,
CHAR_EXT,
CHAR_EXT,
]);
expect(session.$getDisplayTokens('❤️', 0)).toEqual([CHAR, CHAR_EXT]);
// Second pass hits the memoized emoji-base classification.
expect(session.$getDisplayTokens('❤️', 0)).toEqual([CHAR, CHAR_EXT]);
});

View File

@@ -0,0 +1,271 @@
/**
* 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.
*/
/**
* Ace positions the caret on a monospace grid (`column × charWidth`) and
* renders anything it classifies as "full width" inside a forced
* `2 × charWidth` inline-block (`.ace_cjk`), so the grid model and the pixels
* agree. Its width tables only cover East-Asian ranges, though:
*
* - Astral emoji (💡, surrogate pairs) happen to work: the renderer's
* surrogate-pair branch forces the 2-cell box and the column counter
* counts the two code units.
* - BMP emoji with default emoji presentation (✨ U+2728, ⭐ U+2B50, …) fall
* through BOTH nets: they count as 1 column but render ~1.6 cells wide, so
* the caret drifts on any line containing one (issue #41664, upstream
* ajaxorg/ace#3404).
* - VS16 sequences (❤️ = U+2764 U+FE0F) render as one color glyph while the
* two code units count as two ordinary columns, without the forced box.
*
* The patch extends the exact mechanism Ace already uses — count 2 columns
* and force the 2-cell box — to emoji-presentation codepoints and VS16
* sequences, by overriding the two `EditSession` width methods (verbatim
* copies of the ace-builds internals plus one branch; the originals call a
* module-private `isFullWidth`, so they cannot be extended in place) and
* wrapping the text layer's `$renderToken` to emit the forced-width span for
* emoji runs before delegating everything else to the original.
*
* Deliberately out of scope: ZWJ sequences (family emoji) and flag pairs
* render as one glyph but count per component; handling them needs grapheme
* segmentation across every Ace layer, and they were equally misaligned
* before this patch.
*/
const VS16 = 0xfe0f;
// Emoji_Presentation covers exactly the codepoints browsers render as color
// emoji without a variation selector. Lone surrogates never match it (the
// astral path is already consistent), so only BMP emoji change behavior.
const EMOJI_PRESENTATION_RE = /\p{Emoji_Presentation}/u;
// One renderable emoji cluster: an emoji-presentation codepoint with an
// optional (redundant) VS16, or a text-presentation emoji forced to color
// presentation by VS16.
const EMOJI_CLUSTER_SOURCE =
'(?:\\p{Emoji_Presentation}\\uFE0F?|\\p{Emoji}\\uFE0F)';
const EMOJI_RUN_TEST_RE = new RegExp(`${EMOJI_CLUSTER_SOURCE}`, 'u');
const EMOJI_RUN_SPLIT_RE = new RegExp(`(${EMOJI_CLUSTER_SOURCE}+)`, 'gu');
const EMOJI_CLUSTER_RE = new RegExp(EMOJI_CLUSTER_SOURCE, 'gu');
// Memoized per-code-unit classification; $getStringScreenWidth runs per
// character per rendered line, so the regexes must not run every time.
const EMOJI_BASE_RE = /\p{Emoji}/u;
const emojiBaseCache = new Map<number, boolean>();
function isEmojiBase(code: number): boolean {
let result = emojiBaseCache.get(code);
if (result === undefined) {
result = EMOJI_BASE_RE.test(String.fromCharCode(code));
emojiBaseCache.set(code, result);
}
return result;
}
const bmpEmojiCache = new Map<number, boolean>();
export function isBmpEmojiPresentation(code: number): boolean {
if (code < 0x2000 || code > 0xffff || (code >= 0xd800 && code <= 0xdfff)) {
return false;
}
let result = bmpEmojiCache.get(code);
if (result === undefined) {
result = EMOJI_PRESENTATION_RE.test(String.fromCharCode(code));
bmpEmojiCache.set(code, result);
}
return result;
}
/**
* Screen-column contribution of one code unit under the emoji rules, or null
* when Ace's default handling should apply. Emoji-presentation chars are 2
* columns; VS16 tops its base up to 2 in total (so ❤️ = 1 + 1 and a
* redundant ✨️ = 2 + 0).
*/
export function emojiWidth(code: number, prevCode: number): number | null {
if (isBmpEmojiPresentation(code)) {
return 2;
}
if (code === VS16) {
return isBmpEmojiPresentation(prevCode) ? 0 : 1;
}
return null;
}
interface PatchableEditSessionClass {
prototype: {
isFullWidth: (c: number) => boolean;
getScreenTabSize: (screenColumn: number) => number;
$getStringScreenWidth: (
str: string,
maxScreenColumn?: number,
screenColumn?: number,
) => [number, number];
$getDisplayTokens: (str: string, offset: number) => number[];
};
}
interface TextLayerInstance {
config: { characterWidth: number };
dom: { createElement: (tag: string) => HTMLElement };
}
type RenderToken = (
this: TextLayerInstance,
parent: HTMLElement,
screenColumn: number,
token: { type: string; value: string },
value: string,
) => number;
interface PatchableTextLayerClass {
prototype: { $renderToken: RenderToken };
}
let patched = false;
export function patchAceEmojiWidths(
EditSession: PatchableEditSessionClass,
TextLayer: PatchableTextLayerClass,
): void {
if (patched) {
return;
}
patched = true;
const sessionProto = EditSession.prototype;
const { isFullWidth } = sessionProto;
// Mirrors EditSession.prototype.$getStringScreenWidth from ace-builds
// (src-noconflict/ace.js), adding the emojiWidth branch.
sessionProto.$getStringScreenWidth = function $getStringScreenWidth(
this: PatchableEditSessionClass['prototype'],
str: string,
maxScreenColumn?: number,
screenColumn?: number,
): [number, number] {
if (maxScreenColumn === 0) {
return [0, 0];
}
const max = maxScreenColumn ?? Infinity;
let screen = screenColumn || 0;
let column;
for (column = 0; column < str.length; column += 1) {
const c = str.charCodeAt(column);
const emoji = emojiWidth(c, column > 0 ? str.charCodeAt(column - 1) : 0);
if (c === 9) {
screen += this.getScreenTabSize(screen);
} else if (emoji !== null) {
screen += emoji;
} else if (c >= 0x1100 && isFullWidth(c)) {
screen += 2;
} else {
screen += 1;
}
if (screen > max) {
break;
}
}
return [screen, column];
};
// Display-token codes from ace's edit_session module.
const CHAR = 1;
const CHAR_EXT = 2;
const PUNCTUATION = 9;
const SPACE = 10;
const TAB = 11;
const TAB_SPACE = 12;
// Mirrors EditSession.prototype.$getDisplayTokens, adding the emoji branch.
// One entry per code unit is preserved (VS16 pushes a lone CHAR_EXT) so the
// wrap-split offset math stays aligned with document columns.
sessionProto.$getDisplayTokens = function $getDisplayTokens(
this: PatchableEditSessionClass['prototype'],
str: string,
offset: number,
): number[] {
const arr: number[] = [];
let tabSize: number;
for (let i = 0; i < str.length; i += 1) {
const c = str.charCodeAt(i);
const emoji = emojiWidth(c, i > 0 ? str.charCodeAt(i - 1) : 0);
if (c === 9) {
tabSize = this.getScreenTabSize(arr.length + offset);
arr.push(TAB);
for (let n = 1; n < tabSize; n += 1) {
arr.push(TAB_SPACE);
}
} else if (emoji === 2) {
arr.push(CHAR, CHAR_EXT);
} else if (emoji === 0) {
arr.push(CHAR_EXT);
} else if (c === VS16 && i > 0 && isEmojiBase(str.charCodeAt(i - 1))) {
// VS16 after a text-presentation emoji base (❤️): keep the pair
// atomic for wrap splitting; its width contribution stays 1.
arr.push(CHAR_EXT);
} else if (c === 32) {
arr.push(SPACE);
} else if ((c > 39 && c < 48) || (c > 57 && c < 64)) {
arr.push(PUNCTUATION);
} else if (c >= 0x1100 && isFullWidth(c)) {
arr.push(CHAR, CHAR_EXT);
} else {
arr.push(CHAR);
}
}
return arr;
};
// Pre-split token text on emoji runs: emoji clusters get the same forced
// 2 × charWidth `.ace_cjk` box Ace gives CJK (so rendered geometry matches
// the 2-column model above); everything else delegates to the original.
const origRenderToken = TextLayer.prototype.$renderToken;
TextLayer.prototype.$renderToken = function $renderToken(
this: TextLayerInstance,
parent: HTMLElement,
screenColumn: number,
token: { type: string; value: string },
value: string,
): number {
if (!EMOJI_RUN_TEST_RE.test(value)) {
return origRenderToken.call(this, parent, screenColumn, token, value);
}
let column = screenColumn;
value.split(EMOJI_RUN_SPLIT_RE).forEach((part, index) => {
if (!part) {
return;
}
const isEmojiRun = index % 2 === 1;
if (isEmojiRun) {
// Parts at odd indices are the split's capture group, so every one
// is a non-empty sequence of clusters; matchAll never comes up empty.
Array.from(part.matchAll(EMOJI_CLUSTER_RE), m => m[0]).forEach(
cluster => {
const span = this.dom.createElement('span');
span.style.width = `${this.config.characterWidth * 2}px`;
span.className = 'ace_cjk';
span.textContent = cluster;
parent.appendChild(span);
column += 2;
},
);
} else {
column = origRenderToken.call(this, parent, column, token, part);
}
});
return column;
};
}

View File

@@ -16,7 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import { forwardRef, useEffect, useCallback, ComponentType } from 'react';
import {
forwardRef,
useEffect,
useCallback,
useRef,
ComponentType,
} from 'react';
import type {
Editor as OrigEditor,
@@ -35,6 +41,8 @@ import {
import { useTheme, css, type SupersetTheme } from '@apache-superset/core/theme';
import { Global } from '@emotion/react';
import { patchAceEmojiWidths } from './emojiWidthPatch';
export { getTooltipHTML } from './Tooltip';
export { useJsonValidation } from './useJsonValidation';
export type {
@@ -162,6 +170,14 @@ export function AsyncAceEditor(
config.setModuleUrl('ace/mode/javascript_worker', javascriptWorkerUrl);
config.setModuleUrl('ace/mode/html_worker', htmlWorkerUrl);
// Align caret math and rendered glyph geometry for emoji (issue #41664);
// see emojiWidthPatch for the full story. Applied once, globally, since
// the prototypes are shared by every Ace editor instance.
patchAceEmojiWidths(
acequire('ace/edit_session').EditSession,
acequire('ace/layer/text').Text,
);
await Promise.all(aceModules.map(x => aceModuleLoaders[x]()));
const inferredMode =
@@ -179,6 +195,7 @@ export function AsyncAceEditor(
theme = inferredTheme,
tabSize = defaultTabSize,
defaultValue = '',
onLoad,
...props
},
ref,
@@ -281,6 +298,62 @@ export function AsyncAceEditor(
};
}, [ref]);
// Ace caches the measured glyph width in its internal FontMetrics and
// only re-measures when its hidden measure node's own size changes. If
// the editor font finishes loading after construction, the cached width
// can stop matching the rendered glyphs and the caret drifts further
// from the text the longer the line, the residual misalignment in issue
// #41664 that the font-family CSS from #38928 does not address.
// `updateFontSize` runs Ace's `checkForSizeChanges`, which on a metrics
// change emits `changeCharacterSize`; Ace's renderer reacts with a
// forced resize and full re-render.
//
// Re-measure on every signal that glyph geometry may have changed:
// - immediately, for the already-loaded case;
// - after explicitly loading the editor font via `fonts.load()` —
// `fonts.ready` alone is NOT enough, because it can settle before a
// lazily-referenced editor font even starts loading, in which case
// waiting on it misses the swap entirely;
// - after `fonts.ready`, for everything in flight at mount;
// - on every subsequent `loadingdone` event while mounted, for fonts
// that load even later (removed on unmount via fontCleanupsRef).
const fontCleanupsRef = useRef<(() => void)[]>([]);
useEffect(
() => () => {
fontCleanupsRef.current.forEach(cleanup => cleanup());
fontCleanupsRef.current = [];
},
[],
);
const handleLoad = useCallback(
(editor: Ace.Editor) => {
const remeasure = () => {
try {
editor.renderer.updateFontSize();
} catch {
// The editor was destroyed before a pending font event fired.
}
};
remeasure();
const { fonts } = document;
if (fonts) {
// The size in the shorthand is irrelevant; any value triggers
// the load of the family's faces.
fonts
.load?.(`12px ${editorFontFamily}`)
?.then(remeasure)
?.catch(() => {});
fonts.ready?.then(remeasure)?.catch(() => {});
fonts.addEventListener?.('loadingdone', remeasure);
fontCleanupsRef.current.push(() =>
fonts.removeEventListener?.('loadingdone', remeasure),
);
}
onLoad?.(editor);
},
[onLoad, editorFontFamily],
);
return (
<>
<Global
@@ -316,8 +389,9 @@ export function AsyncAceEditor(
}
/* Adjust selection color */
.ace_editor .ace_selection {
background-color: ${token.colorEditorSelection ??
token.colorPrimaryBgHover} !important;
background-color: ${
token.colorEditorSelection ?? token.colorPrimaryBgHover
} !important;
}
/* Improve active line highlighting */
@@ -468,6 +542,7 @@ export function AsyncAceEditor(
tabSize={tabSize}
defaultValue={defaultValue}
setOptions={{ fontFamily: editorFontFamily }}
onLoad={handleLoad}
{...props}
/>
</>

View File

@@ -31,12 +31,7 @@ export type { AntdButtonProps, ButtonType, ButtonVariantType, ButtonColorType };
export type OnClickHandler = MouseEventHandler<HTMLElement>;
export type ButtonStyle =
| 'primary'
| 'secondary'
| 'tertiary'
| 'danger'
| 'link'
| 'dashed';
'primary' | 'secondary' | 'tertiary' | 'danger' | 'link' | 'dashed';
export type ButtonSize = 'default' | 'small' | 'xsmall';

View File

@@ -37,13 +37,7 @@ import 'ace-builds/src-noconflict/theme-github';
import 'ace-builds/src-noconflict/theme-monokai';
export type CodeEditorMode =
| 'handlebars'
| 'css'
| 'json'
| 'sql'
| 'markdown'
| 'javascript'
| 'html';
'handlebars' | 'css' | 'json' | 'sql' | 'markdown' | 'javascript' | 'html';
export type CodeEditorTheme = 'light' | 'dark';

View File

@@ -34,8 +34,9 @@ export const DropdownButton = ({
const { type: buttonType } = rest;
// divider implementation for default (non-primary) buttons
const defaultBtnCss = css`
${(!buttonType || buttonType === 'default') &&
`.ant-dropdown-trigger {
${
(!buttonType || buttonType === 'default') &&
`.ant-dropdown-trigger {
position: relative;
&:before {
content: '';
@@ -48,7 +49,8 @@ export const DropdownButton = ({
.anticon {
vertical-align: middle;
}
}`}
}`
}
`;
const button = (
<Dropdown.Button
@@ -58,13 +60,13 @@ export const DropdownButton = ({
defaultBtnCss,
css`
.ant-btn {
height: ${styleConfig?.controlHeight ??
theme.buttonControlHeightSM ??
30}px;
height: ${
styleConfig?.controlHeight ?? theme.buttonControlHeightSM ?? 30
}px;
box-shadow: ${styleConfig?.boxShadow ?? 'none'};
font-size: ${styleConfig?.fontSize ??
theme.buttonFontSize ??
theme.fontSizeSM}px;
font-size: ${
styleConfig?.fontSize ?? theme.buttonFontSize ?? theme.fontSizeSM
}px;
font-weight: ${styleConfig?.fontWeight ?? theme.fontWeightStrong};
}
`,

View File

@@ -221,18 +221,22 @@ export const DynamicEditableTitle = memo(
onPressEnter={handleKeyPress}
placeholder={placeholder}
css={css`
${!canEdit &&
`&[disabled] {
${
!canEdit &&
`&[disabled] {
cursor: default;
}
`}
`
}
font-size: ${theme.fontSizeXL}px;
transition: auto;
${inputWidth &&
inputWidth > 0 &&
css`
width: ${inputWidth}px;
`}
${
inputWidth &&
inputWidth > 0 &&
css`
width: ${inputWidth}px;
`
}
`}
disabled={!canEdit}
/>

View File

@@ -167,9 +167,11 @@ export const EmptyState: React.FC<EmptyStateProps> = ({
{image && <ImageContainer image={image} size={size} />}
<div
css={(theme: SupersetTheme) => css`
max-width: ${containerSize === 'large'
? theme.sizeUnit * 150
: theme.sizeUnit * 100}px;
max-width: ${
containerSize === 'large'
? theme.sizeUnit * 150
: theme.sizeUnit * 100
}px;
`}
>
{title && <Title size={effectiveTextSize}>{title}</Title>}

View File

@@ -21,8 +21,7 @@ export type { FormProps, FormInstance, FormItemProps } from 'antd/es/form';
export interface LabeledErrorBoundInputProps {
label?: string;
validationMethods:
| { onBlur: (value: any) => void }
| { onChange: (value: any) => void };
{ onBlur: (value: any) => void } | { onChange: (value: any) => void };
errorMessage?: string | null;
helpText?: string;
required?: boolean;

View File

@@ -77,8 +77,7 @@ customIcons.forEach(customIcon => {
});
export type IconNameType =
| keyof typeof antdEnhancedIcons
| keyof typeof iconOverrides;
keyof typeof antdEnhancedIcons | keyof typeof iconOverrides;
type IconComponentType = Record<IconNameType, IconComponent>;

View File

@@ -27,12 +27,7 @@ import type {
export type OnClickHandler = MouseEventHandler<HTMLElement>;
export type LabelType =
| 'success'
| 'warning'
| 'error'
| 'info'
| 'default'
| 'primary';
'success' | 'warning' | 'error' | 'info' | 'default' | 'primary';
export interface LabelProps extends HTMLAttributes<HTMLSpanElement> {
key?: string;

View File

@@ -17,10 +17,7 @@
* under the License.
*/
export type PositionOption =
| 'floating'
| 'inline'
| 'inline-centered'
| 'normal';
'floating' | 'inline' | 'inline-centered' | 'normal';
export type SizeOption = 's' | 'm' | 'l';

View File

@@ -81,11 +81,4 @@ export type Tags = {
};
export type ContentType =
| Dashboards
| Description
| LastModified
| Owner
| Rows
| Sql
| Table
| Tags;
Dashboards | Description | LastModified | Owner | Rows | Sql | Table | Tags;

View File

@@ -73,14 +73,16 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
const closeButtonWidth = theme.sizeUnit * 14;
return css`
${responsive &&
css`
max-width: ${maxWidth ?? '900px'};
padding-left: ${theme.sizeUnit * 3}px;
padding-right: ${theme.sizeUnit * 3}px;
padding-bottom: 0;
top: 0;
`}
${
responsive &&
css`
max-width: ${maxWidth ?? '900px'};
padding-left: ${theme.sizeUnit * 3}px;
padding-right: ${theme.sizeUnit * 3}px;
padding-bottom: 0;
top: 0;
`
}
.ant-modal-content {
background-color: ${theme.colorBgContainer};
@@ -168,40 +170,46 @@ export const StyledModal = styled(BaseModal)<StyledModalProps>`
padding: 0;
}
${draggable &&
css`
.ant-modal-header {
padding: 0;
${
draggable &&
css`
.ant-modal-header {
padding: 0;
.draggable-trigger {
cursor: move;
padding: ${theme.sizeUnit * 4}px ${closeButtonWidth}px
${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px;
width: 100%;
}
}
`}
${resizable &&
css`
.resizable {
pointer-events: all;
.resizable-wrapper {
height: 100%;
}
.ant-modal-content {
height: 100%;
.ant-modal-body {
height: ${hideFooter
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`};
.draggable-trigger {
cursor: move;
padding: ${theme.sizeUnit * 4}px ${closeButtonWidth}px
${theme.sizeUnit * 4}px ${theme.sizeUnit * 4}px;
width: 100%;
}
}
}
`}
`
}
${
resizable &&
css`
.resizable {
pointer-events: all;
.resizable-wrapper {
height: 100%;
}
.ant-modal-content {
height: 100%;
.ant-modal-body {
height: ${
hideFooter
? `calc(100% - ${MODAL_HEADER_HEIGHT}px)`
: `calc(100% - ${MODAL_HEADER_HEIGHT}px - ${MODAL_FOOTER_HEIGHT}px)`
};
}
}
}
`
}
`;
}}
`;

View File

@@ -18,9 +18,6 @@
*/
import { useEffect, useMemo, useState } from 'react';
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
// TODO: Upgrade to remark-gfm v4+ after migrating to React 18.
// remark-gfm v4+ requires react-markdown v9+, which requires React 18.
// Currently pinned to v3.0.1 for compatibility with react-markdown v8 and React 17.
import remarkGfm from 'remark-gfm';
import { cloneDeep, mergeWith } from 'lodash-es';
import { FeatureFlag, isFeatureEnabled } from '../../utils';
@@ -139,7 +136,7 @@ export function SafeMarkdown({
rehypePlugins={rehypePlugins}
remarkPlugins={[remarkGfm]}
skipHtml={false}
transformLinkUri={transformLinkUri}
urlTransform={transformLinkUri}
>
{source}
</ReactMarkdown>

View File

@@ -51,8 +51,10 @@ const StyledTabs = ({
.ant-tabs-content-holder {
overflow: ${allowOverflow ? 'visible' : 'auto'};
${fullHeight && 'height: 100%;'}
${contentHeight &&
`height: ${typeof contentHeight === 'number' ? `${contentHeight}px` : contentHeight};`}
${
contentHeight &&
`height: ${typeof contentHeight === 'number' ? `${contentHeight}px` : contentHeight};`
}
${contentPadding}
}
.ant-tabs-content {
@@ -66,9 +68,11 @@ const StyledTabs = ({
margin: 0;
}
.ant-tabs-nav-wrap {
${!(tabBarStyle && 'paddingLeft' in tabBarStyle)
? `padding: 0 ${theme.sizeUnit * 4}px;`
: ''}
${
!(tabBarStyle && 'paddingLeft' in tabBarStyle)
? `padding: 0 ${theme.sizeUnit * 4}px;`
: ''
}
}
.ant-tabs-tab {
flex: 1 1 auto;

View File

@@ -51,9 +51,7 @@ export type JsonPrimitive = string | number | boolean | null;
* (Ref: https://github.com/microsoft/TypeScript/issues/15300).
*/
export type StrictJsonValue =
| JsonPrimitive
| StrictJsonObject
| StrictJsonArray;
JsonPrimitive | StrictJsonObject | StrictJsonArray;
export type StrictJsonArray = StrictJsonValue[];
/**
* More strict JSON objects that makes sure all values are plain objects.
@@ -79,12 +77,7 @@ export type Mode = RequestInit['mode'];
export type Redirect = RequestInit['redirect'];
export type ClientTimeout = number | undefined;
export type ParseMethod =
| 'json'
| 'json-bigint'
| 'text'
| 'raw'
| null
| undefined;
'json' | 'json-bigint' | 'text' | 'raw' | null | undefined;
export type Signal = RequestInit['signal'];
export type Stringify = boolean;
export type Url = string;

View File

@@ -18,12 +18,6 @@
*/
export type RowDataValue =
| string
| number
| boolean
| Date
| bigint
| null
| undefined;
string | number | boolean | Date | bigint | null | undefined;
export type RowData = Record<string, RowDataValue>;

View File

@@ -36,9 +36,7 @@ export interface ItemWithLoader<T> {
type InclusiveLoaderResult<V> = V | Promise<V>;
export type RegistryValue<V, W extends InclusiveLoaderResult<V>> =
| V
| W
| undefined;
V | W | undefined;
export type RegistryEntry<V, W extends InclusiveLoaderResult<V>> = {
key: string;

View File

@@ -59,8 +59,7 @@ export default async function handleError(error: ErrorInput): Promise<never> {
errorMessage = `${status} ${statusText}`;
try {
errorJson = (await error.json()) as
| SupersetApiErrorPayload
| SupersetApiMultiErrorsPayload;
SupersetApiErrorPayload | SupersetApiMultiErrorsPayload;
originalError = errorJson;
} catch (error_) {
originalError = error;

View File

@@ -46,10 +46,7 @@ interface TimeoutError {
}
type ErrorType =
| SupersetClientResponse
| TimeoutError
| { response: Response }
| string;
SupersetClientResponse | TimeoutError | { response: Response } | string;
type ErrorTextSource = 'dashboard' | 'chart' | 'query' | 'dataset' | 'database';
@@ -162,10 +159,7 @@ export function parseErrorJson(responseJson: JsonObject): ClientErrorObject {
export function getClientErrorObject(
response:
| SupersetClientResponse
| TimeoutError
| { response: Response }
| string,
SupersetClientResponse | TimeoutError | { response: Response } | string,
): Promise<ClientErrorObject> {
// takes a SupersetClientResponse as input, attempts to read response as Json
// if possible, and returns a Promise that resolves to a plain object with

View File

@@ -186,10 +186,7 @@ export function isFilterDivider(
export function isChartCustomization(
filterElement:
| Filter
| Divider
| ChartCustomization
| ChartCustomizationDivider,
Filter | Divider | ChartCustomization | ChartCustomizationDivider,
): filterElement is ChartCustomization {
return filterElement.type === ChartCustomizationType.ChartCustomization;
}
@@ -204,10 +201,7 @@ export type FilterConfiguration = Array<Filter | Divider>;
export type Filters = {
[filterId: string]:
| Filter
| Divider
| ChartCustomization
| ChartCustomizationDivider;
Filter | Divider | ChartCustomization | ChartCustomizationDivider;
};
export type PartialFilters = {
@@ -226,8 +220,7 @@ export type ChartCustomizationConfiguration = Array<
export type ChartCustomizations = {
[chartCustomizationId: string]:
| ChartCustomization
| ChartCustomizationDivider;
ChartCustomization | ChartCustomizationDivider;
};
export type PartialChartCustomizations = {

View File

@@ -53,9 +53,7 @@ export type SetAdhocFilter = BaseSimpleAdhocFilter & {
};
export type SimpleAdhocFilter =
| UnaryAdhocFilter
| BinaryAdhocFilter
| SetAdhocFilter;
UnaryAdhocFilter | BinaryAdhocFilter | SetAdhocFilter;
export interface FreeFormAdhocFilter extends BaseAdhocFilter {
expressionType: 'SQL';

View File

@@ -21,12 +21,7 @@ import { Currency, Maybe, QueryFormMetric } from '../../types';
import { Column } from './Column';
export type Aggregate =
| 'AVG'
| 'COUNT'
| 'COUNT_DISTINCT'
| 'MAX'
| 'MIN'
| 'SUM';
'AVG' | 'COUNT' | 'COUNT_DISTINCT' | 'MAX' | 'MIN' | 'SUM';
export interface AdhocMetricBase {
hasCustomLabel?: boolean;

View File

@@ -74,8 +74,7 @@ interface _PostProcessingAggregation {
};
}
export type PostProcessingAggregation =
| _PostProcessingAggregation
| DefaultPostProcessing;
_PostProcessingAggregation | DefaultPostProcessing;
export type BoxPlotQueryObjectWhiskerType = 'tukey' | 'min/max' | 'percentile';
interface _PostProcessingBoxplot {
@@ -88,8 +87,7 @@ interface _PostProcessingBoxplot {
};
}
export type PostProcessingBoxplot =
| _PostProcessingBoxplot
| DefaultPostProcessing;
_PostProcessingBoxplot | DefaultPostProcessing;
interface _PostProcessingContribution {
operation: 'contribution';
@@ -101,8 +99,7 @@ interface _PostProcessingContribution {
};
}
export type PostProcessingContribution =
| _PostProcessingContribution
| DefaultPostProcessing;
_PostProcessingContribution | DefaultPostProcessing;
interface _PostProcessingPivot {
operation: 'pivot';
@@ -132,8 +129,7 @@ interface _PostProcessingProphet {
};
}
export type PostProcessingProphet =
| _PostProcessingProphet
| DefaultPostProcessing;
_PostProcessingProphet | DefaultPostProcessing;
interface _PostProcessingDiff {
operation: 'diff';
@@ -155,8 +151,7 @@ interface _PostProcessingRolling {
};
}
export type PostProcessingRolling =
| _PostProcessingRolling
| DefaultPostProcessing;
_PostProcessingRolling | DefaultPostProcessing;
interface _PostProcessingCum {
operation: 'cum';
@@ -177,8 +172,7 @@ export interface _PostProcessingCompare {
};
}
export type PostProcessingCompare =
| _PostProcessingCompare
| DefaultPostProcessing;
_PostProcessingCompare | DefaultPostProcessing;
interface _PostProcessingSort {
operation: 'sort';
@@ -199,8 +193,7 @@ interface _PostProcessingResample {
};
}
export type PostProcessingResample =
| _PostProcessingResample
| DefaultPostProcessing;
_PostProcessingResample | DefaultPostProcessing;
interface _PostProcessingRename {
operation: 'rename';
@@ -211,8 +204,7 @@ interface _PostProcessingRename {
};
}
export type PostProcessingRename =
| _PostProcessingRename
| DefaultPostProcessing;
_PostProcessingRename | DefaultPostProcessing;
interface _PostProcessingFlatten {
operation: 'flatten';
@@ -222,8 +214,7 @@ interface _PostProcessingFlatten {
};
}
export type PostProcessingFlatten =
| _PostProcessingFlatten
| DefaultPostProcessing;
_PostProcessingFlatten | DefaultPostProcessing;
interface _PostProcessingRank {
operation: 'rank';
@@ -245,8 +236,7 @@ interface _PostProcessingHistogram {
};
}
export type PostProcessingHistogram =
| _PostProcessingHistogram
| DefaultPostProcessing;
_PostProcessingHistogram | DefaultPostProcessing;
/**
* Parameters for chart data postprocessing.

View File

@@ -51,8 +51,7 @@ export type QueryFormColumn = PhysicalColumn | AdhocColumn;
* Format: [metric/column, is_ascending].
*/
export type QueryFormOrderBy =
| [QueryFormColumn | QueryFormMetric | {}, boolean]
| [];
[QueryFormColumn | QueryFormMetric | {}, boolean] | [];
export interface FormDataResidual {
[key: string]: any;

View File

@@ -30,14 +30,7 @@ export enum ComparisonTimeRangeType {
}
export type DateTimeGrainType =
| 'second'
| 'minute'
| 'hour'
| 'day'
| 'week'
| 'month'
| 'quarter'
| 'year';
'second' | 'minute' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
export type CustomRangeKey =
| 'sinceMode'

View File

@@ -30,8 +30,7 @@ const defaultMockLoadFormData = jest.fn(({ formData }: { formData: unknown }) =>
);
type MockLoadFormData =
| typeof defaultMockLoadFormData
| jest.Mock<Promise<unknown>, unknown[]>;
typeof defaultMockLoadFormData | jest.Mock<Promise<unknown>, unknown[]>;
let mockLoadFormData: MockLoadFormData = defaultMockLoadFormData;

View File

@@ -57,6 +57,7 @@ import { SearchOption, SortByItem } from '../types';
import getInitialSortState, { shouldSort } from '../utils/getInitialSortState';
import getInitialFilterModel from '../utils/getInitialFilterModel';
import reconcileColumnState from '../utils/reconcileColumnState';
import getColumnStateSignature from '../utils/getColumnStateSignature';
import { PAGE_SIZE_OPTIONS } from '../consts';
import { getCompleteFilterState } from '../utils/filterStateManager';
@@ -337,11 +338,11 @@ const AgGridDataTable: FunctionComponent<AgGridTableProps> = memo(
timestamp: Date.now(),
};
const stateHash = JSON.stringify({
columnOrder: columnState.map(c => c.colId),
sorts: sortModel,
filters: filterModel,
});
const stateHash = getColumnStateSignature(
columnState,
sortModel,
filterModel,
);
if (stateHash !== lastCapturedStateRef.current) {
lastCapturedStateRef.current = stateHash;

View File

@@ -80,8 +80,7 @@ function getQueryMode(controls: ControlStateMapping): QueryMode {
return mode as QueryMode;
}
const rawColumns = controls?.all_columns?.value as
| QueryFormColumn[]
| undefined;
QueryFormColumn[] | undefined;
const hasRawColumns = rawColumns && rawColumns.length > 0;
return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
}

View File

@@ -753,8 +753,7 @@ const transformProps = (
// Strip saved filter from chartState after initial application to prevent re-injection
let chartState = serverPaginationData?.chartState as
| AgGridChartState
| undefined;
AgGridChartState | undefined;
const chartStateHasFilter = !!(
chartState?.filterModel && Object.keys(chartState.filterModel).length > 0
);

View File

@@ -189,10 +189,7 @@ export interface InputColumn {
isPercentMetric: boolean;
config: TableColumnConfig;
formatter?:
| TimeFormatter
| NumberFormatter
| CustomFormatter
| CurrencyFormatter;
TimeFormatter | NumberFormatter | CustomFormatter | CurrencyFormatter;
originalLabel?: string;
metricName?: string;
}

View File

@@ -0,0 +1,51 @@
/**
* 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 type { ColumnState } from '@superset-ui/core/components/ThemedAgGridReact';
type SortModelEntry = {
colId: string;
sort: 'asc' | 'desc';
sortIndex: number;
};
/**
* Stable signature of the persisted parts of AG Grid column state (order,
* value aggregation, sorts, filters), used to detect changes worth saving.
* `aggFunc` is normalized so the "None" option (`null`/`undefined`) produces
* a distinct signature instead of reverting to the default on reload.
*/
export default function getColumnStateSignature(
columnState: ColumnState[],
sortModel: SortModelEntry[],
filterModel: Record<string, unknown>,
): string {
return JSON.stringify({
columnOrder: columnState.map(col => col.colId),
aggregations: columnState.map(col => ({
colId: col.colId,
// "None" (null/undefined) maps to an explicit sentinel; functions map
// to a fixed marker so the signature never depends on JSON.stringify
// dropping them (defensive: persisted aggFuncs are always strings).
aggFunc:
typeof col.aggFunc === 'function' ? 'custom' : (col.aggFunc ?? null),
})),
sorts: sortModel,
filters: filterModel,
});
}

View File

@@ -84,3 +84,20 @@ test('drops stale order when a dynamic group by swaps the dimension column', ()
],
});
});
test('preserves per-column aggFunc, including explicit "None" (null), through reconciliation', () => {
// Regression #107166: saved aggFunc must reach applyColumnState untouched.
const colDefs: ColDef[] = [{ field: 'name' }, { field: 'SUM(Sales_Amount)' }];
const savedColumnState: ColumnState[] = [
{ colId: 'name' },
{ colId: 'SUM(Sales_Amount)', aggFunc: null },
];
expect(reconcileColumnState(savedColumnState, colDefs)).toEqual({
applyOrder: true,
columnState: [
{ colId: 'name' },
{ colId: 'SUM(Sales_Amount)', aggFunc: null },
],
});
});

View File

@@ -20,7 +20,10 @@ import '@testing-library/jest-dom';
import { render, screen, waitFor } from '@superset-ui/core/spec';
import { QueryMode, TimeGranularity, SMART_DATE_ID } from '@superset-ui/core';
import { GenericDataType } from '@apache-superset/core/common';
import { setupAGGridModules } from '@superset-ui/core/components/ThemedAgGridReact';
import {
setupAGGridModules,
type ColumnState,
} from '@superset-ui/core/components/ThemedAgGridReact';
import AgGridTableChart from '../src/AgGridTableChart';
import transformProps from '../src/transformProps';
import { ProviderWrapper } from '../../plugin-chart-table/test/testHelpers';
@@ -726,3 +729,46 @@ test('AgGridTableChart pins no summary row when totals are absent', async () =>
const pinnedRows = document.querySelectorAll('.ag-floating-bottom .ag-row');
expect(pinnedRows.length).toBe(0);
});
test('AgGridTableChart emits column state with aggFunc through the debounced save path', async () => {
const props = transformProps(testData.basic);
const onChartStateChange = jest.fn();
render(
ProviderWrapper({
children: (
<AgGridTableChart
{...props}
setDataMask={mockSetDataMask}
slice_id={1}
onChartStateChange={onChartStateChange}
chartState={{
columnState: [{ colId: 'sum__num', aggFunc: null }],
sortModel: [],
filterModel: {},
}}
/>
),
}),
);
await waitFor(() => {
expect(document.querySelector('.ag-container')).toBeInTheDocument();
});
// The save path is debounced (SLOW_DEBOUNCE = 500ms); wait for a capture.
await waitFor(() => expect(onChartStateChange).toHaveBeenCalled(), {
timeout: 3000,
});
const savedState =
onChartStateChange.mock.calls[onChartStateChange.mock.calls.length - 1][0];
const savedColumn = (savedState.columnState as ColumnState[]).find(
col => col.colId === 'sum__num',
);
// Entries must carry an aggFunc key so "None" survives reload. The value
// itself is not assertable here: aggregation state needs an enterprise
// (SharedAggregation) module; the community modules always report null.
expect(savedColumn).toMatchObject({ aggFunc: null });
});

View File

@@ -47,8 +47,7 @@ const findConditionalFormattingControl = (): ControlConfig | null => {
};
const findMetricsMapStateToProps = ():
| ControlConfig['mapStateToProps']
| null => {
ControlConfig['mapStateToProps'] | null => {
for (const section of config.controlPanelSections) {
if (!section) continue;
for (const row of section.controlSetRows) {

View File

@@ -0,0 +1,82 @@
/**
* 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 type { ColumnState } from '@superset-ui/core/components/ThemedAgGridReact';
import getColumnStateSignature from '../../src/utils/getColumnStateSignature';
const filterModel = {};
test('signature changes when a column value aggregation changes', () => {
const before = getColumnStateSignature(
[{ colId: 'sales', aggFunc: 'sum' }],
[],
filterModel,
);
const after = getColumnStateSignature(
[{ colId: 'sales', aggFunc: 'avg' }],
[],
filterModel,
);
expect(after).not.toEqual(before);
});
test('switching value aggregation to "None" (null/undefined) changes the signature', () => {
// Regression #107166: "None" must be captured so it persists on reload.
const sumState = getColumnStateSignature(
[{ colId: 'sales', aggFunc: 'sum' }],
[],
filterModel,
);
const noneNull = getColumnStateSignature(
[{ colId: 'sales', aggFunc: null }],
[],
filterModel,
);
const noneUndefined = getColumnStateSignature(
[{ colId: 'sales' }],
[],
filterModel,
);
expect(noneNull).not.toEqual(sumState);
expect(noneUndefined).not.toEqual(sumState);
// null and undefined both represent "None" and must be treated identically.
expect(noneNull).toEqual(noneUndefined);
});
test('signature is stable when nothing changes', () => {
const columnState: ColumnState[] = [{ colId: 'sales', aggFunc: 'sum' }];
const a = getColumnStateSignature(columnState, [], filterModel);
const b = getColumnStateSignature([...columnState], [], filterModel);
expect(a).toEqual(b);
});
test('a function aggFunc is distinguishable from "None"', () => {
// Pins the defensive marker: function aggFuncs stay distinct from "None".
const customAgg = getColumnStateSignature(
[{ colId: 'sales', aggFunc: () => 42 }],
[],
filterModel,
);
const none = getColumnStateSignature(
[{ colId: 'sales', aggFunc: null }],
[],
filterModel,
);
expect(customAgg).not.toEqual(none);
});

View File

@@ -42,11 +42,7 @@ export type BoxPlotFormDataWhiskerOptions =
| '10/90 percentiles';
export type BoxPlotFormXTickLayout =
| '45°'
| '90°'
| 'auto'
| 'flat'
| 'staggered';
'45°' | '90°' | 'auto' | 'flat' | 'staggered';
// @ts-expect-error
export const DEFAULT_FORM_DATA: BoxPlotQueryFormData = {

View File

@@ -221,8 +221,7 @@ const config: ControlPanelConfig = {
return {
queryResponse: chart?.queriesResponse?.[0] as
| ChartDataResponseResult
| undefined,
ChartDataResponseResult | undefined,
appliedColumnNames: metricColumn,
columnsPropsObject: { colnames, coltypes },
};

View File

@@ -521,8 +521,7 @@ export function transformIntervalAnnotation(
});
const allIntervalData: (
| MarkArea1DDataItemOption
| MarkArea2DDataItemOption
MarkArea1DDataItemOption | MarkArea2DDataItemOption
)[] = annotations.map(annotation => {
const { intervalEnd, time = '' } = annotation;
const combinedLabel = (intervalsByStartTime.get(time) || []).join('\n');

View File

@@ -29,11 +29,7 @@ import type { CallbackDataParams } from 'echarts/types/src/util/types';
import { BaseTransformedProps, LegendFormData } from '../types';
export type WaterfallFormXTicksLayout =
| '45°'
| '90°'
| 'auto'
| 'flat'
| 'staggered';
'45°' | '90°' | 'auto' | 'flat' | 'staggered';
export type ISeriesData = {
originalValue?: number;

View File

@@ -21,11 +21,7 @@ import { LegendOrientation, LegendType } from '../types';
import { getLegendLayoutResult, LegendLayoutResult } from './series';
type LegendDataItem =
| string
| number
| null
| undefined
| { name?: string | number | null };
string | number | null | undefined | { name?: string | number | null };
export type ResolvedLegendLayout = {
effectiveLegendMargin?: string | number | null;

View File

@@ -46,9 +46,7 @@ import {
*/
export class EChartOptionsParseError extends Error {
public readonly errorType:
| 'parse_error'
| 'security_error'
| 'validation_error';
'parse_error' | 'security_error' | 'validation_error';
public readonly validationErrors: string[];
@@ -57,9 +55,7 @@ export class EChartOptionsParseError extends Error {
constructor(
message: string,
errorType:
| 'parse_error'
| 'security_error'
| 'validation_error' = 'parse_error',
'parse_error' | 'security_error' | 'validation_error' = 'parse_error',
validationErrors: string[] = [],
location?: { line: number; column: number },
) {

View File

@@ -73,11 +73,7 @@ const ESTIMATED_LEGEND_SELECTOR_WIDTH = 112;
const LEGEND_TEXT_WIDTH_CACHE = new Map<string, number>();
type LegendDataItem =
| string
| number
| null
| undefined
| { name?: string | number | null };
string | number | null | undefined | { name?: string | number | null };
export type LegendLayoutResult = {
effectiveMargin?: number;

View File

@@ -968,11 +968,9 @@ test('should apply dashed line style to time comparison series with single metri
const series = (transformed.echartOptions.series as SeriesOption[]) || [];
const mainSeries = series.find(s => s.name === 'sum__num') as
| (SeriesOption & { lineStyle?: { type?: number[] | string } })
| undefined;
(SeriesOption & { lineStyle?: { type?: number[] | string } }) | undefined;
const comparisonSeries = series.find(s => s.name === '1 week ago') as
| (SeriesOption & { lineStyle?: { type?: number[] | string } })
| undefined;
(SeriesOption & { lineStyle?: { type?: number[] | string } }) | undefined;
expect(mainSeries).toBeDefined();
expect(comparisonSeries).toBeDefined();
@@ -1013,13 +1011,11 @@ test('should apply dashed line style to time comparison series with metric__offs
const series = (transformed.echartOptions.series as SeriesOption[]) || [];
const mainSeries = series.find(s => s.name === 'sum__num') as
| (SeriesOption & { lineStyle?: { type?: number[] | string } })
| undefined;
(SeriesOption & { lineStyle?: { type?: number[] | string } }) | undefined;
const comparisonSeries = series.find(
s => s.name === 'sum__num__1 week ago',
) as
| (SeriesOption & { lineStyle?: { type?: number[] | string } })
| undefined;
(SeriesOption & { lineStyle?: { type?: number[] | string } }) | undefined;
expect(mainSeries).toBeDefined();
expect(comparisonSeries).toBeDefined();
@@ -1051,8 +1047,7 @@ test('should apply connectNulls to time comparison series', () => {
const series = (transformed.echartOptions.series as SeriesOption[]) || [];
const comparisonSeries = series.find(s => s.name === '1 week ago') as
| (SeriesOption & { connectNulls?: boolean })
| undefined;
(SeriesOption & { connectNulls?: boolean }) | undefined;
expect(comparisonSeries).toBeDefined();
expect(comparisonSeries?.connectNulls).toBe(true);

View File

@@ -67,11 +67,7 @@ const {
chartHeight: number;
chartWidth: number;
legendItems?: (
| string
| number
| null
| undefined
| { name?: string | number | null }
string | number | null | undefined | { name?: string | number | null }
)[];
legendMargin?: string | number | null;
orientation: LegendOrientation;
@@ -94,11 +90,7 @@ const {
chartHeight: number;
chartWidth: number;
legendItems?: (
| string
| number
| null
| undefined
| { name?: string | number | null }
string | number | null | undefined | { name?: string | number | null }
)[];
legendMargin?: string | number | null;
orientation: LegendOrientation;

View File

@@ -29,8 +29,7 @@ export function getQueryMode(controls: ControlStateMapping): QueryMode {
return mode as QueryMode;
}
const rawColumns = controls?.all_columns?.value as
| QueryFormColumn[]
| undefined;
QueryFormColumn[] | undefined;
const hasRawColumns = rawColumns && rawColumns.length > 0;
return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
}

View File

@@ -900,8 +900,7 @@ class PivotData {
props: Record<string, unknown>;
aggregator: (...args: unknown[]) => Aggregator;
formattedAggregators:
| Record<string, Record<string, (...args: unknown[]) => Aggregator>>
| false;
Record<string, Record<string, (...args: unknown[]) => Aggregator>> | false;
tree: Record<string, Record<string, Aggregator>>;
rowKeys: string[][];
colKeys: string[][];

View File

@@ -42,9 +42,7 @@ export type FilterType = Record<string, DataRecordValue>;
export type SelectedFiltersType = Record<string, DataRecordValue[]>;
export type DateFormatter =
| TimeFormatter
| NumberFormatter
| ((value: DataRecordValue) => string);
TimeFormatter | NumberFormatter | ((value: DataRecordValue) => string);
export enum MetricsLayoutEnum {
ROWS = 'ROWS',
COLUMNS = 'COLUMNS',

View File

@@ -1135,13 +1135,15 @@ export default function TableChart<D extends DataRecord = DataRecord>(
text-align: ${sharedStyle.textAlign};
white-space: ${value instanceof Date ? 'nowrap' : undefined};
position: relative;
font-weight: ${color
? `${theme.fontWeightBold}`
: `${theme.fontWeightNormal}`};
font-weight: ${
color ? `${theme.fontWeightBold}` : `${theme.fontWeightNormal}`
};
background: ${backgroundColor || undefined};
padding-left: ${column.isChildColumn
? `${theme.sizeUnit * 5}px`
: `${theme.sizeUnit}px`};
padding-left: ${
column.isChildColumn
? `${theme.sizeUnit * 5}px`
: `${theme.sizeUnit}px`
};
`;
const cellBarStyles = css`
@@ -1149,10 +1151,11 @@ export default function TableChart<D extends DataRecord = DataRecord>(
height: 100%;
display: block;
top: 0;
${valueRange &&
typeof value === 'number' &&
valueRangeFlag &&
`
${
valueRange &&
typeof value === 'number' &&
valueRangeFlag &&
`
width: ${`${cellWidth({
value: value as number,
valueRange,
@@ -1171,15 +1174,18 @@ export default function TableChart<D extends DataRecord = DataRecord>(
theme,
})
};
`}
`
}
`;
let arrowStyles = css`
color: ${basicColorFormatters &&
basicColorFormatters[row.index][originKey]?.arrowColor ===
ColorSchemeEnum.Green
? theme.colorSuccess
: theme.colorError};
color: ${
basicColorFormatters &&
basicColorFormatters[row.index][originKey]?.arrowColor ===
ColorSchemeEnum.Green
? theme.colorSuccess
: theme.colorError
};
margin-right: ${theme.sizeUnit}px;
`;
@@ -1188,10 +1194,12 @@ export default function TableChart<D extends DataRecord = DataRecord>(
basicColorColumnFormatters?.length > 0
) {
arrowStyles = css`
color: ${basicColorColumnFormatters[row.index][column.key]
?.arrowColor === ColorSchemeEnum.Green
? theme.colorSuccess
: theme.colorError};
color: ${
basicColorColumnFormatters[row.index][column.key]
?.arrowColor === ColorSchemeEnum.Green
? theme.colorSuccess
: theme.colorError
};
margin-right: ${theme.sizeUnit}px;
`;
}

View File

@@ -66,8 +66,7 @@ function getQueryMode(controls: ControlStateMapping): QueryMode {
return mode as QueryMode;
}
const rawColumns = controls?.all_columns?.value as
| QueryFormColumn[]
| undefined;
QueryFormColumn[] | undefined;
const hasRawColumns = rawColumns && rawColumns.length > 0;
return hasRawColumns ? QueryMode.Raw : QueryMode.Aggregate;
}

View File

@@ -48,8 +48,7 @@ const findConditionalFormattingControl = (): ControlConfig | null => {
};
const findMetricsMapStateToProps = ():
| ControlConfig['mapStateToProps']
| null => {
ControlConfig['mapStateToProps'] | null => {
for (const section of config.controlPanelSections) {
if (!section) continue;
for (const row of section.controlSetRows) {

View File

@@ -17,7 +17,7 @@
* under the License.
*/
// eslint-disable-next-line import/no-extraneous-dependencies
import { render, screen } from '@testing-library/react';
import { createEvent, fireEvent, render, screen } from '@testing-library/react';
// eslint-disable-next-line import/no-extraneous-dependencies
import '@testing-library/jest-dom';
import { supersetTheme, ThemeProvider } from '@apache-superset/core/theme';
@@ -69,3 +69,28 @@ test('leaves labels untouched when no format is provided', () => {
expect(screen.getByText('[1, 81)')).toBeInTheDocument();
});
test('clicking a legend item toggles the category without triggering anchor navigation', () => {
// Regression proof for #33576: the legend items are href="#" anchors, so a
// click whose default action is not prevented would navigate to "#" and
// scroll the browser window to the top of the page.
const toggleCategory = jest.fn();
renderWithTheme(
<Legend
format={null}
categories={{
Positive: { enabled: true, color: [0, 255, 0] },
Negative: { enabled: true, color: [255, 0, 0] },
}}
toggleCategory={toggleCategory}
/>,
);
const legendItem = screen.getByRole('button', { name: 'Positive' });
const clickEvent = createEvent.click(legendItem);
fireEvent(legendItem, clickEvent);
expect(clickEvent.defaultPrevented).toBe(true);
expect(toggleCategory).toHaveBeenCalledTimes(1);
expect(toggleCategory).toHaveBeenCalledWith('Positive');
});

View File

@@ -36,8 +36,7 @@ export interface DeckPathFormData extends SqlaFormData {
tooltip_contents?: unknown[];
tooltip_template?: string;
line_width?:
| string
| { type?: 'fix' | 'metric'; value?: QueryFormMetric | number };
string | { type?: 'fix' | 'metric'; value?: QueryFormMetric | number };
line_width_multiplier?: number;
min_width?: number;
max_width?: number;

View File

@@ -529,8 +529,7 @@ export const mapProvider = {
options: getLabeledMapRendererOptions({
hasMapboxKey: hasKey,
currentValue: state.form_data?.map_renderer as
| MapProvider
| undefined,
MapProvider | undefined,
}),
default: getDefaultMapRenderer(),
};

View File

@@ -422,11 +422,7 @@ const getLineColumnFilters = ({
const obj = data.object ?? {};
const extraProps = (obj.extraProps ?? {}) as Record<string, unknown>;
const dimensionVal = (obj[col] ?? extraProps[col]) as
| string
| number
| boolean
| null
| undefined;
string | number | boolean | null | undefined;
if (dimensionVal == null) {
throw new Error(
@@ -484,11 +480,7 @@ const getGeojsonFilters = ({
unknown
>;
const dimensionVal = properties[col] as
| string
| number
| boolean
| null
| undefined;
string | number | boolean | null | undefined;
if (dimensionVal == null) {
throw new Error(

View File

@@ -348,9 +348,9 @@ const TreeNodeRenderer: React.FC<TreeNodeRendererProps> = ({
<Icons.SortAscendingOutlined
iconSize="m"
css={css`
color: ${sortedTables[data.id]
? theme.colorPrimary
: 'inherit'};
color: ${
sortedTables[data.id] ? theme.colorPrimary : 'inherit'
};
`}
/>
}

View File

@@ -37,10 +37,7 @@ import { BYTES_PER_CHAR, KB_STORAGE } from '../constants';
type SqlLabState = SqlLabRootState['sqlLab'];
type ClearEntityHelperValue =
| Table[]
| SqlLabState['queries']
| QueryEditor[]
| UnsavedQueryEditor;
Table[] | SqlLabState['queries'] | QueryEditor[] | UnsavedQueryEditor;
interface ClearEntityHelpersMap {
tables: (tables: Table[]) => ReturnType<typeof emptyTablePersistData>;

View File

@@ -613,8 +613,7 @@ export default function sqlLabReducer(
? actionId
: ((
getFromArr(state.queryEditors, actionId, 'tabViewId') as
| QueryEditor
| undefined
QueryEditor | undefined
)?.id ?? actionId);
if (
unsavedQueryEditor?.id === normalizedId &&

View File

@@ -188,17 +188,21 @@ export const EmptyFolderDropZone = styled.div<{
margin-left: ${depth * FOLDER_INDENTATION_WIDTH + ITEM_INDENTATION_WIDTH}px;
padding: ${theme.paddingLG}px;
border: 2px dashed
${isOver
? isForbidden
? theme.colorError
: theme.colorPrimary
: 'transparent'};
${
isOver
? isForbidden
? theme.colorError
: theme.colorPrimary
: 'transparent'
};
border-radius: ${theme.borderRadius}px;
background: ${isOver
? isForbidden
? theme.colorErrorBg
: theme.colorPrimaryBg
: 'transparent'};
background: ${
isOver
? isForbidden
? theme.colorErrorBg
: theme.colorPrimaryBg
: 'transparent'
};
text-align: center;
transition: all 0.2s ease-in-out;
cursor: ${isOver && isForbidden ? 'not-allowed' : 'default'};

View File

@@ -305,8 +305,8 @@ export function serializeForAPI(items: TreeItem[]): DatasourceFolder[] {
(
child,
): child is
| DatasourceFolder
| { uuid: string; type: FoldersEditorItemType } => child !== null,
DatasourceFolder | { uuid: string; type: FoldersEditorItemType } =>
child !== null,
);
};

View File

@@ -210,7 +210,11 @@ test('does not attach click handler when enableSorting is false', () => {
test('does not call progressSort on click when enableSorting is false', () => {
const progressSort = jest.fn();
const { getByText } = render(
<Header {...mockedProps} enableSorting={false} progressSort={progressSort} />,
<Header
{...mockedProps}
enableSorting={false}
progressSort={progressSort}
/>,
);
fireEvent.click(getByText(mockedProps.displayName));
expect(progressSort).not.toHaveBeenCalled();

View File

@@ -28,7 +28,7 @@ import { antdWarningAlertStyles } from './styles';
const supersetTextDocs = getDatabaseDocumentationLinks();
export const DOCUMENTATION_LINK = supersetTextDocs
? supersetTextDocs.support
: 'https://superset.apache.org/docs/databases/installing-database-drivers';
: 'https://superset.apache.org/user-docs/databases/#installing-database-drivers';
export interface IProps {
errorMessage: string;

View File

@@ -26,7 +26,7 @@ import { ErrorAlert } from 'src/components';
const supersetTextDocs = getDatabaseDocumentationLinks();
export const DOCUMENTATION_LINK = supersetTextDocs
? supersetTextDocs.support
: 'https://superset.apache.org/docs/databases/installing-database-drivers';
: 'https://superset.apache.org/user-docs/databases/#installing-database-drivers';
export interface IProps {
errorMessage: string;

View File

@@ -106,9 +106,9 @@ const OptionItem = styled.li<{ $active: boolean }>`
transition: background 0.15s;
&:hover {
background: ${$active
? theme.colorPrimaryBgHover
: theme.colorFillTertiary};
background: ${
$active ? theme.colorPrimaryBgHover : theme.colorFillTertiary
};
outline: 2px solid ${theme.colorPrimary};
outline-offset: -2px;
}

View File

@@ -129,6 +129,7 @@ export enum ListViewFilterOperator {
Between = 'between',
DashboardIsFav = 'dashboard_is_favorite',
ChartIsFav = 'chart_is_favorite',
TagIsFav = 'tag_is_favorite',
ChartIsCertified = 'chart_is_certified',
DashboardIsCertified = 'dashboard_is_certified',
DatasetIsCertified = 'dataset_is_certified',

View File

@@ -312,7 +312,7 @@ export function handleComponentDrop(dropResult: DropResult) {
source &&
!(
// ensure it has moved
(destination.id === source.id && destination.index === source.index)
destination.id === source.id && destination.index === source.index
)
) {
dispatch(moveComponent(dropResult));

View File

@@ -725,8 +725,7 @@ export function saveDashboardRequest(
updatedBy: dashboard.changed_by_name as string,
overwriteConfirmItems:
overwriteConfirmItems as DashboardState['overwriteConfirmMetadata'] extends
| { overwriteConfirmItems: infer I }
| undefined
{ overwriteConfirmItems: infer I } | undefined
? I
: never,
dashboardId: id,

View File

@@ -174,8 +174,7 @@ const AddSliceCard: FC<{
datasourceUrl?: string;
datasourceName?: string;
innerRef?:
| RefObject<HTMLDivElement>
| ((node: HTMLDivElement | null) => void);
RefObject<HTMLDivElement> | ((node: HTMLDivElement | null) => void);
isSelected?: boolean;
lastModified?: string;
sliceName: string;

View File

@@ -144,12 +144,14 @@ export const StatusIndicatorDot: FC<StatusIndicatorDotProps> = ({
background-color ${theme.motionDurationMid} ease-in-out,
border-color ${theme.motionDurationMid} ease-in-out;
border: ${statusConfig.needsBorder ? '1px solid' : 'none'};
border-color: ${statusConfig.needsBorder
? statusConfig.outlineColor
: 'transparent'};
box-shadow: ${statusConfig.needsBorder
? 'none'
: `0 0 0 2px ${theme.colorBgContainer}`};
border-color: ${
statusConfig.needsBorder ? statusConfig.outlineColor : 'transparent'
};
box-shadow: ${
statusConfig.needsBorder
? 'none'
: `0 0 0 2px ${theme.colorBgContainer}`
};
margin-left: ${theme.marginXS}px;
margin-right: ${theme.marginXS}px;
cursor: help;

View File

@@ -53,9 +53,7 @@ import type {
} from '../types';
type RelatedChartsFilter =
| AppliedNativeFilterType
| AppliedCrossFilterType
| Filter;
AppliedNativeFilterType | AppliedCrossFilterType | Filter;
interface DashboardActions {
addSliceToDashboard: (id: number, component: LayoutItem | undefined) => void;

View File

@@ -141,8 +141,9 @@ const DashboardContentWrapper = styled.div`
& .dashboard-component-tabs {
box-shadow: 0 ${theme.sizeUnit}px ${theme.sizeUnit}px 0
${addAlpha(theme.colorBorderSecondary, 0.1)};
padding-left: ${theme.sizeUnit *
2}px; /* note this is added to tab-level padding, to match header */
padding-left: ${
theme.sizeUnit * 2
}px; /* note this is added to tab-level padding, to match header */
}
.dropdown-toggle.btn.btn-primary .caret {
@@ -295,12 +296,14 @@ const StyledDashboardContent = styled.div<{
margin: ${theme.sizeUnit * 4}px;
margin-left: ${marginLeft}px;
${editMode &&
`
${
editMode &&
`
max-width: calc(100% - ${
BUILDER_SIDEPANEL_WIDTH + theme.sizeUnit * 16
}px);
`}
`
}
/* this is the ParentSize wrapper */
& > div:first-of-type {
@@ -337,8 +340,10 @@ const StyledDashboardContent = styled.div<{
}
&.fade-out {
box-shadow: ${theme.dashboardTileBoxShadow ??
`0 0 0 1px ${addAlpha(theme.colorBorder, 0.5)}`};
box-shadow: ${
theme.dashboardTileBoxShadow ??
`0 0 0 1px ${addAlpha(theme.colorBorder, 0.5)}`
};
}
& .missing-chart-container {

View File

@@ -75,8 +75,7 @@ const defaultTestFilter = {
function createTestState(overrides: Record<string, unknown> = {}) {
const nativeFilterConfig = (
overrides.dashboardInfo as
| { metadata?: { native_filter_configuration?: unknown[] } }
| undefined
{ metadata?: { native_filter_configuration?: unknown[] } } | undefined
)?.metadata?.native_filter_configuration ?? [defaultTestFilter];
const nativeFiltersMap: Record<string, unknown> = {};

View File

@@ -26,7 +26,12 @@ import {
useState,
} from 'react';
import { t } from '@apache-superset/core/translation';
import { getExtensionsRegistry, QueryData, VizType } from '@superset-ui/core';
import {
getExtensionsRegistry,
JsonObject,
QueryData,
VizType,
} from '@superset-ui/core';
import {
css,
styled,
@@ -58,6 +63,7 @@ type SliceHeaderProps = SliceHeaderControlsProps & {
filters: object;
handleToggleFullSize: () => void;
formData: object;
ownState?: JsonObject;
width: number;
height: number;
queriedDttm?: string | null;
@@ -174,6 +180,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
height,
exportPivotExcel = () => ({}),
chartHolderRef,
ownState,
},
ref,
) => {
@@ -381,6 +388,7 @@ const SliceHeader = forwardRef<HTMLDivElement, SliceHeaderProps>(
crossFiltersEnabled={isCrossFiltersEnabled}
exportPivotExcel={exportPivotExcel}
chartHolderRef={chartHolderRef}
ownState={ownState}
/>
)}
</>

View File

@@ -36,6 +36,7 @@ import {
getChartMetadataRegistry,
VizType,
BinaryQueryObjectFilterClause,
JsonObject,
QueryFormData,
} from '@superset-ui/core';
import { css, useTheme, styled } from '@apache-superset/core/theme';
@@ -140,6 +141,8 @@ export interface SliceHeaderControlsProps {
supersetCanDownload?: boolean;
crossFiltersEnabled?: boolean;
ownState?: JsonObject;
}
type SliceHeaderControlsPropsWithRouter = SliceHeaderControlsProps &
RouteComponentProps;
@@ -486,7 +489,12 @@ const SliceHeaderControls = (
<div data-test="view-query-menu-item">{t('View query')}</div>
}
modalTitle={t('View query')}
modalBody={<ViewQueryModal latestQueryFormData={props.formData} />}
modalBody={
<ViewQueryModal
latestQueryFormData={props.formData}
ownState={props.ownState}
/>
}
draggable
resizable
responsive

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { QueryFormData } from '@superset-ui/core';
import { JsonObject, QueryFormData } from '@superset-ui/core';
export interface SliceHeaderControlsProps {
slice: {
@@ -60,4 +60,5 @@ export interface SliceHeaderControlsProps {
supersetCanDownload?: boolean;
crossFiltersEnabled?: boolean;
ownState?: JsonObject;
}

View File

@@ -30,10 +30,12 @@ const DragHandleContainer = styled.div<{ position: 'left' | 'top' }>`
height: ${theme.sizeUnit * 5}px;
overflow: hidden;
cursor: move;
${position === 'top' &&
css`
transform: rotate(90deg);
`}
${
position === 'top' &&
css`
transform: rotate(90deg);
`
}
& path {
fill: ${theme.colorIcon};
}

View File

@@ -480,6 +480,26 @@ const Chart = (props: ChartProps) => {
(formData as JsonObject).dashboardId = dashboardInfo.id;
// Memoize ownState so it keeps a stable reference across re-renders that
// don't change its logical value. ViewQueryModal depends on ownState; a fresh
// object on every render would refetch the query unnecessarily.
const ownState = useMemo(
() =>
createOwnStateWithChartState(
(dataMaskOwnState as JsonObject) || EMPTY_OBJECT,
{
state:
getChartStateWithFallback(
chartState as { state?: JsonObject } | undefined,
formData as JsonObject,
sliceVizType,
) ?? undefined,
},
sliceVizType,
),
[dataMaskOwnState, chartState, formData, sliceVizType],
);
const exportTable = useCallback(
async (format: string, isFullCSV: boolean, isPivot = false) => {
const logAction =
@@ -517,8 +537,7 @@ const Chart = (props: ChartProps) => {
actualRowCount = (queriesResponse![0] as JsonObject).rowcount as number;
} else {
actualRowCount = (exportFormData as JsonObject)?.row_limit as
| number
| undefined;
number | undefined;
}
// Handle streaming CSV exports based on row threshold
@@ -727,6 +746,7 @@ const Chart = (props: ChartProps) => {
height={getHeaderHeight()}
exportPivotExcel={exportPivotExcel as unknown as (arg0: string) => void}
chartHolderRef={props.chartHolderRef}
ownState={ownState}
/>
{/*
@@ -777,18 +797,7 @@ const Chart = (props: ChartProps) => {
formData={
formData as unknown as import('@superset-ui/core').QueryFormData
}
ownState={createOwnStateWithChartState(
(dataMask[props.id]?.ownState as JsonObject) || EMPTY_OBJECT,
{
state:
getChartStateWithFallback(
chartState as { state?: JsonObject } | undefined,
formData as JsonObject,
slice.viz_type,
) ?? undefined,
},
slice.viz_type,
)}
ownState={ownState}
queriesResponse={chart.queriesResponse ?? null}
timeout={timeout}
triggerQuery={chart.triggerQuery}

View File

@@ -43,13 +43,10 @@ export type ComponentType =
(typeof componentTypes)[keyof typeof componentTypes];
export type HeaderStyleValue =
| typeof SMALL_HEADER
| typeof MEDIUM_HEADER
| typeof LARGE_HEADER;
typeof SMALL_HEADER | typeof MEDIUM_HEADER | typeof LARGE_HEADER;
export type BackgroundStyleValue =
| typeof BACKGROUND_TRANSPARENT
| typeof BACKGROUND_WHITE;
typeof BACKGROUND_TRANSPARENT | typeof BACKGROUND_WHITE;
export interface ComponentMeta {
width?: number;

View File

@@ -46,32 +46,36 @@ const ButtonsContainer = styled.div<{ isVertical: boolean }>`
${({ theme, isVertical }) => css`
display: flex;
${isVertical
? css`
flex-direction: column;
align-items: center;
position: sticky;
z-index: 100;
bottom: 0;
padding: ${theme.sizeUnit * 4}px;
padding-top: ${theme.sizeUnit * 6}px;
background: linear-gradient(
${tinycolor(theme.colorBgLayout).setAlpha(0).toRgbString()},
${theme.colorBgContainer} 20%
);
`
: css`
align-items: center;
margin-left: auto;
`}
${
isVertical
? css`
flex-direction: column;
align-items: center;
position: sticky;
z-index: 100;
bottom: 0;
padding: ${theme.sizeUnit * 4}px;
padding-top: ${theme.sizeUnit * 6}px;
background: linear-gradient(
${tinycolor(theme.colorBgLayout).setAlpha(0).toRgbString()},
${theme.colorBgContainer} 20%
);
`
: css`
align-items: center;
margin-left: auto;
`
}
`}
`;
const applyButtonStyle = (theme: SupersetTheme, isVertical: boolean) => css`
${isVertical &&
css`
margin-bottom: ${theme.sizeUnit * 3}px;
`}
${
isVertical &&
css`
margin-bottom: ${theme.sizeUnit * 3}px;
`
}
`;
const clearAllButtonStyle = (theme: SupersetTheme, isVertical: boolean) => css`
@@ -88,11 +92,13 @@ const clearAllButtonStyle = (theme: SupersetTheme, isVertical: boolean) => css`
color: ${theme.colorTextDisabled};
}
${!isVertical &&
css`
text-transform: capitalize;
font-weight: ${theme.fontWeightNormal};
`}
${
!isVertical &&
css`
text-transform: capitalize;
font-weight: ${theme.fontWeightNormal};
`
}
}
`;

View File

@@ -63,14 +63,16 @@ const CrossFilter = (props: {
<div
key={`${filter.name}${filter.emitterId}`}
css={css`
${orientation === FilterBarOrientation.Vertical
? `
${
orientation === FilterBarOrientation.Vertical
? `
display: block;
margin-bottom: ${theme.sizeUnit * 4}px;
`
: `
: `
display: flex;
`}
`
}
`}
>
<CrossFilterTitle
@@ -89,21 +91,23 @@ const CrossFilter = (props: {
<div
data-test="cross-filters-divider"
css={css`
${orientation === FilterBarOrientation.Horizontal
? `
${
orientation === FilterBarOrientation.Horizontal
? `
width: 1px;
height: 22px;
margin-left: ${theme.sizeUnit * 4}px;
margin-right: ${theme.sizeUnit}px;
flex-shrink: 0;
`
: `
: `
width: 100%;
height: 1px;
display: block;
margin-bottom: ${theme.sizeUnit * 4}px;
margin-top: ${theme.sizeUnit * 4}px;
`}
`
}
background: ${theme.colorSplit};
`}
/>

View File

@@ -67,13 +67,15 @@ const CrossFilterTag = (props: {
return (
<StyledTag
css={css`
${orientation === FilterBarOrientation.Vertical
? `
${
orientation === FilterBarOrientation.Vertical
? `
margin-top: ${theme.sizeUnit * 2}px;
`
: `
: `
margin-left: ${theme.sizeUnit * 2}px;
`}
`
}
`}
closable
onClose={() => removeCrossFilter(filter.emitterId)}

Some files were not shown because too many files have changed in this diff Show More