mirror of
https://github.com/apache/superset.git
synced 2026-04-28 20:44:24 +00:00
Compare commits
2 Commits
fix/postgr
...
ts_migrate
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c92a78ab35 | ||
|
|
04ba0a00b3 |
@@ -35,7 +35,6 @@ import {
|
||||
SaveDatasetModal,
|
||||
ISaveableDatasource,
|
||||
} from 'src/SqlLab/components/SaveDatasetModal';
|
||||
import { getDatasourceAsSaveableDataset } from 'src/utils/datasourceUtils';
|
||||
import useQueryEditor from 'src/SqlLab/hooks/useQueryEditor';
|
||||
import { QueryEditor } from 'src/SqlLab/types';
|
||||
import useLogAction from 'src/logger/useLogAction';
|
||||
@@ -215,7 +214,15 @@ const SaveQuery = ({
|
||||
onHide={() => setShowSaveDatasetModal(false)}
|
||||
buttonTextOnSave={t('Save & Explore')}
|
||||
buttonTextOnOverwrite={t('Overwrite & Explore')}
|
||||
datasource={getDatasourceAsSaveableDataset(query)}
|
||||
datasource={{
|
||||
columns,
|
||||
name: query.name || query.description || t('Undefined'),
|
||||
dbId: query.dbId || 0,
|
||||
sql: query.sql || '',
|
||||
catalog: query.catalog,
|
||||
schema: query.schema,
|
||||
templateParams: query.templateParams,
|
||||
}}
|
||||
/>
|
||||
<Modal
|
||||
className="save-query-modal"
|
||||
|
||||
@@ -92,7 +92,7 @@ export function MarshmallowErrorMessage({
|
||||
children: (
|
||||
<JSONTree
|
||||
data={extra.messages}
|
||||
shouldExpandNodeInitially={() => true}
|
||||
shouldExpandNode={() => true}
|
||||
hideRoot
|
||||
theme={jsonTreeTheme}
|
||||
/>
|
||||
|
||||
@@ -67,7 +67,7 @@ export const CopyToClipboardButton = ({
|
||||
}) => (
|
||||
<CopyToClipboard
|
||||
text={
|
||||
data && columns ? prepareCopyToClipboardTabularData(data, columns) : ''
|
||||
data && columns ? prepareCopyToClipboardTabularData([data], columns) : ''
|
||||
}
|
||||
wrapped={false}
|
||||
copyNode={
|
||||
|
||||
@@ -62,7 +62,8 @@ export const TableControls = ({
|
||||
name &&
|
||||
!originalTimeColumns.includes(name),
|
||||
)
|
||||
.map(([colname]) => colname);
|
||||
.map(([colname]) => colname)
|
||||
.filter((colname): colname is string => colname !== undefined);
|
||||
const formattedData = useMemo(
|
||||
() => applyFormattingToTabularData(data, formattedTimeColumns),
|
||||
[data, formattedTimeColumns],
|
||||
|
||||
@@ -18,33 +18,49 @@
|
||||
*/
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
interface DebouncedMessageQueueConfig {
|
||||
callback?: (events: any[]) => void;
|
||||
sizeThreshold?: number;
|
||||
delayThreshold?: number;
|
||||
}
|
||||
|
||||
class DebouncedMessageQueue {
|
||||
private queue: any[];
|
||||
|
||||
private sizeThreshold: number;
|
||||
|
||||
private delayThreshold: number;
|
||||
|
||||
private callback: (events: any[]) => void;
|
||||
|
||||
private trigger: () => void;
|
||||
|
||||
constructor({
|
||||
callback = () => {},
|
||||
sizeThreshold = 1000,
|
||||
delayThreshold = 1000,
|
||||
}) {
|
||||
}: DebouncedMessageQueueConfig = {}) {
|
||||
this.queue = [];
|
||||
this.sizeThreshold = sizeThreshold;
|
||||
this.delayThreshold = delayThreshold;
|
||||
|
||||
this.trigger = debounce(this.trigger.bind(this), this.delayThreshold);
|
||||
this.trigger = debounce(this.triggerQueue.bind(this), this.delayThreshold);
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
append(eventData) {
|
||||
append(eventData: any): void {
|
||||
this.queue.push(eventData);
|
||||
this.trigger();
|
||||
}
|
||||
|
||||
trigger() {
|
||||
private triggerQueue(): void {
|
||||
if (this.queue.length > 0) {
|
||||
const events = this.queue.splice(0, this.sizeThreshold);
|
||||
this.callback.call(null, events);
|
||||
|
||||
// If there are remaining items, call it again.
|
||||
if (this.queue.length > 0) {
|
||||
this.trigger();
|
||||
this.triggerQueue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -50,8 +50,8 @@ describe('utils/common', () => {
|
||||
});
|
||||
describe('prepareCopyToClipboardTabularData', () => {
|
||||
it('converts empty array', () => {
|
||||
const data = [];
|
||||
const columns = [];
|
||||
const data: any[] = [];
|
||||
const columns: any[] = [];
|
||||
expect(prepareCopyToClipboardTabularData(data, columns)).toEqual('');
|
||||
});
|
||||
it('converts non empty array', () => {
|
||||
@@ -77,7 +77,7 @@ describe('utils/common', () => {
|
||||
});
|
||||
describe('applyFormattingToTabularData', () => {
|
||||
it('does not mutate empty array', () => {
|
||||
const data = [];
|
||||
const data: any[] = [];
|
||||
expect(applyFormattingToTabularData(data, [])).toEqual(data);
|
||||
});
|
||||
it('does not mutate array without temporal column', () => {
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
getTimeFormatter,
|
||||
TimeFormats,
|
||||
ensureIsArray,
|
||||
JsonObject,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
// ATTENTION: If you change any constants, make sure to also change constants.py
|
||||
@@ -36,7 +37,7 @@ export const SHORT_TIME = 'h:m a';
|
||||
|
||||
const DATETIME_FORMATTER = getTimeFormatter(TimeFormats.DATABASE_DATETIME);
|
||||
|
||||
export function storeQuery(query) {
|
||||
export function storeQuery(query: JsonObject): Promise<string> {
|
||||
return SupersetClient.post({
|
||||
endpoint: '/kv/store/',
|
||||
postPayload: { data: query },
|
||||
@@ -47,7 +48,7 @@ export function storeQuery(query) {
|
||||
});
|
||||
}
|
||||
|
||||
export function optionLabel(opt) {
|
||||
export function optionLabel(opt: any): string {
|
||||
if (opt === null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
@@ -66,28 +67,31 @@ export function optionLabel(opt) {
|
||||
return opt;
|
||||
}
|
||||
|
||||
export function optionValue(opt) {
|
||||
export function optionValue(opt: any): any {
|
||||
if (opt === null) {
|
||||
return NULL_STRING;
|
||||
}
|
||||
return opt;
|
||||
}
|
||||
|
||||
export function optionFromValue(opt) {
|
||||
export function optionFromValue(opt: any): { value: any; label: string } {
|
||||
// From a list of options, handles special values & labels
|
||||
return { value: optionValue(opt), label: optionLabel(opt) };
|
||||
}
|
||||
|
||||
function getColumnName(column) {
|
||||
function getColumnName(column: any): string {
|
||||
return column.name || column;
|
||||
}
|
||||
|
||||
export function prepareCopyToClipboardTabularData(data, columns) {
|
||||
export function prepareCopyToClipboardTabularData(
|
||||
data: JsonObject[],
|
||||
columns: any[],
|
||||
): string {
|
||||
let result = columns.length
|
||||
? `${columns.map(getColumnName).join('\t')}\n`
|
||||
: '';
|
||||
for (let i = 0; i < data.length; i += 1) {
|
||||
const row = {};
|
||||
const row: Record<number, any> = {};
|
||||
for (let j = 0; j < columns.length; j += 1) {
|
||||
// JavaScript does not maintain the order of a mixed set of keys (i.e integers and strings)
|
||||
// the below function orders the keys based on the column names.
|
||||
@@ -103,7 +107,10 @@ export function prepareCopyToClipboardTabularData(data, columns) {
|
||||
return result;
|
||||
}
|
||||
|
||||
export function applyFormattingToTabularData(data, timeFormattedColumns) {
|
||||
export function applyFormattingToTabularData(
|
||||
data: JsonObject[],
|
||||
timeFormattedColumns: string[],
|
||||
): JsonObject[] {
|
||||
if (
|
||||
!data ||
|
||||
data.length === 0 ||
|
||||
@@ -115,19 +122,22 @@ export function applyFormattingToTabularData(data, timeFormattedColumns) {
|
||||
return data.map(row => ({
|
||||
...row,
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
...timeFormattedColumns.reduce((acc, colName) => {
|
||||
if (row[colName] !== null && row[colName] !== undefined) {
|
||||
acc[colName] = DATETIME_FORMATTER(row[colName]);
|
||||
}
|
||||
return acc;
|
||||
}, {}),
|
||||
...timeFormattedColumns.reduce(
|
||||
(acc, colName) => {
|
||||
if (row[colName] !== null && row[colName] !== undefined) {
|
||||
acc[colName] = DATETIME_FORMATTER(row[colName]);
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
export const noOp = () => undefined;
|
||||
export const noOp = (): undefined => undefined;
|
||||
|
||||
// Detects the user's OS through the browser
|
||||
export const detectOS = () => {
|
||||
export const detectOS = (): string => {
|
||||
const { appVersion } = navigator;
|
||||
|
||||
// Leveraging this condition because of stackOverflow
|
||||
@@ -140,8 +150,8 @@ export const detectOS = () => {
|
||||
return 'Unknown OS';
|
||||
};
|
||||
|
||||
export const isSafari = () => {
|
||||
export const isSafari = (): boolean => {
|
||||
const { userAgent } = navigator;
|
||||
|
||||
return userAgent && /^((?!chrome|android).)*safari/i.test(userAgent);
|
||||
return Boolean(userAgent && /^((?!chrome|android).)*safari/i.test(userAgent));
|
||||
};
|
||||
@@ -16,12 +16,22 @@
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
export const getDatasourceAsSaveableDataset = source => ({
|
||||
columns: source.columns,
|
||||
import { Dataset } from '@superset-ui/chart-controls';
|
||||
import { ISaveableDatasource } from '../SqlLab/components/SaveDatasetModal';
|
||||
|
||||
export const getDatasourceAsSaveableDataset = (
|
||||
source: Partial<Dataset> | any,
|
||||
): ISaveableDatasource => ({
|
||||
columns: (source.columns || []).map((col: any) => ({
|
||||
column_name: col.column_name || col.name || '',
|
||||
name: col.name || col.column_name || '',
|
||||
type: col.type || null,
|
||||
is_dttm: col.is_dttm || null,
|
||||
})),
|
||||
name: source?.datasource_name || source?.name || 'Untitled',
|
||||
dbId: source?.database?.id || source?.dbId,
|
||||
sql: source?.sql || '',
|
||||
dbId: source?.database?.id || (source as any)?.dbId || 0,
|
||||
sql: (source as any)?.sql || '',
|
||||
catalog: source?.catalog,
|
||||
schema: source?.schema,
|
||||
templateParams: source?.templateParams,
|
||||
templateParams: (source as any)?.templateParams,
|
||||
});
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import memoizeOne from 'memoize-one';
|
||||
import { isControlPanelSectionConfig } from '@superset-ui/chart-controls';
|
||||
import { getChartControlPanelRegistry } from '@superset-ui/core';
|
||||
import { controls } from '../explore/controls';
|
||||
|
||||
const memoizedControls = memoizeOne((vizType, controlPanel) => {
|
||||
const controlsMap = {};
|
||||
(controlPanel?.controlPanelSections || [])
|
||||
.filter(isControlPanelSectionConfig)
|
||||
.forEach(section => {
|
||||
section.controlSetRows.forEach(row => {
|
||||
row.forEach(control => {
|
||||
if (!control) return;
|
||||
if (typeof control === 'string') {
|
||||
// For now, we have to look in controls.jsx to get the config for some controls.
|
||||
// Once everything is migrated out, delete this if statement.
|
||||
controlsMap[control] = controls[control];
|
||||
} else if (control.name && control.config) {
|
||||
// condition needed because there are elements, e.g. <hr /> in some control configs (I'm looking at you, FilterBox!)
|
||||
controlsMap[control.name] = control.config;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return controlsMap;
|
||||
});
|
||||
|
||||
const getControlsForVizType = vizType => {
|
||||
const controlPanel = getChartControlPanelRegistry().get(vizType);
|
||||
return memoizedControls(vizType, controlPanel);
|
||||
};
|
||||
|
||||
export default getControlsForVizType;
|
||||
66
superset-frontend/src/utils/getControlsForVizType.ts
Normal file
66
superset-frontend/src/utils/getControlsForVizType.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* 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 memoizeOne from 'memoize-one';
|
||||
import {
|
||||
isControlPanelSectionConfig,
|
||||
ControlStateMapping,
|
||||
ControlPanelConfig,
|
||||
} from '@superset-ui/chart-controls';
|
||||
import { getChartControlPanelRegistry } from '@superset-ui/core';
|
||||
import { controls } from '../explore/controls';
|
||||
|
||||
const memoizedControls = memoizeOne(
|
||||
(vizType: string, controlPanel: ControlPanelConfig): ControlStateMapping => {
|
||||
const controlsMap: ControlStateMapping = {};
|
||||
(controlPanel?.controlPanelSections || [])
|
||||
.filter(isControlPanelSectionConfig)
|
||||
.forEach(section => {
|
||||
section.controlSetRows.forEach(row => {
|
||||
row.forEach(control => {
|
||||
if (!control) return;
|
||||
if (typeof control === 'string') {
|
||||
// For now, we have to look in controls.jsx to get the config for some controls.
|
||||
// Once everything is migrated out, delete this if statement.
|
||||
controlsMap[control] = (controls as any)[control];
|
||||
} else if (
|
||||
control &&
|
||||
typeof control === 'object' &&
|
||||
'name' in control &&
|
||||
'config' in control
|
||||
) {
|
||||
// condition needed because there are elements, e.g. <hr /> in some control configs (I'm looking at you, FilterBox!)
|
||||
controlsMap[control.name] = control.config;
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
return controlsMap;
|
||||
},
|
||||
);
|
||||
|
||||
const getControlsForVizType = (vizType: string): ControlStateMapping => {
|
||||
const controlPanel = getChartControlPanelRegistry().get(vizType);
|
||||
return memoizedControls(
|
||||
vizType,
|
||||
(controlPanel as ControlPanelConfig) || { controlPanelSections: [] },
|
||||
);
|
||||
};
|
||||
|
||||
export default getControlsForVizType;
|
||||
@@ -19,7 +19,7 @@
|
||||
import { initFeatureFlags } from '@superset-ui/core';
|
||||
import getBootstrapData from './getBootstrapData';
|
||||
|
||||
function getDomainsConfig() {
|
||||
function getDomainsConfig(): string[] {
|
||||
const appContainer = document.getElementById('app');
|
||||
if (!appContainer) {
|
||||
return [];
|
||||
@@ -42,13 +42,15 @@ function getDomainsConfig() {
|
||||
initFeatureFlags(bootstrapData.common.feature_flags);
|
||||
|
||||
if (bootstrapData?.common?.conf?.SUPERSET_WEBSERVER_DOMAINS) {
|
||||
bootstrapData.common.conf.SUPERSET_WEBSERVER_DOMAINS.forEach(hostName => {
|
||||
availableDomains.add(hostName);
|
||||
});
|
||||
bootstrapData.common.conf.SUPERSET_WEBSERVER_DOMAINS.forEach(
|
||||
(hostName: string) => {
|
||||
availableDomains.add(hostName);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Array.from(availableDomains);
|
||||
}
|
||||
|
||||
export const availableDomains = getDomainsConfig();
|
||||
export const availableDomains: string[] = getDomainsConfig();
|
||||
|
||||
export const allowCrossDomain = availableDomains.length > 1;
|
||||
export const allowCrossDomain: boolean = availableDomains.length > 1;
|
||||
@@ -18,7 +18,16 @@
|
||||
*/
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export function addToObject(state, arrKey, obj) {
|
||||
interface StateWithId {
|
||||
id?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function addToObject<T extends Record<string, any>>(
|
||||
state: T,
|
||||
arrKey: keyof T,
|
||||
obj: StateWithId,
|
||||
): T {
|
||||
const newObject = { ...state[arrKey] };
|
||||
const copiedObject = { ...obj };
|
||||
|
||||
@@ -29,18 +38,28 @@ export function addToObject(state, arrKey, obj) {
|
||||
return { ...state, [arrKey]: newObject };
|
||||
}
|
||||
|
||||
export function alterInObject(state, arrKey, obj, alterations) {
|
||||
export function alterInObject<T extends Record<string, any>>(
|
||||
state: T,
|
||||
arrKey: keyof T,
|
||||
obj: StateWithId,
|
||||
alterations: Record<string, any>,
|
||||
): T {
|
||||
const newObject = { ...state[arrKey] };
|
||||
newObject[obj.id] = { ...newObject[obj.id], ...alterations };
|
||||
newObject[obj.id!] = { ...newObject[obj.id!], ...alterations };
|
||||
return { ...state, [arrKey]: newObject };
|
||||
}
|
||||
|
||||
export function alterInArr(state, arrKey, obj, alterations) {
|
||||
export function alterInArr<T extends Record<string, any>>(
|
||||
state: T,
|
||||
arrKey: keyof T,
|
||||
obj: StateWithId,
|
||||
alterations: Record<string, any>,
|
||||
): T {
|
||||
// Finds an item in an array in the state and replaces it with a
|
||||
// new object with an altered property
|
||||
const idKey = 'id';
|
||||
const newArr = [];
|
||||
state[arrKey].forEach(arrItem => {
|
||||
const newArr: any[] = [];
|
||||
state[arrKey].forEach((arrItem: any) => {
|
||||
if (obj[idKey] === arrItem[idKey]) {
|
||||
newArr.push({ ...arrItem, ...alterations });
|
||||
} else {
|
||||
@@ -50,9 +69,14 @@ export function alterInArr(state, arrKey, obj, alterations) {
|
||||
return { ...state, [arrKey]: newArr };
|
||||
}
|
||||
|
||||
export function removeFromArr(state, arrKey, obj, idKey = 'id') {
|
||||
const newArr = [];
|
||||
state[arrKey].forEach(arrItem => {
|
||||
export function removeFromArr<T extends Record<string, any>>(
|
||||
state: T,
|
||||
arrKey: keyof T,
|
||||
obj: StateWithId,
|
||||
idKey: string = 'id',
|
||||
): T {
|
||||
const newArr: any[] = [];
|
||||
state[arrKey].forEach((arrItem: any) => {
|
||||
if (!(obj[idKey] === arrItem[idKey])) {
|
||||
newArr.push(arrItem);
|
||||
}
|
||||
@@ -60,12 +84,16 @@ export function removeFromArr(state, arrKey, obj, idKey = 'id') {
|
||||
return { ...state, [arrKey]: newArr };
|
||||
}
|
||||
|
||||
export function addToArr(state, arrKey, obj) {
|
||||
export function addToArr<T extends Record<string, any>>(
|
||||
state: T,
|
||||
arrKey: keyof T,
|
||||
obj: StateWithId,
|
||||
): T {
|
||||
const newObj = { ...obj };
|
||||
if (!newObj.id) {
|
||||
newObj.id = nanoid();
|
||||
}
|
||||
const newState = {};
|
||||
newState[arrKey] = [...state[arrKey], newObj];
|
||||
const newState: Record<string, any> = {};
|
||||
newState[arrKey as string] = [...state[arrKey], newObj];
|
||||
return { ...state, ...newState };
|
||||
}
|
||||
Reference in New Issue
Block a user