mirror of
https://github.com/apache/superset.git
synced 2026-07-07 07:15:37 +00:00
Compare commits
12 Commits
chart-samp
...
test/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ead906f93a | ||
|
|
9e50d9eca6 | ||
|
|
033b7bc385 | ||
|
|
c0b0a2fdaf | ||
|
|
44c1ad5c71 | ||
|
|
fb496e158a | ||
|
|
69ce27f173 | ||
|
|
51e708da6b | ||
|
|
21b4185819 | ||
|
|
689b4f623e | ||
|
|
ad8e6d4140 | ||
|
|
83d93b8b42 |
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -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:
|
||||
|
||||
@@ -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
|
||||
---
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
4
docs/src/types/yaml.d.ts
vendored
4
docs/src/types/yaml.d.ts
vendored
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"../superset-frontend/**/*",
|
||||
"src/webpack.extend.ts",
|
||||
"src/shims/**"
|
||||
]
|
||||
}
|
||||
|
||||
7598
docs/yarn.lock
7598
docs/yarn.lock
File diff suppressed because it is too large
Load Diff
1412
superset-embedded-sdk/package-lock.json
generated
1412
superset-embedded-sdk/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
2721
superset-frontend/package-lock.json
generated
2721
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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!",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 />);
|
||||
|
||||
|
||||
@@ -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]);
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)`
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
`;
|
||||
}}
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -53,9 +53,7 @@ export type SetAdhocFilter = BaseSimpleAdhocFilter & {
|
||||
};
|
||||
|
||||
export type SimpleAdhocFilter =
|
||||
| UnaryAdhocFilter
|
||||
| BinaryAdhocFilter
|
||||
| SetAdhocFilter;
|
||||
UnaryAdhocFilter | BinaryAdhocFilter | SetAdhocFilter;
|
||||
|
||||
export interface FreeFormAdhocFilter extends BaseAdhocFilter {
|
||||
expressionType: 'SQL';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
);
|
||||
|
||||
@@ -189,10 +189,7 @@ export interface InputColumn {
|
||||
isPercentMetric: boolean;
|
||||
config: TableColumnConfig;
|
||||
formatter?:
|
||||
| TimeFormatter
|
||||
| NumberFormatter
|
||||
| CustomFormatter
|
||||
| CurrencyFormatter;
|
||||
TimeFormatter | NumberFormatter | CustomFormatter | CurrencyFormatter;
|
||||
originalLabel?: string;
|
||||
metricName?: string;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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 },
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 });
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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 = {
|
||||
|
||||
@@ -221,8 +221,7 @@ const config: ControlPanelConfig = {
|
||||
|
||||
return {
|
||||
queryResponse: chart?.queriesResponse?.[0] as
|
||||
| ChartDataResponseResult
|
||||
| undefined,
|
||||
ChartDataResponseResult | undefined,
|
||||
appliedColumnNames: metricColumn,
|
||||
columnsPropsObject: { colnames, coltypes },
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 },
|
||||
) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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[][];
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -529,8 +529,7 @@ export const mapProvider = {
|
||||
options: getLabeledMapRendererOptions({
|
||||
hasMapboxKey: hasKey,
|
||||
currentValue: state.form_data?.map_renderer as
|
||||
| MapProvider
|
||||
| undefined,
|
||||
MapProvider | undefined,
|
||||
}),
|
||||
default: getDefaultMapRenderer(),
|
||||
};
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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'
|
||||
};
|
||||
`}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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'};
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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> = {};
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
`
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
|
||||
@@ -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};
|
||||
`}
|
||||
/>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user