Compare commits

..

17 Commits

Author SHA1 Message Date
Enzo Martellucci
bec3d94a5b Merge branch master into enxdev/refactor/typescript-migration-ChartRenderer 2025-03-15 17:07:39 +01:00
Đỗ Trọng Hải
a49a15f990 chore(docs): remove customized "Edit this page on GitHub" button (#32407)
Signed-off-by: hainenber <dotronghai96@gmail.com>
2025-03-15 21:27:10 +07:00
Evan Rusackas
eb39ddbfe3 feat(docs): Adding Kapa.ai integration (#32682) 2025-03-15 12:34:42 +07:00
dependabot[bot]
974d36d35e chore(deps): bump jinja2 from 3.1.5 to 3.1.6 in /superset/translations (#32580)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-15 12:17:18 +07:00
Beto Dealmeida
b64e3254fc feat: DB migration for dataset folders (#32680) 2025-03-14 17:16:02 -04:00
Maxime Beauchemin
9907db9e1a feat: add a note to install cors-related dependency when using ENABLE_CORS (#32662) 2025-03-14 10:33:31 -07:00
Michael S. Molina
b4dd64aa24 fix: Update RELEASING/README.md (#32678) 2025-03-14 10:28:18 -07:00
Pedro Martin-Steenstrup
6e049225f9 docs: add Hometogo to users list (#32668) 2025-03-14 10:27:28 -07:00
Beto Dealmeida
831369a44b fix(gsheets): update params from encrypted extra (#32661) 2025-03-14 12:00:53 -04:00
Evan Rusackas
7c9c30db1d chore(examples): Touching up Vehicle Sales a bit (#32623) 2025-03-14 09:31:02 -06:00
Vitor Avila
0c6d868483 fix(import): Import a DB connection with expanded rows enabled (#32657) 2025-03-14 12:02:39 -03:00
Andrey Yakir
777760b096 fix(dashboard): Ensure dashboardId is included in form_data for embedded mode (#32646) 2025-03-14 10:36:42 -04:00
Vitor Avila
e8ad096173 fix(sync perms): Avoid UnboundLocalError during perm sync for DBs that don't support catalogs (#32658) 2025-03-13 21:07:49 -03:00
Dolph Mathews
2f6f5c6778 fix: Upgrade node base image to Debian 12 bookworm (#32652) 2025-03-13 12:56:24 -07:00
Enzo Martellucci
c805c96f5a Merge branch 'master' into enxdev/refactor/typescript-migration-ChartRenderer 2025-02-25 09:29:36 +01:00
Enzo Martellucci
a3ec4080e6 wip(ChartRenderer) 2025-02-10 11:01:19 +01:00
Enzo Martellucci
3f6e511048 wip(ChartRenderer): migrates ChartRenderer to Ts 2025-02-06 14:41:31 +01:00
67 changed files with 2283 additions and 2417 deletions

View File

@@ -29,7 +29,7 @@ ARG BUILD_TRANSLATIONS="false"
######################################################################
# superset-node-ci used as a base for building frontend assets and CI
######################################################################
FROM --platform=${BUILDPLATFORM} node:20-bullseye-slim AS superset-node-ci
FROM --platform=${BUILDPLATFORM} node:20-bookworm-slim AS superset-node-ci
ARG BUILD_TRANSLATIONS
ENV BUILD_TRANSLATIONS=${BUILD_TRANSLATIONS}
ARG DEV_MODE="false" # Skip frontend build in dev mode

View File

@@ -452,7 +452,7 @@ cd ../
# Compile translations for the backend
./scripts/translations/generate_po_files.sh
./scripts/translations/generate_mo_files.sh
# update build version number
sed -i '' "s/version_string = .*/version_string = \"$SUPERSET_VERSION\"/" setup.py

View File

@@ -199,6 +199,7 @@ Join our growing community!
### Travel
- [Agoda](https://www.agoda.com/) [@lostseaway, @maiake, @obombayo]
- [HomeToGo](https://hometogo.com/) [@pedromartinsteenstrup]
- [Skyscanner](https://www.skyscanner.net/) [@cleslie, @stanhoucke]
### Others

View File

@@ -333,6 +333,31 @@ const config: Config = {
// src: 'https://www.bugherd.com/sidebarv2.js?apikey=enilpiu7bgexxsnoqfjtxa',
// async: true,
// },
'/script/matomo.js',
{
src: 'https://widget.kapa.ai/kapa-widget.bundle.js',
async: true,
'data-website-id': 'c6a8a8b8-3127-48f9-97a7-51e9e10d20d0',
'data-project-name': 'Apache Superset',
'data-project-color': '#1AA1C2',
'data-project-logo':
'https://images.seeklogo.com/logo-png/50/2/superset-icon-logo-png_seeklogo-500354.png',
'data-modal-override-open-id': 'ask-ai-input',
'data-modal-override-open-class': 'search-input',
'data-modal-open-by-default': 'true',
'data-modal-disclaimer':
'This is a custom LLM for Apache Superset with access to all [documentation](superset.apache.org/docs/intro/), [GitHub Open Issues, PRs and READMEs](github.com/apache/superset).&#10;&#10;Companies deploy assistants like this ([built by kapa.ai](https://kapa.ai)) on docs via [website widget](https://docs.kapa.ai/integrations/website-widget) (Docker, Reddit), in [support forms](https://docs.kapa.ai/integrations/support-form-deflector) for ticket deflection (Monday.com, Mapbox), or as [Slack bots](https://docs.kapa.ai/integrations/slack-bot) with private sources.',
'data-modal-example-questions':
'How do I use Docker Compose?,How to run Supersets on kubernetes?',
'data-button-text-color': '#FFFFFF',
'data-modal-header-bg-color': '#1AA1C2',
'data-modal-title-color': '#FFFFFF',
'data-modal-title': 'Superset Ask AI',
'data-modal-disclaimer-text-color': '#000000',
'data-consent-required': 'true',
'data-consent-screen-disclaimer':
"By clicking \"I agree, let's chat\", you consent to the use of the AI assistant in accordance with kapa.ai's [Privacy Policy](https://www.kapa.ai/content/privacy-policy). This service uses reCAPTCHA, which requires your consent to Google's [Privacy Policy](https://policies.google.com/privacy) and [Terms of Service](https://policies.google.com/terms). By proceeding, you explicitly agree to both kapa.ai's and Google's privacy policies.",
},
],
customFields: {
matomoUrl: 'https://analytics.apache.org',

View File

@@ -256,13 +256,3 @@ a > span > svg {
height: 28px;
}
}
/* Edit Button */
.edit-page-link {
position: sticky;
bottom: 0px;
right: 0px;
border-radius: 10px;
background-color: #ccc;
}

View File

@@ -1,61 +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 styled from '@emotion/styled';
import DocItem from '@theme-original/DocItem';
const EditPageLink = styled('a')`
position: fixed;
bottom: 40px;
right: 10px;
padding: 1rem;
padding-left: 4rem;
background-color: #444;
border-radius: 10px;
z-index: 9999;
background-image: url('/img/github-dark.png');
background-size: 2rem;
background-position: 1rem center;
background-repeat: no-repeat;
transition: background-color 0.3s; /* Smooth transition for hover effect */
bpx-shadow: 0 0 0 0 rgba(0, 0, 0, 0); /* Smooth transition for hover effect */
scale: 0.9;
transition: all 0.3s;
transform-origin: bottom right;
&:hover {
background-color: #333;
box-shadow: 5px 5px 10px 0 rgba(0, 0, 0, 0.3);
scale: 1;
}
`;
export default function DocItemWrapper(props) {
return (
<>
<EditPageLink
href={props.content.metadata.editUrl}
target="_blank"
rel="noopener noreferrer"
>
Edit this page on GitHub
</EditPageLink>
<DocItem {...props} />
</>
);
}

View File

@@ -48823,7 +48823,6 @@
"@types/react": "*",
"@types/react-loadable": "*",
"@types/tinycolor2": "*",
"nanoid": "^5.0.9",
"react": "^17.0.2",
"react-loadable": "^5.5.0",
"tinycolor2": "*"

View File

@@ -42,7 +42,7 @@ const FlexRowContainer = styled.div`
`;
export interface MetricOptionProps {
metric: Omit<Metric, 'id' | 'uuid'> & { label?: string };
metric: Omit<Metric, 'id'> & { label?: string };
openInNewWindow?: boolean;
showFormula?: boolean;
showType?: boolean;

View File

@@ -97,7 +97,7 @@ export const getColumnTooltipNode = (
);
};
type MetricType = Omit<Metric, 'id' | 'uuid'> & { label?: string };
type MetricType = Omit<Metric, 'id'> & { label?: string };
export const getMetricTooltipNode = (
metric: MetricType,

View File

@@ -121,7 +121,6 @@ export const TestDataset: Dataset = {
main_dttm_col: 'ds',
metrics: [
{
uuid: '123',
certification_details: null,
certified_by: null,
d3format: null,

View File

@@ -32,7 +32,6 @@ describe('defineSavedMetrics', () => {
{
metric_name: 'COUNT(*) non-default-dataset-metric',
expression: 'COUNT(*) non-default-dataset-metric',
uuid: '1',
},
],
type: DatasourceType.Table,
@@ -49,7 +48,6 @@ describe('defineSavedMetrics', () => {
{
metric_name: 'COUNT(*) non-default-dataset-metric',
expression: 'COUNT(*) non-default-dataset-metric',
uuid: '1',
},
]);
// @ts-ignore

View File

@@ -24,24 +24,15 @@ describe('mainMetric', () => {
expect(mainMetric(null)).toBeUndefined();
});
it('prefers the "count" metric when first', () => {
const metrics = [
{ metric_name: 'count', uuid: '1' },
{ metric_name: 'foo', uuid: '2' },
];
const metrics = [{ metric_name: 'count' }, { metric_name: 'foo' }];
expect(mainMetric(metrics)).toBe('count');
});
it('prefers the "count" metric when not first', () => {
const metrics = [
{ metric_name: 'foo', uuid: '1' },
{ metric_name: 'count', uuid: '2' },
];
const metrics = [{ metric_name: 'foo' }, { metric_name: 'count' }];
expect(mainMetric(metrics)).toBe('count');
});
it('selects the first metric when "count" is not an option', () => {
const metrics = [
{ metric_name: 'foo', uuid: '2' },
{ metric_name: 'not_count', uuid: '2' },
];
const metrics = [{ metric_name: 'foo' }, { metric_name: 'not_count' }];
expect(mainMetric(metrics)).toBe('foo');
});
});

View File

@@ -81,7 +81,6 @@
"@types/react": "*",
"@types/react-loadable": "*",
"@types/tinycolor2": "*",
"nanoid": "^5.0.9",
"react": "^17.0.2",
"react-loadable": "^5.5.0",
"tinycolor2": "*"

View File

@@ -16,7 +16,6 @@
* specific language governing permissions and limitations
* under the License.
*/
import { nanoid } from 'nanoid';
import { Column } from './Column';
import { Metric } from './Metric';
@@ -59,7 +58,6 @@ export const DEFAULT_METRICS: Metric[] = [
{
metric_name: 'COUNT(*)',
expression: 'COUNT(*)',
uuid: nanoid(),
},
];

View File

@@ -60,7 +60,6 @@ export type SavedMetric = string;
*/
export interface Metric {
id?: number;
uuid: string;
metric_name: string;
expression?: Maybe<string>;
certification_details?: Maybe<string>;

View File

@@ -20,10 +20,10 @@ import { DatasourceType, DEFAULT_METRICS } from '@superset-ui/core';
test('DEFAULT_METRICS', () => {
expect(DEFAULT_METRICS).toEqual([
expect.objectContaining({
{
metric_name: 'COUNT(*)',
expression: 'COUNT(*)',
}),
},
]);
});

View File

@@ -149,7 +149,6 @@ describe('BigNumberWithTrendline', () => {
label: 'value',
metric_name: 'value',
d3format: '.2f',
uuid: '1',
},
],
},
@@ -175,7 +174,6 @@ describe('BigNumberWithTrendline', () => {
metric_name: 'value',
d3format: '.2f',
currency: { symbol: 'USD', symbolPosition: 'prefix' },
uuid: '1',
},
],
},

View File

@@ -17,15 +17,16 @@
* under the License.
*/
import { PureComponent } from 'react';
import { Dispatch } from 'redux';
import {
ensureIsArray,
FeatureFlag,
isFeatureEnabled,
logging,
SqlaFormData,
QueryFormData,
styled,
t,
SqlaFormData,
ClientErrorObject,
ChartDataResponse,
} from '@superset-ui/core';
@@ -39,53 +40,11 @@ import { getUrlParam } from 'src/utils/urlUtils';
import { isCurrentUserBot } from 'src/utils/isBot';
import { ChartSource } from 'src/types/ChartSource';
import { ResourceStatus } from 'src/hooks/apiResources/apiResources';
import { Dispatch } from 'redux';
import { Annotation } from 'src/explore/components/controls/AnnotationLayerControl';
import ChartRenderer from './ChartRenderer';
import { ChartErrorMessage } from './ChartErrorMessage';
import { getChartRequiredFieldsMissingMessage } from '../../utils/getChartRequiredFieldsMissingMessage';
export type ChartErrorType = Partial<ClientErrorObject>;
export interface ChartProps {
annotationData?: Annotation;
actions: Actions;
chartId: string;
datasource?: {
database?: {
name: string;
};
};
dashboardId?: number;
initialValues?: object;
formData: QueryFormData;
labelColors?: string;
sharedLabelColors?: string;
width: number;
height: number;
setControlValue: Function;
timeout?: number;
vizType: string;
triggerRender?: boolean;
force?: boolean;
isFiltersInitialized?: boolean;
chartAlert?: string;
chartStatus?: string;
chartStackTrace?: string;
queriesResponse: ChartDataResponse[];
triggerQuery?: boolean;
chartIsStale?: boolean;
errorMessage?: React.ReactNode;
addFilter?: (type: string) => void;
onQuery?: () => void;
onFilterMenuOpen?: (chartId: string, column: string) => void;
onFilterMenuClose?: (chartId: string, column: string) => void;
ownState: boolean;
postTransformProps?: Function;
datasetsStatus?: 'loading' | 'error' | 'complete';
isInView?: boolean;
emitCrossFilters?: boolean;
}
export type Actions = {
logEvent(
LOG_ACTIONS_RENDER_CHART: string,
@@ -111,7 +70,51 @@ export type Actions = {
dashboardId: number | undefined,
ownState: boolean,
): Dispatch;
chartRenderingSucceeded(arg0: { key: string }): Dispatch;
updateDataMask(chartId: string, dataMask: { dataMask: any }): Dispatch;
};
export type ChartErrorType = Partial<ClientErrorObject>;
export interface ChartProps {
annotationData?: Annotation;
actions: Actions;
chartId: string;
datasource?: {
database?: {
name: string;
};
};
dashboardId?: number;
initialValues?: object;
formData: QueryFormData;
labelColors?: string;
sharedLabelColors?: string;
width: number;
height: number;
setControlValue: Function;
timeout?: number;
vizType: string;
triggerRender?: boolean;
force?: boolean;
chartAlert?: string;
chartStatus?: string;
chartStackTrace?: string;
queriesResponse: ChartDataResponse[];
triggerQuery?: boolean;
chartIsStale?: boolean;
addFilter?: (type: string) => void;
onQuery?: () => void;
onFilterMenuOpen?: (chartId: string, column: string) => void;
onFilterMenuClose?: (chartId: string, column: string) => void;
ownState: boolean;
postTransformProps?: Function;
datasetsStatus?: 'loading' | 'error' | 'complete';
emitCrossFilters?: boolean;
errorMessage?: React.ReactNode;
isInView?: boolean;
filters?: string | string[];
}
const BLANK = {};
const NONEXISTENT_DATASET = t(
'The dataset associated with this chart no longer exists',
@@ -172,6 +175,12 @@ const MessageSpan = styled.span`
color: ${({ theme }) => theme.colors.grayscale.base};
`;
const MonospaceDiv = styled.div`
font-family: ${({ theme }) => theme.typography.families.monospace};
word-break: break-word;
overflow-x: auto;
white-space: pre-wrap;
`;
class Chart extends PureComponent<ChartProps, {}> {
static defaultProps = defaultProps;
@@ -267,7 +276,8 @@ class Chart extends PureComponent<ChartProps, {}> {
key={chartId}
chartId={chartId}
error={error}
subtitle={message}
subtitle={<MonospaceDiv>{message}</MonospaceDiv>}
copyText={message}
link={queryResponse ? queryResponse.link : undefined}
source={dashboardId ? ChartSource.Dashboard : ChartSource.Explore}
stackTrace={chartStackTrace}
@@ -296,7 +306,11 @@ class Chart extends PureComponent<ChartProps, {}> {
isCurrentUserBot() ? (
<ChartRenderer
{...this.props}
source={this.props.dashboardId ? 'dashboard' : 'explore'}
source={
this.props.dashboardId
? ChartSource.Dashboard
: ChartSource.Explore
}
data-test={this.props.vizType}
/>
) : (
@@ -331,7 +345,6 @@ class Chart extends PureComponent<ChartProps, {}> {
if (errorMessage && ensureIsArray(queriesResponse).length === 0) {
return (
<EmptyState
size="large"
title={t('Add required control values to preview chart')}
description={getChartRequiredFieldsMissingMessage(true)}
image="chart.svg"
@@ -347,7 +360,6 @@ class Chart extends PureComponent<ChartProps, {}> {
) {
return (
<EmptyState
size="large"
title={t('Your chart is ready to go!')}
description={
<span>

View File

@@ -1,381 +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 { snakeCase, isEqual, cloneDeep } from 'lodash';
import PropTypes from 'prop-types';
import { createRef, Component } from 'react';
import {
SuperChart,
logging,
Behavior,
t,
getChartMetadataRegistry,
VizType,
isFeatureEnabled,
FeatureFlag,
} from '@superset-ui/core';
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
import { EmptyState } from 'src/components/EmptyState';
import { ChartSource } from 'src/types/ChartSource';
import ChartContextMenu from './ChartContextMenu/ChartContextMenu';
const propTypes = {
annotationData: PropTypes.object,
actions: PropTypes.object,
chartId: PropTypes.number.isRequired,
datasource: PropTypes.object,
initialValues: PropTypes.object,
formData: PropTypes.object.isRequired,
latestQueryFormData: PropTypes.object,
labelsColor: PropTypes.object,
labelsColorMap: PropTypes.object,
height: PropTypes.number,
width: PropTypes.number,
setControlValue: PropTypes.func,
vizType: PropTypes.string.isRequired,
triggerRender: PropTypes.bool,
// state
chartAlert: PropTypes.string,
chartStatus: PropTypes.string,
queriesResponse: PropTypes.arrayOf(PropTypes.object),
triggerQuery: PropTypes.bool,
chartIsStale: PropTypes.bool,
// dashboard callbacks
addFilter: PropTypes.func,
setDataMask: PropTypes.func,
onFilterMenuOpen: PropTypes.func,
onFilterMenuClose: PropTypes.func,
ownState: PropTypes.object,
postTransformProps: PropTypes.func,
source: PropTypes.oneOf([ChartSource.Dashboard, ChartSource.Explore]),
emitCrossFilters: PropTypes.bool,
};
const BLANK = {};
const BIG_NO_RESULT_MIN_WIDTH = 300;
const BIG_NO_RESULT_MIN_HEIGHT = 220;
const behaviors = [Behavior.InteractiveChart];
const defaultProps = {
addFilter: () => BLANK,
onFilterMenuOpen: () => BLANK,
onFilterMenuClose: () => BLANK,
initialValues: BLANK,
setControlValue() {},
triggerRender: false,
};
class ChartRenderer extends Component {
constructor(props) {
super(props);
const suppressContextMenu = getChartMetadataRegistry().get(
props.formData.viz_type ?? props.vizType,
)?.suppressContextMenu;
this.state = {
showContextMenu:
props.source === ChartSource.Dashboard &&
!suppressContextMenu &&
isFeatureEnabled(FeatureFlag.DrillToDetail),
inContextMenu: false,
legendState: undefined,
};
this.hasQueryResponseChange = false;
this.contextMenuRef = createRef();
this.handleAddFilter = this.handleAddFilter.bind(this);
this.handleRenderSuccess = this.handleRenderSuccess.bind(this);
this.handleRenderFailure = this.handleRenderFailure.bind(this);
this.handleSetControlValue = this.handleSetControlValue.bind(this);
this.handleOnContextMenu = this.handleOnContextMenu.bind(this);
this.handleContextMenuSelected = this.handleContextMenuSelected.bind(this);
this.handleContextMenuClosed = this.handleContextMenuClosed.bind(this);
this.handleLegendStateChanged = this.handleLegendStateChanged.bind(this);
this.onContextMenuFallback = this.onContextMenuFallback.bind(this);
this.hooks = {
onAddFilter: this.handleAddFilter,
onContextMenu: this.state.showContextMenu
? this.handleOnContextMenu
: undefined,
onError: this.handleRenderFailure,
setControlValue: this.handleSetControlValue,
onFilterMenuOpen: this.props.onFilterMenuOpen,
onFilterMenuClose: this.props.onFilterMenuClose,
onLegendStateChanged: this.handleLegendStateChanged,
setDataMask: dataMask => {
this.props.actions?.updateDataMask(this.props.chartId, dataMask);
},
};
// TODO: queriesResponse comes from Redux store but it's being edited by
// the plugins, hence we need to clone it to avoid state mutation
// until we change the reducers to use Redux Toolkit with Immer
this.mutableQueriesResponse = cloneDeep(this.props.queriesResponse);
}
shouldComponentUpdate(nextProps, nextState) {
const resultsReady =
nextProps.queriesResponse &&
['success', 'rendered'].indexOf(nextProps.chartStatus) > -1 &&
!nextProps.queriesResponse?.[0]?.error;
if (resultsReady) {
if (!isEqual(this.state, nextState)) {
return true;
}
this.hasQueryResponseChange =
nextProps.queriesResponse !== this.props.queriesResponse;
if (this.hasQueryResponseChange) {
this.mutableQueriesResponse = cloneDeep(nextProps.queriesResponse);
}
return (
this.hasQueryResponseChange ||
!isEqual(nextProps.datasource, this.props.datasource) ||
nextProps.annotationData !== this.props.annotationData ||
nextProps.ownState !== this.props.ownState ||
nextProps.filterState !== this.props.filterState ||
nextProps.height !== this.props.height ||
nextProps.width !== this.props.width ||
nextProps.triggerRender ||
nextProps.labelsColor !== this.props.labelsColor ||
nextProps.labelsColorMap !== this.props.labelsColorMap ||
nextProps.formData.color_scheme !== this.props.formData.color_scheme ||
nextProps.formData.stack !== this.props.formData.stack ||
nextProps.cacheBusterProp !== this.props.cacheBusterProp ||
nextProps.emitCrossFilters !== this.props.emitCrossFilters
);
}
return false;
}
handleAddFilter(col, vals, merge = true, refresh = true) {
this.props.addFilter(col, vals, merge, refresh);
}
handleRenderSuccess() {
const { actions, chartStatus, chartId, vizType } = this.props;
if (['loading', 'rendered'].indexOf(chartStatus) < 0) {
actions.chartRenderingSucceeded(chartId);
}
// only log chart render time which is triggered by query results change
// currently we don't log chart re-render time, like window resize etc
if (this.hasQueryResponseChange) {
actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
slice_id: chartId,
viz_type: vizType,
start_offset: this.renderStartTime,
ts: new Date().getTime(),
duration: Logger.getTimestamp() - this.renderStartTime,
});
}
}
handleRenderFailure(error, info) {
const { actions, chartId } = this.props;
logging.warn(error);
actions.chartRenderingFailed(
error.toString(),
chartId,
info ? info.componentStack : null,
);
// only trigger render log when query is changed
if (this.hasQueryResponseChange) {
actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
slice_id: chartId,
has_err: true,
error_details: error.toString(),
start_offset: this.renderStartTime,
ts: new Date().getTime(),
duration: Logger.getTimestamp() - this.renderStartTime,
});
}
}
handleSetControlValue(...args) {
const { setControlValue } = this.props;
if (setControlValue) {
setControlValue(...args);
}
}
handleOnContextMenu(offsetX, offsetY, filters) {
this.contextMenuRef.current.open(offsetX, offsetY, filters);
this.setState({ inContextMenu: true });
}
handleContextMenuSelected() {
this.setState({ inContextMenu: false });
}
handleContextMenuClosed() {
this.setState({ inContextMenu: false });
}
handleLegendStateChanged(legendState) {
this.setState({ legendState });
}
// When viz plugins don't handle `contextmenu` event, fallback handler
// calls `handleOnContextMenu` with no `filters` param.
onContextMenuFallback(event) {
if (!this.state.inContextMenu) {
event.preventDefault();
this.handleOnContextMenu(event.clientX, event.clientY);
}
}
render() {
const { chartAlert, chartStatus, chartId, emitCrossFilters } = this.props;
// Skip chart rendering
if (chartStatus === 'loading' || !!chartAlert || chartStatus === null) {
return null;
}
this.renderStartTime = Logger.getTimestamp();
const {
width,
height,
datasource,
annotationData,
initialValues,
ownState,
filterState,
chartIsStale,
formData,
latestQueryFormData,
postTransformProps,
} = this.props;
const currentFormData =
chartIsStale && latestQueryFormData ? latestQueryFormData : formData;
const vizType = currentFormData.viz_type || this.props.vizType;
// It's bad practice to use unprefixed `vizType` as classnames for chart
// container. It may cause css conflicts as in the case of legacy table chart.
// When migrating charts, we should gradually add a `superset-chart-` prefix
// to each one of them.
const snakeCaseVizType = snakeCase(vizType);
const chartClassName =
vizType === VizType.Table
? `superset-chart-${snakeCaseVizType}`
: snakeCaseVizType;
const webpackHash =
process.env.WEBPACK_MODE === 'development'
? `-${
// eslint-disable-next-line camelcase
typeof __webpack_require__ !== 'undefined' &&
// eslint-disable-next-line camelcase, no-undef
typeof __webpack_require__.h === 'function' &&
// eslint-disable-next-line no-undef, camelcase
__webpack_require__.h()
}`
: '';
let noResultsComponent;
const noResultTitle = t('No results were returned for this query');
const noResultDescription =
this.props.source === ChartSource.Explore
? t(
'Make sure that the controls are configured properly and the datasource contains data for the selected time range',
)
: undefined;
const noResultImage = 'chart.svg';
if (width > BIG_NO_RESULT_MIN_WIDTH && height > BIG_NO_RESULT_MIN_HEIGHT) {
noResultsComponent = (
<EmptyState
size="large"
title={noResultTitle}
description={noResultDescription}
image={noResultImage}
/>
);
} else {
noResultsComponent = (
<EmptyState size="small" title={noResultTitle} image={noResultImage} />
);
}
// Check for Behavior.DRILL_TO_DETAIL to tell if chart can receive Drill to
// Detail props or if it'll cause side-effects (e.g. excessive re-renders).
const drillToDetailProps = getChartMetadataRegistry()
.get(formData.viz_type)
?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
? { inContextMenu: this.state.inContextMenu }
: {};
return (
<>
{this.state.showContextMenu && (
<ChartContextMenu
ref={this.contextMenuRef}
id={chartId}
formData={currentFormData}
onSelection={this.handleContextMenuSelected}
onClose={this.handleContextMenuClosed}
/>
)}
<div
onContextMenu={
this.state.showContextMenu ? this.onContextMenuFallback : undefined
}
>
<SuperChart
disableErrorBoundary
key={`${chartId}${webpackHash}`}
id={`chart-id-${chartId}`}
className={chartClassName}
chartType={vizType}
width={width}
height={height}
annotationData={annotationData}
datasource={datasource}
initialValues={initialValues}
formData={currentFormData}
ownState={ownState}
filterState={filterState}
hooks={this.hooks}
behaviors={behaviors}
queriesData={this.mutableQueriesResponse}
onRenderSuccess={this.handleRenderSuccess}
onRenderFailure={this.handleRenderFailure}
noResults={noResultsComponent}
postTransformProps={postTransformProps}
emitCrossFilters={emitCrossFilters}
legendState={this.state.legendState}
{...drillToDetailProps}
/>
</div>
</>
);
}
}
ChartRenderer.propTypes = propTypes;
ChartRenderer.defaultProps = defaultProps;
export default ChartRenderer;

View File

@@ -0,0 +1,418 @@
/**
* 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 { snakeCase, cloneDeep, isEqual } from 'lodash';
import { useState, useRef, useCallback, useEffect, useMemo } from 'react';
import {
SuperChart,
logging,
Behavior,
t,
isFeatureEnabled,
FeatureFlag,
getChartMetadataRegistry,
QueryFormData,
ChartDataResponse,
VizType as enumVizType,
JsonObject,
FilterState,
} from '@superset-ui/core';
import { Logger, LOG_ACTIONS_RENDER_CHART } from 'src/logger/LogUtils';
import { EmptyState } from 'src/components/EmptyState';
import { ChartSource } from 'src/types/ChartSource';
import { Annotation } from 'src/explore/components/controls/AnnotationLayerControl';
import ChartContextMenu from './ChartContextMenu/ChartContextMenu';
import { Actions } from './Chart';
type ChartRendererProps = {
dashboardId?: number;
latestQueryFormData?: QueryFormData;
labelsColorMap?: string;
setDataMask?: (dataMask: any) => void;
source?: ChartSource;
annotationData?: Annotation;
actions: Actions;
chartId: string;
datasource?: {
database?: {
name: string;
};
};
initialValues?: object;
formData: QueryFormData;
labelsColor?: string;
height?: number;
width?: number;
setControlValue?: Function;
vizType: string;
triggerRender?: boolean;
chartAlert?: string;
chartStatus?: string;
queriesResponse?: ChartDataResponse[];
triggerQuery?: boolean;
chartIsStale?: boolean;
filterState?: FilterState[];
addFilter?: (
col: string,
vals: string | string[],
merge: boolean,
refresh: boolean,
) => void;
onFilterMenuOpen?: (chartId: string, column: string) => void;
onFilterMenuClose?: (chartId: string, column: string) => void;
ownState?: boolean | JsonObject;
postTransformProps?: Function;
emitCrossFilters?: boolean;
};
const BLANK = {};
const BIG_NO_RESULT_MIN_WIDTH = 300;
const BIG_NO_RESULT_MIN_HEIGHT = 220;
const behaviors = [Behavior.InteractiveChart];
const ChartRenderer = (props: ChartRendererProps) => {
const {
annotationData,
actions,
chartId,
datasource,
initialValues = BLANK,
formData,
latestQueryFormData,
height,
width,
setControlValue,
vizType,
chartStatus,
queriesResponse = [],
chartIsStale,
chartAlert,
addFilter = () => BLANK,
setDataMask,
onFilterMenuOpen = () => BLANK,
onFilterMenuClose = () => BLANK,
ownState,
filterState,
postTransformProps,
source,
emitCrossFilters,
triggerRender,
labelsColor,
labelsColorMap,
} = props;
if (chartStatus === 'loading' || !!chartAlert || chartStatus === null) {
return null;
}
const suppressContextMenu = getChartMetadataRegistry().get(
formData.viz_type ?? vizType,
)?.suppressContextMenu;
const [showContextMenu, setShowContextMenu] = useState<Boolean>(false);
const [inContextMenu, setInContextMenu] = useState(false);
const [legendState, setLegendState] = useState<any>(undefined);
const contextMenuRef = useRef<any>(null);
const prevProps = useRef(props);
const mutableQueriesResponse = useRef(cloneDeep(queriesResponse));
const [hasQueryResponseChange, setHasQueryResponseChange] = useState(false);
const renderStartTime = useRef<number>(0);
const resultsReady = useMemo(
() =>
queriesResponse &&
['success', 'rendered'].includes(chartStatus) &&
!queriesResponse?.[0]?.error,
[queriesResponse, chartStatus],
);
const queryResponseChanged = useMemo(
() => queriesResponse !== prevProps.current.queriesResponse,
[queriesResponse],
);
const shouldRender = useMemo(
() =>
resultsReady &&
(queryResponseChanged ||
!isEqual(datasource, prevProps.current.datasource) ||
annotationData !== prevProps.current.annotationData ||
ownState !== prevProps.current.ownState ||
filterState !== prevProps.current.filterState ||
height !== prevProps.current.height ||
width !== prevProps.current.width ||
triggerRender ||
labelsColor !== prevProps.current.labelsColor ||
labelsColorMap !== prevProps.current.labelsColorMap ||
formData.color_scheme !== prevProps.current.formData.color_scheme ||
formData.stack !== prevProps.current.formData.stack ||
emitCrossFilters !== prevProps.current.emitCrossFilters),
[resultsReady, queryResponseChanged, props],
);
if (!shouldRender) {
return null;
}
useEffect(() => {
if (queryResponseChanged) {
setHasQueryResponseChange(true);
mutableQueriesResponse.current = cloneDeep(queriesResponse);
}
}, [queryResponseChanged, queriesResponse]);
useEffect(() => {
prevProps.current = props;
}, [props]);
useEffect(() => {
mutableQueriesResponse.current = cloneDeep(queriesResponse);
}, [queriesResponse]);
useEffect(() => {
setShowContextMenu(
source === ChartSource.Dashboard &&
!suppressContextMenu &&
isFeatureEnabled(FeatureFlag.DrillToDetail),
);
}, [source, suppressContextMenu]);
useEffect(() => {
// only log chart render time which is triggered by query results change
// currently we don't log chart re-render time, like window resize etc
if (hasQueryResponseChange) {
actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
slice_id: chartId,
has_err: false,
error_details: '',
start_offset: renderStartTime.current,
ts: new Date().getTime(),
duration: Logger.getTimestamp() - renderStartTime.current,
});
}
}, [hasQueryResponseChange]);
/**
* Hooks region
*/
const handleAddFilter = useCallback(
(col: string, vals: string | string[], merge = true, refresh = true) => {
alert(col);
console.log(col, vals, merge, refresh);
addFilter(col, vals, merge, refresh);
},
[],
);
const handleOnContextMenu = useCallback(
(offsetX: number, offsetY: number, filters: undefined) => {
if (contextMenuRef.current) {
contextMenuRef.current.open(offsetX, offsetY, filters);
}
// setInContextMenu({ inContextMenu: true });
// setInContextMenu(true);
},
[],
);
const handleSetControlValue = useCallback(
(...args: string[]) => {
if (setControlValue) {
setControlValue(...args);
}
},
[setControlValue],
);
const handleRenderFailure = (
error: { toString: () => string },
info: { componentStack: string } | null,
) => {
logging.warn(error);
actions.chartRenderingFailed(
error.toString(),
chartId,
info ? info.componentStack : null,
);
// only trigger render log when query is changed
if (hasQueryResponseChange) {
actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
slice_id: chartId,
has_err: true,
error_details: error.toString(),
start_offset: renderStartTime.current,
ts: new Date().getTime(),
duration: Logger.getTimestamp() - renderStartTime.current,
});
}
};
const handleRenderSuccess = useCallback(() => {
if (!['loading', 'rendered'].includes(chartStatus || '')) {
actions.chartRenderingSucceeded({ key: chartId });
}
actions.logEvent(LOG_ACTIONS_RENDER_CHART, {
slice_id: chartId,
has_err: false,
error_details: '',
start_offset: renderStartTime.current,
ts: new Date().getTime(),
duration: Logger.getTimestamp() - renderStartTime.current,
});
}, [actions, chartId, chartStatus, vizType]);
// end Hooks region
const currentFormData =
chartIsStale && latestQueryFormData ? latestQueryFormData : formData;
const snakeCaseVizType = snakeCase(currentFormData.viz_type || vizType);
const chartClassName =
vizType === enumVizType.Table
? `superset-chart-${snakeCaseVizType}`
: snakeCaseVizType;
const webpackHash =
process.env.WEBPACK_MODE === 'development'
? `-${
// eslint-disable-next-line camelcase
// @ts-ignore
typeof __webpack_require__ !== 'undefined' &&
// @ts-ignore
typeof __webpack_require__.h === 'function' &&
// eslint-disable-next-line camelcase, no-undef
// @ts-ignore
typeof __webpack_require__.h === 'function' &&
// eslint-disable-next-line no-undef, camelcase
// @ts-ignore
__webpack_require__.h()
}`
: '';
let noResultsComponent;
const noResultTitle = t('No results were returned for this query');
const noResultDescription =
source === ChartSource.Explore
? t(
'Make sure that the controls are configured properly and the datasource contains data for the selected time range',
)
: undefined;
const noResultImage = 'chart.svg';
if (
typeof width === 'number' &&
typeof height === 'number' &&
width > BIG_NO_RESULT_MIN_WIDTH &&
height > BIG_NO_RESULT_MIN_HEIGHT
) {
noResultsComponent = (
<EmptyState
size="large"
title={noResultTitle}
description={noResultDescription}
image={noResultImage}
/>
);
} else {
noResultsComponent = (
<EmptyState size="small" title={noResultTitle} image={noResultImage} />
);
}
// Check for Behavior.DRILL_TO_DETAIL to tell if chart can receive Drill to
// Detail props or if it'll cause side-effects (e.g. excessive re-renders).
const drillToDetailProps = getChartMetadataRegistry()
.get(formData.viz_type)
?.behaviors.find(behavior => behavior === Behavior.DrillToDetail)
? { inContextMenu }
: {};
const hooks = useMemo(
() => ({
onAddFilter: handleAddFilter,
onContextMenu: showContextMenu ? handleOnContextMenu : undefined,
onError: handleRenderFailure,
setControlValue: handleSetControlValue,
onFilterMenuOpen,
onFilterMenuClose,
onLegendChange: setLegendState,
setDataMask: (dataMask: any) =>
actions?.updateDataMask(chartId, { dataMask }),
}),
[
handleAddFilter,
showContextMenu,
handleOnContextMenu,
handleRenderFailure,
handleSetControlValue,
onFilterMenuOpen,
onFilterMenuClose,
setLegendState,
setDataMask,
chartId,
],
);
return (
<>
{showContextMenu && (
<ChartContextMenu
ref={contextMenuRef}
id={chartId as unknown as number}
formData={currentFormData}
onSelection={() => setInContextMenu(false)}
onClose={() => setInContextMenu(false)}
/>
)}
<div
onContextMenu={
showContextMenu
? event => {
event.preventDefault();
handleOnContextMenu(event.clientX, event.clientY, undefined);
}
: undefined
}
>
<SuperChart
disableErrorBoundary
key={`${chartId}${webpackHash}`}
id={`chart-id-${chartId}`}
className={chartClassName}
chartType={vizType}
width={width}
height={height}
annotationData={annotationData}
datasource={datasource}
initialValues={initialValues}
formData={currentFormData}
ownState={ownState}
filterState={filterState}
hooks={hooks}
behaviors={behaviors}
queriesData={mutableQueriesResponse.current}
onRenderSuccess={handleRenderSuccess}
onRenderFailure={handleRenderFailure}
noResults={noResultsComponent}
postTransformProps={postTransformProps}
// emitCrossFilters={emitCrossFilters}
legendState={legendState}
{...drillToDetailProps}
/>
</div>
</>
);
};
export default ChartRenderer;

View File

@@ -157,6 +157,7 @@ const Chart = props => {
state.datasources[chart.form_data.datasource]) ||
PLACEHOLDER_DATASOURCE,
);
const dashboardInfo = useSelector(state => state.dashboardInfo);
const [descriptionHeight, setDescriptionHeight] = useState(0);
const [height, setHeight] = useState(props.height);
@@ -304,6 +305,8 @@ const Chart = props => {
],
);
formData.dashboardId = dashboardInfo.id;
const onExploreChart = useCallback(
async clickEvent => {
const isOpenInNewTab =

View File

@@ -74,6 +74,7 @@ const defaultState = {
datasources: mockDatasource,
dashboardState: { editMode: false, expandedSlices: {} },
dashboardInfo: {
id: props.dashboardId,
superset_can_explore: false,
superset_can_share: false,
superset_can_csv: false,
@@ -165,7 +166,9 @@ test('should call exportChart when exportCSV is clicked', async () => {
expect(stubbedExportCSV).toHaveBeenCalledTimes(1);
expect(stubbedExportCSV).toHaveBeenCalledWith(
expect.objectContaining({
formData: expect.anything(),
formData: expect.objectContaining({
dashboardId: 111,
}),
resultType: 'full',
resultFormat: 'csv',
}),
@@ -195,6 +198,7 @@ test('should call exportChart with row_limit props.maxRows when exportFullCSV is
expect.objectContaining({
formData: expect.objectContaining({
row_limit: 666,
dashboardId: 111,
}),
resultType: 'full',
resultFormat: 'csv',
@@ -249,6 +253,7 @@ test('should call exportChart with row_limit props.maxRows when exportFullXLSX i
expect.objectContaining({
formData: expect.objectContaining({
row_limit: 666,
dashboardId: 111,
}),
resultType: 'full',
resultFormat: 'xlsx',

View File

@@ -1,151 +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 { useCallback, useEffect, useMemo, useState } from 'react';
import { VariableSizeList as List } from 'react-window';
import { cloneDeep } from 'lodash';
import { FlattenedItem, Folder } from './types';
import DatasourcePanelItem from './DatasourcePanelItem';
const BORDER_WIDTH = 2;
const HEADER_ITEM_HEIGHT = 50;
const METRIC_OR_COLUMN_ITEM_HEIGHT = 32;
const DIVIDER_ITEM_HEIGHT = 16;
const flattenFolderStructure = (
folders: Folder[],
depth = 0,
folderMap: Map<string, Folder> = new Map(),
): { flattenedItems: FlattenedItem[]; folderMap: Map<string, Folder> } => {
const flattenedItems: FlattenedItem[] = [];
folders.forEach((folder, idx) => {
folderMap.set(folder.id, folder);
flattenedItems.push({
type: 'header',
folderId: folder.id,
depth,
height: HEADER_ITEM_HEIGHT,
});
if (!folder.isCollapsed) {
folder.items.forEach(item => {
flattenedItems.push({
type: 'item',
folderId: folder.id,
depth,
item,
height: METRIC_OR_COLUMN_ITEM_HEIGHT,
});
});
if (folder.subFolders && folder.subFolders.length > 0) {
const { flattenedItems: subItems } = flattenFolderStructure(
folder.subFolders,
depth + 1,
folderMap,
);
flattenedItems.push(...subItems);
}
}
if (depth === 0 && idx !== folders.length - 1) {
flattenedItems.push({
type: 'divider',
folderId: folder.id,
depth,
height: DIVIDER_ITEM_HEIGHT,
});
}
});
return { flattenedItems, folderMap };
};
interface DatasourceItemsProps {
width: number;
height: number;
folders: Folder[];
}
export const DatasourceItems = ({
width,
height,
folders,
}: DatasourceItemsProps) => {
const [folderStructure, setFolderStructure] = useState<Folder[]>(folders);
useEffect(() => {
setFolderStructure(prev => (prev !== folders ? folders : prev));
}, [folders]);
const { flattenedItems, folderMap } = useMemo(
() => flattenFolderStructure(folderStructure),
[folderStructure],
);
const handleToggleCollapse = useCallback((folderId: string) => {
setFolderStructure(prevFolders => {
const updatedFolders = cloneDeep(prevFolders);
const updateFolder = (folders: Folder[] | undefined): boolean => {
if (!folders) {
return false;
}
for (let i = 0; i < folders.length; i += 1) {
if (folders[i].id === folderId) {
// eslint-disable-next-line no-param-reassign
folders[i].isCollapsed = !folders[i].isCollapsed;
return true;
}
if (folders[i].subFolders && updateFolder(folders[i].subFolders)) {
return true;
}
}
return false;
};
updateFolder(updatedFolders);
return updatedFolders;
});
}, []);
const getItemSize = useCallback(
(index: number) => flattenedItems[index].height,
[flattenedItems],
);
return (
<List
width={width - BORDER_WIDTH}
height={height}
itemSize={getItemSize}
itemCount={flattenedItems.length}
itemData={{
flattenedItems,
folderMap,
width,
onToggleCollapse: handleToggleCollapse,
}}
overscanCount={5}
>
{DatasourcePanelItem}
</List>
);
};

View File

@@ -59,48 +59,6 @@ const datasource: IDatasource = {
datasource_name: 'table1',
};
const datasourceWithFolders: IDatasource = {
...datasource,
folders: [
{
name: 'Test folder',
type: 'folder',
uuid: '1',
children: [
{
name: 'Test nested folder',
type: 'folder',
uuid: '1.1',
children: [
{
type: 'metric',
uuid: metrics[0].uuid,
name: metrics[0].metric_name,
},
],
},
],
},
{
name: 'Second test folder',
type: 'folder',
uuid: '2',
children: [
{
type: 'column',
uuid: columns[0].uuid,
name: columns[0].column_name,
},
{
type: 'column',
uuid: columns[1].uuid,
name: columns[1].column_name,
},
],
},
],
};
const mockUser = {
createdOn: '2021-04-27T18:12:38.952304',
email: 'admin',
@@ -132,18 +90,6 @@ const props: DatasourcePanelProps = {
width: 300,
};
const propsWithFolders = {
...props,
datasource: datasourceWithFolders,
controls: {
...props.controls,
datasource: {
...props.controls.datasource,
datasource: datasourceWithFolders,
},
},
};
const metricProps = {
savedMetrics: [],
columns: [],
@@ -179,9 +125,13 @@ test('should render the metrics', async () => {
</ExploreContainer>,
{ useRedux: true, useDnd: true },
);
const metricsNum = metrics.length;
metrics.forEach(metric =>
expect(screen.getByText(metric.metric_name)).toBeInTheDocument(),
);
expect(
await screen.findByText(`Showing ${metricsNum} of ${metricsNum}`),
).toBeInTheDocument();
});
test('should render the columns', async () => {
@@ -192,9 +142,13 @@ test('should render the columns', async () => {
</ExploreContainer>,
{ useRedux: true, useDnd: true },
);
const columnsNum = columns.length;
columns.forEach(col =>
expect(screen.getByText(col.column_name)).toBeInTheDocument(),
);
expect(
await screen.findByText(`Showing ${columnsNum} of ${columnsNum}`),
).toBeInTheDocument();
});
describe('DatasourcePanel', () => {
@@ -356,139 +310,3 @@ test('should render only droppable metrics and columns', async () => {
unmount();
});
test('Renders with custom folders', () => {
render(
<ExploreContainer>
<DatasourcePanel {...propsWithFolders} />
<DndMetricSelect {...metricProps} />
</ExploreContainer>,
{
useRedux: true,
useDnd: true,
},
);
expect(screen.getByText('Test folder')).toBeInTheDocument();
expect(screen.getByText('Test nested folder')).toBeInTheDocument();
expect(screen.getByText('Second test folder')).toBeInTheDocument();
expect(screen.getByText('Metrics')).toBeInTheDocument();
expect(screen.getByText('Columns')).toBeInTheDocument();
columns.forEach(col => {
expect(screen.getByText(col.column_name)).toBeInTheDocument();
});
metrics.forEach(metric => {
expect(screen.getByText(metric.metric_name)).toBeInTheDocument();
});
expect(screen.getAllByTestId('DatasourcePanelDragOption').length).toEqual(5);
expect(screen.getAllByTestId('datasource-panel-divider').length).toEqual(3);
});
test('Collapse folders', () => {
render(
<ExploreContainer>
<DatasourcePanel {...propsWithFolders} />
<DndMetricSelect {...metricProps} />
</ExploreContainer>,
{
useRedux: true,
useDnd: true,
},
);
userEvent.click(screen.getAllByRole('button')[0]);
expect(screen.getByText('Test folder')).toBeInTheDocument();
expect(screen.queryByText('Test nested folder')).not.toBeInTheDocument();
expect(screen.getByText('Second test folder')).toBeInTheDocument();
expect(screen.getByText('Metrics')).toBeInTheDocument();
expect(screen.getByText('Columns')).toBeInTheDocument();
expect(screen.queryByText(metrics[0].metric_name)).not.toBeInTheDocument();
userEvent.click(screen.getAllByRole('button')[0]);
expect(screen.getByText('Test folder')).toBeInTheDocument();
expect(screen.getByText('Test nested folder')).toBeInTheDocument();
expect(screen.getByText('Second test folder')).toBeInTheDocument();
expect(screen.getByText('Metrics')).toBeInTheDocument();
expect(screen.getByText('Columns')).toBeInTheDocument();
expect(screen.getByText(metrics[0].metric_name)).toBeInTheDocument();
});
test('Default Metrics and Columns folders dont render when all metrics and columns are assigned to custom folders', () => {
const datasourceWithFullFolders: IDatasource = {
...datasource,
folders: [
{
name: 'Test folder',
type: 'folder',
uuid: '1',
children: [
{
name: 'Test nested folder',
type: 'folder',
uuid: '1.1',
children: metrics.map(m => ({
type: 'metric' as const,
uuid: m.uuid,
name: m.metric_name,
})),
},
],
},
{
name: 'Second test folder',
type: 'folder',
uuid: '2',
children: columns.map(c => ({
type: 'column',
uuid: c.uuid,
name: c.column_name,
})),
},
],
};
const propsWithFullFolders = {
...props,
datasource: datasourceWithFullFolders,
controls: {
...props.controls,
datasource: {
...props.controls.datasource,
datasource: datasourceWithFullFolders,
},
},
};
render(
<ExploreContainer>
<DatasourcePanel {...propsWithFullFolders} />
<DndMetricSelect {...metricProps} />
</ExploreContainer>,
{
useRedux: true,
useDnd: true,
},
);
expect(screen.getByText('Test folder')).toBeInTheDocument();
expect(screen.getByText('Test nested folder')).toBeInTheDocument();
expect(screen.getByText('Second test folder')).toBeInTheDocument();
expect(screen.queryByText('Metrics')).not.toBeInTheDocument();
expect(screen.queryByText('Columns')).not.toBeInTheDocument();
columns.forEach(col => {
expect(screen.getByText(col.column_name)).toBeInTheDocument();
});
metrics.forEach(metric => {
expect(screen.getByText(metric.metric_name)).toBeInTheDocument();
});
expect(screen.getAllByTestId('DatasourcePanelDragOption').length).toEqual(5);
expect(screen.getAllByTestId('datasource-panel-divider').length).toEqual(1);
});

View File

@@ -23,7 +23,7 @@ import DatasourcePanelDragOption from '.';
test('should render', async () => {
render(
<DatasourcePanelDragOption
value={{ metric_name: 'test', uuid: '1' }}
value={{ metric_name: 'test' }}
type={DndItemType.Metric}
/>,
{ useDnd: true },
@@ -38,7 +38,7 @@ test('should render', async () => {
test('should have attribute draggable:true', async () => {
render(
<DatasourcePanelDragOption
value={{ metric_name: 'test', uuid: '1' }}
value={{ metric_name: 'test' }}
type={DndItemType.Metric}
/>,
{ useDnd: true },

View File

@@ -20,84 +20,178 @@ import {
columns,
metrics,
} from 'src/explore/components/DatasourcePanel/fixtures';
import { screen, userEvent, render } from 'spec/helpers/testing-library';
import DatasourcePanelItem, {
DatasourcePanelItemProps,
} from './DatasourcePanelItem';
import { fireEvent, render, within } from 'spec/helpers/testing-library';
import DatasourcePanelItem from './DatasourcePanelItem';
const mockData: DatasourcePanelItemProps['data'] = {
flattenedItems: [
{ type: 'header', depth: 0, folderId: '1', height: 50 },
...metrics.map((m, idx) => ({
type: 'item' as const,
depth: 0,
folderId: '1',
height: 32,
index: idx,
item: { ...m, type: 'metric' as const },
})),
{ type: 'divider', depth: 0, folderId: '1', height: 16 },
{ type: 'header', depth: 0, folderId: '2', height: 50 },
...columns.map((m, idx) => ({
type: 'item' as const,
depth: 0,
folderId: '2',
height: 32,
index: idx,
item: { ...m, type: 'column' as const },
})),
],
folderMap: new Map([
[
'1',
{
id: '1',
isCollapsed: false,
name: 'Metrics',
items: metrics.map(m => ({ ...m, type: 'metric' })),
},
],
[
'2',
{
id: '2',
isCollapsed: false,
name: 'Columns',
items: columns.map(c => ({ ...c, type: 'column' })),
},
],
]),
const mockData = {
metricSlice: metrics,
columnSlice: columns,
totalMetrics: Math.max(metrics.length, 10),
totalColumns: Math.max(columns.length, 13),
width: 300,
onToggleCollapse: jest.fn(),
showAllMetrics: false,
onShowAllMetricsChange: jest.fn(),
showAllColumns: false,
onShowAllColumnsChange: jest.fn(),
collapseMetrics: false,
onCollapseMetricsChange: jest.fn(),
collapseColumns: false,
onCollapseColumnsChange: jest.fn(),
hiddenMetricCount: 0,
hiddenColumnCount: 0,
};
const setup = (data: DatasourcePanelItemProps['data'] = mockData) =>
render(
<>
{data.flattenedItems.map((_, index) => (
<DatasourcePanelItem index={index} data={data} style={{}} />
))}
</>,
test('renders each item accordingly', () => {
const { getByText, getByTestId, rerender, container } = render(
<DatasourcePanelItem index={0} data={mockData} style={{}} />,
{ useDnd: true },
);
test('renders each item accordingly', () => {
setup();
expect(screen.getByText('Metrics')).toBeInTheDocument();
expect(screen.getByText('metric_end_certified')).toBeInTheDocument();
expect(screen.getByText('metric_end')).toBeInTheDocument();
expect(getByText('Metrics')).toBeInTheDocument();
rerender(<DatasourcePanelItem index={1} data={mockData} style={{}} />);
expect(
getByText(
`Showing ${mockData.metricSlice.length} of ${mockData.totalMetrics}`,
),
).toBeInTheDocument();
mockData.metricSlice.forEach((metric, metricIndex) => {
rerender(
<DatasourcePanelItem
index={metricIndex + 2}
data={mockData}
style={{}}
/>,
);
expect(getByTestId('DatasourcePanelDragOption')).toBeInTheDocument();
expect(
within(getByTestId('DatasourcePanelDragOption')).getByText(
metric.metric_name,
),
).toBeInTheDocument();
});
rerender(
<DatasourcePanelItem
index={2 + mockData.metricSlice.length}
data={mockData}
style={{}}
/>,
);
expect(container).toHaveTextContent('');
expect(screen.getByText('Columns')).toBeInTheDocument();
expect(screen.getByText('bootcamp_attend')).toBeInTheDocument();
expect(screen.getByText('calc_first_time_dev')).toBeInTheDocument();
expect(screen.getByText('aaaaaaaaaaa')).toBeInTheDocument();
expect(screen.getByTestId('datasource-panel-divider')).toBeInTheDocument();
expect(screen.getAllByTestId('DatasourcePanelDragOption').length).toEqual(5);
const startIndexOfColumnSection = mockData.metricSlice.length + 3;
rerender(
<DatasourcePanelItem
index={startIndexOfColumnSection}
data={mockData}
style={{}}
/>,
);
expect(getByText('Columns')).toBeInTheDocument();
rerender(
<DatasourcePanelItem
index={startIndexOfColumnSection + 1}
data={mockData}
style={{}}
/>,
);
expect(
getByText(
`Showing ${mockData.columnSlice.length} of ${mockData.totalColumns}`,
),
).toBeInTheDocument();
mockData.columnSlice.forEach((column, columnIndex) => {
rerender(
<DatasourcePanelItem
index={startIndexOfColumnSection + columnIndex + 2}
data={mockData}
style={{}}
/>,
);
expect(getByTestId('DatasourcePanelDragOption')).toBeInTheDocument();
expect(
within(getByTestId('DatasourcePanelDragOption')).getByText(
column.column_name,
),
).toBeInTheDocument();
});
});
test('can collapse metrics and columns', () => {
setup();
userEvent.click(screen.getAllByRole('button')[0]);
expect(mockData.onToggleCollapse).toHaveBeenCalled();
mockData.onCollapseMetricsChange.mockClear();
mockData.onCollapseColumnsChange.mockClear();
const { queryByText, getByRole, rerender } = render(
<DatasourcePanelItem index={0} data={mockData} style={{}} />,
{ useDnd: true },
);
fireEvent.click(getByRole('button'));
expect(mockData.onCollapseMetricsChange).toHaveBeenCalled();
expect(mockData.onCollapseColumnsChange).not.toHaveBeenCalled();
const startIndexOfColumnSection = mockData.metricSlice.length + 3;
rerender(
<DatasourcePanelItem
index={startIndexOfColumnSection}
data={mockData}
style={{}}
/>,
);
fireEvent.click(getByRole('button'));
expect(mockData.onCollapseColumnsChange).toHaveBeenCalled();
rerender(
<DatasourcePanelItem
index={1}
data={{
...mockData,
collapseMetrics: true,
}}
style={{}}
/>,
);
expect(
queryByText(
`Showing ${mockData.metricSlice.length} of ${mockData.totalMetrics}`,
),
).not.toBeInTheDocument();
rerender(
<DatasourcePanelItem
index={2}
data={{
...mockData,
collapseMetrics: true,
}}
style={{}}
/>,
);
expect(queryByText('Columns')).toBeInTheDocument();
});
test('shows ineligible items count', () => {
const hiddenColumnCount = 3;
const hiddenMetricCount = 1;
const dataWithHiddenItems = {
...mockData,
hiddenColumnCount,
hiddenMetricCount,
};
const { getByText, rerender } = render(
<DatasourcePanelItem index={1} data={dataWithHiddenItems} style={{}} />,
{ useDnd: true },
);
expect(
getByText(`${hiddenMetricCount} ineligible item(s) are hidden`),
).toBeInTheDocument();
const startIndexOfColumnSection = mockData.metricSlice.length + 3;
rerender(
<DatasourcePanelItem
index={startIndexOfColumnSection + 1}
data={dataWithHiddenItems}
style={{}}
/>,
);
expect(
getByText(`${hiddenColumnCount} ineligible item(s) are hidden`),
).toBeInTheDocument();
});

View File

@@ -16,24 +16,71 @@
* specific language governing permissions and limitations
* under the License.
*/
import { CSSProperties } from 'react';
import { CSSProperties, FC } from 'react';
import { css, styled, useTheme } from '@superset-ui/core';
import { css, Metric, styled, t, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import DatasourcePanelDragOption from './DatasourcePanelDragOption';
import { DndItemType } from '../DndItemType';
import { DndItemValue, FlattenedItem, Folder } from './types';
import { DndItemValue } from './types';
export type DataSourcePanelColumn = {
is_dttm?: boolean | null;
description?: string | null;
expression?: string | null;
is_certified?: number | null;
column_name?: string | null;
name?: string | null;
type?: string;
};
type Props = {
index: number;
style: CSSProperties;
data: {
metricSlice: Metric[];
columnSlice: DataSourcePanelColumn[];
totalMetrics: number;
totalColumns: number;
width: number;
showAllMetrics: boolean;
onShowAllMetricsChange: (showAll: boolean) => void;
showAllColumns: boolean;
onShowAllColumnsChange: (showAll: boolean) => void;
collapseMetrics: boolean;
onCollapseMetricsChange: (collapse: boolean) => void;
collapseColumns: boolean;
onCollapseColumnsChange: (collapse: boolean) => void;
hiddenMetricCount: number;
hiddenColumnCount: number;
};
};
export const DEFAULT_MAX_COLUMNS_LENGTH = 50;
export const DEFAULT_MAX_METRICS_LENGTH = 50;
export const ITEM_HEIGHT = 30;
const Button = styled.button`
background: none;
border: none;
text-decoration: underline;
color: ${({ theme }) => theme.colors.primary.dark1};
`;
const ButtonContainer = styled.div`
text-align: center;
padding-top: 2px;
`;
const LabelWrapper = styled.div`
${({ theme }) => css`
color: ${theme.colors.grayscale.dark1};
overflow: hidden;
text-overflow: ellipsis;
font-size: ${theme.typography.sizes.s}px;
background-color: ${theme.colors.grayscale.light4};
margin: ${theme.gridUnit * 2}px 0;
border-radius: ${theme.borderRadius}px;
border-radius: 4px;
padding: 0 ${theme.gridUnit}px;
&:first-of-type {
@@ -76,95 +123,146 @@ const SectionHeaderButton = styled.button`
border: none;
background: transparent;
width: 100%;
height: 100%;
padding-inline: 0;
padding-inline: 0px;
`;
const SectionHeader = styled.span`
${({ theme }) => css`
color: ${theme.colors.grayscale.dark1};
${({ theme }) => `
font-size: ${theme.typography.sizes.m}px;
font-weight: ${theme.typography.weights.medium};
line-height: 1.3;
`}
`;
const Divider = styled.div`
${({ theme }) => css`
height: 16px;
border-bottom: 1px solid ${theme.colors.grayscale.light3};
const Box = styled.div`
${({ theme }) => `
border: 1px ${theme.colors.grayscale.light4} solid;
border-radius: ${theme.gridUnit}px;
font-size: ${theme.typography.sizes.s}px;
padding: ${theme.gridUnit}px;
color: ${theme.colors.grayscale.light1};
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
`}
`;
export interface DatasourcePanelItemProps {
index: number;
style: CSSProperties;
data: {
flattenedItems: FlattenedItem[];
folderMap: Map<string, Folder>;
width: number;
onToggleCollapse: (folderId: string) => void;
};
}
const DatasourcePanelItem: FC<Props> = ({ index, style, data }) => {
const {
metricSlice: _metricSlice,
columnSlice,
totalMetrics,
totalColumns,
width,
showAllMetrics,
onShowAllMetricsChange,
showAllColumns,
onShowAllColumnsChange,
collapseMetrics,
onCollapseMetricsChange,
collapseColumns,
onCollapseColumnsChange,
hiddenMetricCount,
hiddenColumnCount,
} = data;
const metricSlice = collapseMetrics ? [] : _metricSlice;
const DatasourcePanelItem = ({
index,
style,
data,
}: DatasourcePanelItemProps) => {
const { flattenedItems, folderMap, width, onToggleCollapse } = data;
const item = flattenedItems[index];
const EXTRA_LINES = collapseMetrics ? 1 : 2;
const isColumnSection = collapseMetrics
? index >= 1
: index > metricSlice.length + EXTRA_LINES;
const HEADER_LINE = isColumnSection
? metricSlice.length + EXTRA_LINES + 1
: 0;
const SUBTITLE_LINE = HEADER_LINE + 1;
const BOTTOM_LINE =
(isColumnSection ? columnSlice.length : metricSlice.length) +
(collapseMetrics ? HEADER_LINE : SUBTITLE_LINE) +
1;
const collapsed = isColumnSection ? collapseColumns : collapseMetrics;
const setCollapse = isColumnSection
? onCollapseColumnsChange
: onCollapseMetricsChange;
const showAll = isColumnSection ? showAllColumns : showAllMetrics;
const setShowAll = isColumnSection
? onShowAllColumnsChange
: onShowAllMetricsChange;
const theme = useTheme();
if (!item) return null;
const folder = folderMap.get(item.folderId);
if (!folder) return null;
const indentation = item.depth * theme.gridUnit * 4;
const hiddenCount = isColumnSection ? hiddenColumnCount : hiddenMetricCount;
return (
<div
style={{
...style,
paddingLeft: theme.gridUnit * 4 + indentation,
paddingRight: theme.gridUnit * 4,
}}
style={style}
css={css`
padding: 0 ${theme.gridUnit * 4}px;
`}
>
{item.type === 'header' && (
<SectionHeaderButton onClick={() => onToggleCollapse(folder.id)}>
<SectionHeader>{folder.name}</SectionHeader>
{folder.isCollapsed ? (
{index === HEADER_LINE && (
<SectionHeaderButton onClick={() => setCollapse(!collapsed)}>
<SectionHeader>
{isColumnSection ? t('Columns') : t('Metrics')}
</SectionHeader>
{collapsed ? (
<Icons.DownOutlined iconSize="s" />
) : (
<Icons.UpOutlined iconSize="s" />
)}
</SectionHeaderButton>
)}
{item.type === 'item' && item.item && (
{index === SUBTITLE_LINE && !collapsed && (
<div
css={css`
display: flex;
gap: ${theme.gridUnit * 2}px;
justify-content: space-between;
align-items: baseline;
`}
>
<div
className="field-length"
css={css`
flex-shrink: 0;
`}
>
{isColumnSection
? t(`Showing %s of %s`, columnSlice?.length, totalColumns)
: t(`Showing %s of %s`, metricSlice?.length, totalMetrics)}
</div>
{hiddenCount > 0 && (
<Box>{t(`%s ineligible item(s) are hidden`, hiddenCount)}</Box>
)}
</div>
)}
{index > SUBTITLE_LINE && index < BOTTOM_LINE && (
<LabelWrapper
key={
(item.item.type === 'column'
? item.item.column_name
: item.item.metric_name) + String(width)
(isColumnSection
? columnSlice[index - SUBTITLE_LINE - 1].column_name
: metricSlice[index - SUBTITLE_LINE - 1].metric_name) +
String(width)
}
className="column"
>
<DatasourcePanelDragOption
value={item.item as DndItemValue}
type={
item.item.type === 'column'
? DndItemType.Column
: DndItemType.Metric
value={
isColumnSection
? (columnSlice[index - SUBTITLE_LINE - 1] as DndItemValue)
: metricSlice[index - SUBTITLE_LINE - 1]
}
type={isColumnSection ? DndItemType.Column : DndItemType.Metric}
/>
</LabelWrapper>
)}
{item.type === 'divider' && (
<Divider data-test="datasource-panel-divider" />
)}
{index === BOTTOM_LINE &&
!collapsed &&
(isColumnSection
? totalColumns > DEFAULT_MAX_COLUMNS_LENGTH
: totalMetrics > DEFAULT_MAX_METRICS_LENGTH) && (
<ButtonContainer>
<Button onClick={() => setShowAll(!showAll)}>
{showAll ? t('Show less...') : t('Show all...')}
</Button>
</ButtonContainer>
)}
</div>
);
};

View File

@@ -26,7 +26,6 @@ export const columns = [
filterable: true,
groupby: true,
id: 516,
uuid: '516',
is_dttm: false,
python_date_format: null,
type: 'DOUBLE',
@@ -41,7 +40,6 @@ export const columns = [
filterable: true,
groupby: true,
id: 477,
uuid: '477',
is_dttm: false,
python_date_format: null,
type: 'VARCHAR',
@@ -54,8 +52,7 @@ export const columns = [
expression: null,
filterable: true,
groupby: true,
id: 517,
uuid: '517',
id: 516,
is_dttm: false,
python_date_format: null,
type: 'INT',
@@ -73,7 +70,6 @@ const metricsFiltered = {
description: null,
expression: '',
id: 56,
uuid: '56',
is_certified: true,
metric_name: 'metric_end_certified',
verbose_name: '',
@@ -88,7 +84,6 @@ const metricsFiltered = {
description: null,
expression: '',
id: 57,
uuid: '57',
is_certified: false,
metric_name: 'metric_end',
verbose_name: '',

View File

@@ -28,6 +28,7 @@ import {
import { ControlConfig } from '@superset-ui/chart-controls';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList as List } from 'react-window';
import { matchSorter, rankings } from 'match-sorter';
import Alert from 'src/components/Alert';
@@ -38,19 +39,22 @@ import { FAST_DEBOUNCE } from 'src/constants';
import { ExploreActions } from 'src/explore/actions/exploreActions';
import Control from 'src/explore/components/Control';
import { useDebounceValue } from 'src/hooks/useDebounceValue';
import DatasourcePanelItem, {
ITEM_HEIGHT,
DataSourcePanelColumn,
DEFAULT_MAX_COLUMNS_LENGTH,
DEFAULT_MAX_METRICS_LENGTH,
} from './DatasourcePanelItem';
import { DndItemType } from '../DndItemType';
import { DatasourceFolder, DatasourcePanelColumn, DndItemValue } from './types';
import { DndItemValue } from './types';
import { DropzoneContext } from '../ExploreContainer';
import { DatasourceItems } from './DatasourceItems';
import { transformDatasourceWithFolders } from './transformDatasourceFolders';
interface DatasourceControl extends Omit<ControlConfig, 'hidden'> {
datasource?: IDatasource;
}
export interface IDatasource {
metrics: Metric[];
columns: DatasourcePanelColumn[];
folders?: DatasourceFolder[];
columns: DataSourcePanelColumn[];
id: number;
type: DatasourceType;
database: {
@@ -122,18 +126,8 @@ const StyledInfoboxWrapper = styled.div`
const BORDER_WIDTH = 2;
const sortColumns = (slice: DatasourcePanelColumn[]) =>
[...slice]
.sort((col1, col2) => {
if (col1?.is_dttm && !col2?.is_dttm) {
return -1;
}
if (col2?.is_dttm && !col1?.is_dttm) {
return 1;
}
return 0;
})
.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));
const sortCertifiedFirst = (slice: DataSourcePanelColumn[]) =>
slice.sort((a, b) => (b?.is_certified ?? 0) - (a?.is_certified ?? 0));
export default function DataSourcePanel({
datasource,
@@ -143,7 +137,7 @@ export default function DataSourcePanel({
width,
}: Props) {
const [dropzones] = useContext(DropzoneContext);
const { columns: _columns, metrics, folders: _folders } = datasource;
const { columns: _columns, metrics } = datasource;
const allowedColumns = useMemo(() => {
const validators = Object.values(dropzones);
@@ -158,6 +152,21 @@ export default function DataSourcePanel({
);
}, [dropzones, _columns]);
// display temporal column first
const columns = useMemo(
() =>
[...allowedColumns].sort((col1, col2) => {
if (col1?.is_dttm && !col2?.is_dttm) {
return -1;
}
if (col2?.is_dttm && !col1?.is_dttm) {
return 1;
}
return 0;
}),
[allowedColumns],
);
const allowedMetrics = useMemo(() => {
const validators = Object.values(dropzones);
return metrics.filter(metric =>
@@ -167,15 +176,21 @@ export default function DataSourcePanel({
);
}, [dropzones, metrics]);
const hiddenColumnCount = _columns.length - allowedColumns.length;
const hiddenMetricCount = metrics.length - allowedMetrics.length;
const [showSaveDatasetModal, setShowSaveDatasetModal] = useState(false);
const [inputValue, setInputValue] = useState('');
const [showAllMetrics, setShowAllMetrics] = useState(false);
const [showAllColumns, setShowAllColumns] = useState(false);
const [collapseMetrics, setCollapseMetrics] = useState(false);
const [collapseColumns, setCollapseColumns] = useState(false);
const searchKeyword = useDebounceValue(inputValue, FAST_DEBOUNCE);
const filteredColumns = useMemo(() => {
if (!searchKeyword) {
return allowedColumns ?? [];
return columns ?? [];
}
return matchSorter(allowedColumns, searchKeyword, {
return matchSorter(columns, searchKeyword, {
keys: [
{
key: 'verbose_name',
@@ -196,7 +211,7 @@ export default function DataSourcePanel({
],
keepDiacritics: true,
});
}, [allowedColumns, searchKeyword]);
}, [columns, searchKeyword]);
const filteredMetrics = useMemo(() => {
if (!searchKeyword) {
@@ -229,15 +244,22 @@ export default function DataSourcePanel({
});
}, [allowedMetrics, searchKeyword]);
const sortedColumns = useMemo(
() => sortColumns(filteredColumns),
[filteredColumns],
const metricSlice = useMemo(
() =>
showAllMetrics
? filteredMetrics
: filteredMetrics?.slice?.(0, DEFAULT_MAX_METRICS_LENGTH),
[filteredMetrics, showAllMetrics],
);
const folders = useMemo(
const columnSlice = useMemo(
() =>
transformDatasourceWithFolders(filteredMetrics, sortedColumns, _folders),
[_folders, filteredMetrics, sortedColumns],
showAllColumns
? sortCertifiedFirst(filteredColumns)
: sortCertifiedFirst(
filteredColumns?.slice?.(0, DEFAULT_MAX_COLUMNS_LENGTH),
),
[filteredColumns, showAllColumns],
);
const showInfoboxCheck = () => {
@@ -302,17 +324,57 @@ export default function DataSourcePanel({
)}
<AutoSizer>
{({ height }: { height: number }) => (
<DatasourceItems
<List
width={width - BORDER_WIDTH}
height={height}
folders={folders}
/>
itemSize={ITEM_HEIGHT}
itemCount={
(collapseMetrics ? 0 : metricSlice?.length) +
(collapseColumns ? 0 : columnSlice.length) +
2 + // Each section header row
(collapseMetrics ? 0 : 2) +
(collapseColumns ? 0 : 2)
}
itemData={{
metricSlice,
columnSlice,
width,
totalMetrics: filteredMetrics.length,
totalColumns: filteredColumns.length,
showAllMetrics,
onShowAllMetricsChange: setShowAllMetrics,
showAllColumns,
onShowAllColumnsChange: setShowAllColumns,
collapseMetrics,
onCollapseMetricsChange: setCollapseMetrics,
collapseColumns,
onCollapseColumnsChange: setCollapseColumns,
hiddenMetricCount,
hiddenColumnCount,
}}
overscanCount={5}
>
{DatasourcePanelItem}
</List>
)}
</AutoSizer>
</div>
</>
),
[inputValue, datasourceIsSaveable, width, folders],
// eslint-disable-next-line react-hooks/exhaustive-deps
[
columnSlice,
inputValue,
filteredColumns.length,
filteredMetrics.length,
metricSlice,
showAllColumns,
showAllMetrics,
collapseMetrics,
collapseColumns,
datasourceIsSaveable,
width,
],
);
return (

View File

@@ -1,198 +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 { Metric } from '@superset-ui/core';
import { transformDatasourceWithFolders } from './transformDatasourceFolders';
import { DatasourceFolder, DatasourcePanelColumn } from './types';
const mockMetrics: Metric[] = [
{ metric_name: 'metric1', uuid: 'metric1-uuid', expression: 'SUM(col1)' },
{ metric_name: 'metric2', uuid: 'metric2-uuid', expression: 'AVG(col2)' },
{ metric_name: 'metric3', uuid: 'metric3-uuid', expression: 'COUNT(*)' },
];
const mockColumns: DatasourcePanelColumn[] = [
{ column_name: 'column1', uuid: 'column1-uuid', type: 'STRING' },
{ column_name: 'column2', uuid: 'column2-uuid', type: 'NUMERIC' },
{ column_name: 'column3', uuid: 'column3-uuid', type: 'DATETIME' },
];
test('transforms data into default folders when no folder config is provided', () => {
const result = transformDatasourceWithFolders(
mockMetrics,
mockColumns,
undefined,
);
expect(result).toHaveLength(2);
expect(result[0].id).toBe('metrics-default');
expect(result[0].name).toBe('Metrics');
expect(result[0].items).toHaveLength(3);
expect(result[0].items[0].uuid).toBe('metric1-uuid');
expect(result[0].items[0].type).toBe('metric');
expect(result[1].id).toBe('columns-default');
expect(result[1].name).toBe('Columns');
expect(result[1].items).toHaveLength(3);
expect(result[1].items[0].uuid).toBe('column1-uuid');
expect(result[1].items[0].type).toBe('column');
});
test('transforms data according to folder configuration', () => {
const folderConfig: DatasourceFolder[] = [
{
uuid: 'folder1',
type: 'folder',
name: 'Important Metrics',
description: 'Key metrics folder',
children: [
{ type: 'metric', uuid: 'metric1-uuid', name: 'metric1' },
{ type: 'metric', uuid: 'metric2-uuid', name: 'metric2' },
],
},
{
uuid: 'folder2',
type: 'folder',
name: 'Key Dimensions',
children: [{ type: 'column', uuid: 'column1-uuid', name: 'column1' }],
},
];
const result = transformDatasourceWithFolders(
mockMetrics,
mockColumns,
folderConfig,
);
// We expect 4 folders:
// 1. Important Metrics (from config)
// 2. Key Dimensions (from config)
// 3. Metrics (default for unassigned metrics)
// 4. Columns (default for unassigned columns)
expect(result).toHaveLength(4);
expect(result[0].id).toBe('folder1');
expect(result[0].name).toBe('Important Metrics');
expect(result[0].description).toBe('Key metrics folder');
expect(result[0].items).toHaveLength(2);
expect(result[0].items[0].uuid).toBe('metric1-uuid');
expect(result[1].id).toBe('folder2');
expect(result[1].name).toBe('Key Dimensions');
expect(result[1].items).toHaveLength(1);
expect(result[1].items[0].uuid).toBe('column1-uuid');
expect(result[2].id).toBe('metrics-default');
expect(result[2].items).toHaveLength(1);
expect(result[2].items[0].uuid).toBe('metric3-uuid');
expect(result[3].id).toBe('columns-default');
expect(result[3].items).toHaveLength(2);
});
test('handles nested folder structures', () => {
const folderConfig: DatasourceFolder[] = [
{
uuid: 'parent-folder',
type: 'folder',
name: 'Parent Folder',
children: [
{
uuid: 'child-folder',
type: 'folder',
name: 'Child Folder',
children: [{ type: 'metric', uuid: 'metric1-uuid', name: 'metric1' }],
},
{ type: 'column', uuid: 'column1-uuid', name: 'column1' },
],
},
];
const result = transformDatasourceWithFolders(
mockMetrics,
mockColumns,
folderConfig,
);
expect(result[0].id).toBe('parent-folder');
expect(result[0].name).toBe('Parent Folder');
expect(result[0].items).toHaveLength(1);
expect(result[0].subFolders).toHaveLength(1);
const childFolder = result[0].subFolders![0];
expect(childFolder.id).toBe('child-folder');
expect(childFolder.name).toBe('Child Folder');
expect(childFolder.items).toHaveLength(1);
expect(childFolder.parentId).toBe('parent-folder');
});
test('handles empty children arrays', () => {
const folderConfig: DatasourceFolder[] = [
{
uuid: 'empty-folder',
type: 'folder',
name: 'Empty Folder',
children: [],
},
];
const result = transformDatasourceWithFolders(
mockMetrics,
mockColumns,
folderConfig,
);
expect(result[0].id).toBe('empty-folder');
expect(result[0].name).toBe('Empty Folder');
expect(result[0].items).toHaveLength(0);
});
test('handles non-existent metric and column UUIDs in folder config', () => {
const folderConfig: DatasourceFolder[] = [
{
uuid: 'folder1',
type: 'folder',
name: 'Test Folder',
children: [
{
type: 'metric',
uuid: 'non-existent-metric',
name: 'Missing Metric',
},
{
type: 'column',
uuid: 'non-existent-column',
name: 'Missing Column',
},
{ type: 'metric', uuid: 'metric1-uuid', name: 'Existing Metric' },
],
},
];
const result = transformDatasourceWithFolders(
mockMetrics,
mockColumns,
folderConfig,
);
expect(result[0].id).toBe('folder1');
expect(result[0].items).toHaveLength(1);
expect(result[0].items[0].uuid).toBe('metric1-uuid');
});

View File

@@ -1,155 +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 { Metric, t } from '@superset-ui/core';
import {
ColumnItem,
DatasourceFolder,
DatasourcePanelColumn,
Folder,
MetricItem,
} from './types';
const transformToFolderStructure = (
metrics: MetricItem[],
columns: ColumnItem[],
folderConfig: DatasourceFolder[] | undefined,
): Folder[] => {
const metricsMap = new Map<string, MetricItem>();
const columnsMap = new Map<string, ColumnItem>();
const assignedMetricUuids = new Set<string>();
const assignedColumnUuids = new Set<string>();
metrics.forEach(metric => {
metricsMap.set(metric.uuid, metric);
});
columns.forEach(column => {
columnsMap.set(column.uuid, column);
});
const processFolder = (
datasourceFolder: DatasourceFolder,
parentId?: string,
): Folder => {
const folder: Folder = {
id: datasourceFolder.uuid,
name: datasourceFolder.name,
description: datasourceFolder.description,
isCollapsed: false,
items: [],
parentId,
};
if (datasourceFolder.children && datasourceFolder.children.length > 0) {
if (!folder.subFolders) {
folder.subFolders = [];
}
datasourceFolder.children.forEach(child => {
if (child.type === 'folder') {
folder.subFolders!.push(
processFolder(child as DatasourceFolder, folder.id),
);
} else if (child.type === 'metric') {
const metric = metricsMap.get(child.uuid);
if (metric) {
folder.items.push(metric);
assignedMetricUuids.add(metric.uuid);
}
} else if (child.type === 'column') {
const column = columnsMap.get(child.uuid);
if (column) {
folder.items.push(column);
assignedColumnUuids.add(column.uuid);
}
}
});
}
return folder;
};
if (!folderConfig) {
return [
{
id: 'metrics-default',
name: t('Metrics'),
isCollapsed: false,
items: metrics,
},
{
id: 'columns-default',
name: t('Columns'),
isCollapsed: false,
items: columns,
},
];
}
const folders = folderConfig.map(config => processFolder(config));
const unassignedMetrics = metrics.filter(
metric => !assignedMetricUuids.has(metric.uuid),
);
const unassignedColumns = columns.filter(
column => !assignedColumnUuids.has(column.uuid),
);
if (unassignedMetrics.length > 0) {
folders.push({
id: 'metrics-default',
name: t('Metrics'),
isCollapsed: false,
items: unassignedMetrics,
});
}
if (unassignedColumns.length > 0) {
folders.push({
id: 'columns-default',
name: t('Columns'),
isCollapsed: false,
items: unassignedColumns,
});
}
return folders;
};
export const transformDatasourceWithFolders = (
metrics: Metric[],
columns: DatasourcePanelColumn[],
folderConfig: DatasourceFolder[] | undefined,
): Folder[] => {
const metricsWithType: MetricItem[] = metrics.map(metric => ({
...metric,
type: 'metric',
}));
const columnsWithType: ColumnItem[] = columns.map(column => ({
...column,
type: 'column',
}));
return transformToFolderStructure(
metricsWithType,
columnsWithType,
folderConfig,
);
};

View File

@@ -35,55 +35,3 @@ export function isDatasourcePanelDndItem(
export function isSavedMetric(item: any): item is Metric {
return item?.metric_name;
}
export type DatasourcePanelColumn = {
uuid: string;
id?: number;
is_dttm?: boolean | null;
description?: string | null;
expression?: string | null;
is_certified?: number | null;
column_name?: string | null;
name?: string | null;
type?: string;
};
export type DatasourceFolder = {
uuid: string;
type: 'folder';
name: string;
description?: string;
children?: (
| DatasourceFolder
| { type: 'metric'; uuid: string; name: string }
| { type: 'column'; uuid: string; name: string }
)[];
};
export type MetricItem = Metric & {
type: 'metric';
};
export type ColumnItem = DatasourcePanelColumn & {
type: 'column';
};
export type FolderItem = MetricItem | ColumnItem;
export interface Folder {
id: string;
name: string;
description?: string;
isCollapsed: boolean;
items: FolderItem[];
subFolders?: Folder[];
parentId?: string;
}
export interface FlattenedItem {
type: 'header' | 'item' | 'divider';
folderId: string;
depth: number;
item?: FolderItem;
height: number;
}

View File

@@ -76,7 +76,7 @@ test('should only propagate dragging state when dragging the panel option', () =
const { container, getByText } = render(
<ExploreContainer>
<DatasourcePanelDragOption
value={{ metric_name: 'panel option', uuid: '1' }}
value={{ metric_name: 'panel option' }}
type={DndItemType.Metric}
/>
<OptionControlLabel

View File

@@ -202,11 +202,7 @@ test('cannot drop a column that is not part of the simple column selection', ()
type={DndItemType.Column}
/>
<DatasourcePanelDragOption
value={{
metric_name: 'metric_a',
expression: 'AGG(metric_a)',
uuid: '1',
}}
value={{ metric_name: 'metric_a', expression: 'AGG(metric_a)' }}
type={DndItemType.Metric}
/>
{setup({
@@ -381,11 +377,11 @@ describe('when disallow_adhoc_metrics is set', () => {
type={DndItemType.Column}
/>
<DatasourcePanelDragOption
value={{ metric_name: 'metric_a', uuid: '1' }}
value={{ metric_name: 'metric_a' }}
type={DndItemType.Metric}
/>
<DatasourcePanelDragOption
value={{ metric_name: 'avg__num', uuid: '2' }}
value={{ metric_name: 'avg__num' }}
type={DndItemType.AdhocMetricOption}
/>
{setup({

View File

@@ -334,7 +334,7 @@ test('cannot drop a duplicated item', () => {
const { getByTestId } = render(
<>
<DatasourcePanelDragOption
value={{ metric_name: 'metric_a', uuid: '1' }}
value={{ metric_name: 'metric_a' }}
type={DndItemType.Metric}
/>
<DndMetricSelect {...defaultProps} value={metricValues} multi />
@@ -362,7 +362,7 @@ test('can drop a saved metric when disallow_adhoc_metrics', () => {
const { getByTestId } = render(
<>
<DatasourcePanelDragOption
value={{ metric_name: 'metric_a', uuid: '1' }}
value={{ metric_name: 'metric_a' }}
type={DndItemType.Metric}
/>
<DndMetricSelect
@@ -395,15 +395,15 @@ test('cannot drop non-saved metrics when disallow_adhoc_metrics', () => {
const { getByTestId, getAllByTestId } = render(
<>
<DatasourcePanelDragOption
value={{ metric_name: 'metric_a', uuid: '1' }}
value={{ metric_name: 'metric_a' }}
type={DndItemType.Metric}
/>
<DatasourcePanelDragOption
value={{ metric_name: 'metric_c', uuid: '2' }}
value={{ metric_name: 'metric_c' }}
type={DndItemType.Metric}
/>
<DatasourcePanelDragOption
value={{ column_name: 'column_1', uuid: '3' }}
value={{ column_name: 'column_1' }}
type={DndItemType.Column}
/>
<DndMetricSelect

View File

@@ -18,7 +18,6 @@
*/
import { useCallback, useEffect, useMemo, useState } from 'react';
import { nanoid } from 'nanoid';
import {
ensureIsArray,
GenericDataType,
@@ -78,7 +77,6 @@ const coerceMetrics = (
return {
metric_name: metric,
error_text: t('This metric might be incompatible with current dataset'),
uuid: nanoid(),
};
}
if (!isDictionaryForAdhocMetric(metric)) {

View File

@@ -51,10 +51,7 @@ describe('controlUtils', () => {
id: 1,
type: DatasourceType.Table,
columns: [{ column_name: 'a' }],
metrics: [
{ metric_name: 'first', uuid: '1' },
{ metric_name: 'second', uuid: '2' },
],
metrics: [{ metric_name: 'first' }, { metric_name: 'second' }],
column_formats: {},
currency_formats: {},
verbose_map: {},

View File

@@ -33,7 +33,7 @@ const sampleDatasource: Dataset = {
{ column_name: 'sample_column_3' },
{ column_name: 'sample_column_4' },
],
metrics: [{ metric_name: 'saved_metric_2', uuid: '1' }],
metrics: [{ metric_name: 'saved_metric_2' }],
column_formats: {},
currency_formats: {},
verbose_map: {},

View File

@@ -133,10 +133,7 @@ export const exploreInitialData: ExplorePageInitialData = {
id: 8,
type: DatasourceType.Table,
columns: [{ column_name: 'a' }],
metrics: [
{ metric_name: 'first', uuid: '1' },
{ metric_name: 'second', uuid: '2' },
],
metrics: [{ metric_name: 'first' }, { metric_name: 'second' }],
column_formats: {},
currency_formats: {},
verbose_map: {},

View File

@@ -41,7 +41,7 @@ export type ColumnObject = {
type MetricObject = {
id: number;
uuid: string;
uuid: number;
expression?: string;
description?: string;
metric_name: string;

View File

@@ -249,11 +249,11 @@ class SyncPermissionsCommand(BaseCommand):
self, catalog: str | None, schemas: Iterable[str]
) -> None:
# rename existing catalog permission
new_catalog_perm_name = security_manager.get_catalog_perm(
self.db_connection.name,
catalog,
)
if catalog:
new_catalog_perm_name = security_manager.get_catalog_perm(
self.db_connection.name,
catalog,
)
new_catalog_vm = add_vm(db.session, security_manager, new_catalog_perm_name)
perm = security_manager.get_catalog_perm(
self.old_db_connection_name,

View File

@@ -17,13 +17,13 @@
import logging
from collections import Counter
from functools import partial
from typing import Any, cast, Optional
from typing import Any, Optional
from flask_appbuilder.models.sqla import Model
from marshmallow import ValidationError
from sqlalchemy.exc import SQLAlchemyError
from superset import is_feature_enabled, security_manager
from superset import security_manager
from superset.commands.base import BaseCommand, UpdateMixin
from superset.commands.dataset.exceptions import (
DatabaseChangeValidationError,
@@ -39,9 +39,8 @@ from superset.commands.dataset.exceptions import (
DatasetNotFoundError,
DatasetUpdateFailedError,
)
from superset.connectors.sqla.models import SqlaTable, SqlMetric, TableColumn
from superset.connectors.sqla.models import SqlaTable
from superset.daos.dataset import DatasetDAO
from superset.datasets.schemas import FolderSchema
from superset.exceptions import SupersetSecurityException
from superset.sql_parse import Table
from superset.utils.decorators import on_error, transaction
@@ -128,30 +127,16 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
except ValidationError as ex:
exceptions.append(ex)
self._validate_semantics(exceptions)
if exceptions:
raise DatasetInvalidError(exceptions=exceptions)
def _validate_semantics(self, exceptions: list[ValidationError]) -> None:
# we know we have a valid model
self._model = cast(SqlaTable, self._model)
# Validate columns
if columns := self._properties.get("columns"):
self._validate_columns(columns, exceptions)
# Validate metrics
if metrics := self._properties.get("metrics"):
self._validate_metrics(metrics, exceptions)
if folders := self._properties.get("folders"):
try:
validate_folders(folders, self._model.metrics, self._model.columns)
except ValidationError as ex:
exceptions.append(ex)
# dump schema to convert UUID to string
schema = FolderSchema(many=True)
self._properties["folders"] = schema.dump(folders)
if exceptions:
raise DatasetInvalidError(exceptions=exceptions)
def _validate_columns(
self, columns: list[dict[str, Any]], exceptions: list[ValidationError]
@@ -204,60 +189,3 @@ class UpdateDatasetCommand(UpdateMixin, BaseCommand):
if count > 1
]
return duplicates
def validate_folders( # noqa: C901
folders: list[FolderSchema],
metrics: list[SqlMetric],
columns: list[TableColumn],
) -> None:
"""
Additional folder validation.
The marshmallow schema will validate the folder structure, but we still need to
check that UUIDs are valid, names are unique and not reserved, and that there are
no cycles.
"""
if not is_feature_enabled("DATASET_FOLDERS"):
raise ValidationError("Dataset folders are not enabled")
existing = {
"metric": {metric.uuid: metric.metric_name for metric in metrics},
"column": {column.uuid: column.column_name for column in columns},
}
queue: list[tuple[FolderSchema, list[str]]] = [(folder, []) for folder in folders]
seen_uuids = set()
seen_fqns = set() # fully qualified names
while queue:
obj, path = queue.pop(0)
uuid, name, type = obj["uuid"], obj["name"], obj["type"]
if uuid in path:
raise ValidationError(f"Cycle detected: {uuid} appears in its ancestry")
if uuid in seen_uuids:
raise ValidationError(f"Duplicate UUID in folder structure: {uuid}")
seen_uuids.add(uuid)
# folders can have duplicate name as long as they're not siblings
fqn = tuple(path + [name])
if type == "folder" and fqn in seen_fqns:
raise ValidationError(f"Duplicate folder name: {name}")
seen_fqns.add(fqn)
if type == "folder" and name.lower() in {
"metrics",
"columns",
}:
raise ValidationError(f"Folder cannot have name '{name}'")
if type in {"metric", "column"}:
if uuid not in existing[type]:
raise ValidationError(f"Invalid UUID for {type} '{name}': {uuid}")
if name != existing[type][uuid]:
raise ValidationError(f"Mismatched name '{name}' for UUID '{uuid}'")
if children := obj.get("children"):
path.append(uuid)
queue.extend((folder, path) for folder in children)

View File

@@ -561,9 +561,6 @@ DEFAULT_FEATURE_FLAGS: dict[str, bool] = {
"SLACK_ENABLE_AVATARS": False,
# Allow users to optionally specify date formats in email subjects, which will be parsed if enabled. # noqa: E501
"DATE_FORMAT_IN_EMAIL_SUBJECT": False,
# Allow metrics and columns to be grouped into (potentially nested) folders in the
# chart builder
"DATASET_FOLDERS": False,
}
# ------------------------------
@@ -820,6 +817,8 @@ EXPLORE_FORM_DATA_CACHE_CONFIG: CacheConfig = {
STORE_CACHE_KEYS_IN_METADATA_DB = False
# CORS Options
# NOTE: enabling this requires installing the cors-related python dependencies
# `pip install .[cors]` or `pip install apache-superset[cors]`, depending
ENABLE_CORS = False
CORS_OPTIONS: dict[Any, Any] = {}

View File

@@ -69,7 +69,6 @@ from sqlalchemy.sql import column, ColumnElement, literal_column, table
from sqlalchemy.sql.elements import ColumnClause, TextClause
from sqlalchemy.sql.expression import Label
from sqlalchemy.sql.selectable import Alias, TableClause
from sqlalchemy.types import JSON
from superset import app, db, is_feature_enabled, security_manager
from superset.commands.dataset.exceptions import DatasetNotFoundError
@@ -401,7 +400,6 @@ class BaseDatasource(AuditMixinNullable, ImportExportMixin): # pylint: disable=
# one to many
"columns": [o.data for o in self.columns],
"metrics": [o.data for o in self.metrics],
"folders": self.folders,
# TODO deprecate, move logic to JS
"order_by_choices": self.order_by_choices,
"owners": [owner.id for owner in self.owners],
@@ -1020,7 +1018,6 @@ class TableColumn(AuditMixinNullable, ImportExportMixin, CertificationMixin, Mod
"filterable",
"groupby",
"id",
"uuid",
"is_certified",
"is_dttm",
"python_date_format",
@@ -1068,7 +1065,7 @@ class SqlMetric(AuditMixinNullable, ImportExportMixin, CertificationMixin, Model
"extra",
"warning_text",
]
update_from_object_fields = [s for s in export_fields if s != "table_id"]
update_from_object_fields = list(s for s in export_fields if s != "table_id") # noqa: C400
export_parent = "table"
def __repr__(self) -> str:
@@ -1120,7 +1117,6 @@ class SqlMetric(AuditMixinNullable, ImportExportMixin, CertificationMixin, Model
"description",
"expression",
"id",
"uuid",
"is_certified",
"metric_name",
"warning_markdown",
@@ -1197,7 +1193,6 @@ class SqlaTable(
extra = Column(Text)
normalize_columns = Column(Boolean, default=False)
always_filter_main_dttm = Column(Boolean, default=False)
folders = Column(JSON, nullable=True)
baselink = "tablemodelview"

View File

@@ -831,6 +831,7 @@ class ImportV1DatabaseExtraSchema(Schema):
disable_drill_to_detail = fields.Boolean(required=False)
allow_multi_catalog = fields.Boolean(required=False)
version = fields.String(required=False, allow_none=True)
schema_options = fields.Dict(keys=fields.Str(), values=fields.Raw())
class ImportV1DatabaseSchema(Schema):

View File

@@ -194,10 +194,8 @@ class DatasetRestApi(BaseSupersetModelRestApi):
"metrics.id",
"metrics.metric_name",
"metrics.metric_type",
"metrics.uuid",
"metrics.verbose_name",
"metrics.warning_text",
"folders",
"datasource_type",
"url",
"extra",
@@ -623,11 +621,9 @@ class DatasetRestApi(BaseSupersetModelRestApi):
return self.response(201, id=new_model.id, result=item)
except DatasetInvalidError as ex:
return self.response_422(
message=(
ex.normalized_messages()
if isinstance(ex, ValidationError)
else str(ex)
)
message=ex.normalized_messages()
if isinstance(ex, ValidationError)
else str(ex)
)
except DatasetCreateFailedError as ex:
logger.error(
@@ -1180,16 +1176,14 @@ class DatasetRestApi(BaseSupersetModelRestApi):
def render_item_list(item_list: list[dict[str, Any]]) -> list[dict[str, Any]]:
return [
(
{
**item,
"rendered_expression": processor.process_template(
item["expression"]
),
}
if item.get("expression")
else item
)
{
**item,
"rendered_expression": processor.process_template(
item["expression"]
),
}
if item.get("expression")
else item
for item in item_list
]

View File

@@ -20,7 +20,7 @@ from typing import Any
from dateutil.parser import isoparse
from flask_babel import lazy_gettext as _
from marshmallow import fields, pre_load, Schema, ValidationError
from marshmallow.validate import Length, OneOf
from marshmallow.validate import Length
from superset.exceptions import SupersetMarshmallowValidationError
from superset.utils import json
@@ -88,18 +88,6 @@ class DatasetMetricsPutSchema(Schema):
uuid = fields.UUID(allow_none=True)
class FolderSchema(Schema):
uuid = fields.UUID()
type = fields.String(
required=False,
validate=OneOf(["metric", "column", "folder"]),
)
name = fields.String(required=True, validate=Length(1, 250))
description = fields.String(allow_none=True, validate=Length(0, 1000))
# folder can contain metrics, columns, and subfolders:
children = fields.List(fields.Nested(lambda: FolderSchema()), allow_none=True)
class DatasetPostSchema(Schema):
database = fields.Integer(required=True)
catalog = fields.String(allow_none=True, validate=Length(0, 250))
@@ -133,7 +121,6 @@ class DatasetPutSchema(Schema):
owners = fields.List(fields.Integer())
columns = fields.List(fields.Nested(DatasetColumnsPutSchema))
metrics = fields.List(fields.Nested(DatasetMetricsPutSchema))
folders = fields.List(fields.Nested(FolderSchema), required=False)
extra = fields.String(allow_none=True)
is_managed_externally = fields.Boolean(allow_none=True, dump_default=False)
external_url = fields.String(allow_none=True)

View File

@@ -188,8 +188,10 @@ class GSheetsEngineSpec(ShillelaghEngineSpec):
"""
Remove `oauth2_client_info` from `encrypted_extra`.
"""
if "oauth2_client_info" in params.get("encrypted_extra", {}):
del params["encrypted_extra"]["oauth2_client_info"]
ShillelaghEngineSpec.update_params_from_encrypted_extra(database, params)
if "oauth2_client_info" in params:
del params["oauth2_client_info"]
@classmethod
def get_parameters_from_uri(

View File

@@ -0,0 +1,73 @@
# 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.
slice_name: Items Sold
description: null
certified_by: null
certification_details: null
viz_type: big_number
params:
datasource: 21__table
viz_type: big_number
slice_id: 115
x_axis: order_date
metric:
aggregate: SUM
column:
column_name: quantity_ordered
description: null
expression: null
filterable: true
groupby: true
id: 914
is_dttm: false
python_date_format: null
type: BIGINT
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: false
isNew: false
label: SUM(Sales)
optionName: metric_twq59hf4ej_g70qjfmehsq
sqlExpression: null
adhoc_filters:
- clause: WHERE
comparator: No filter
expressionType: SIMPLE
operator: TEMPORAL_RANGE
subject: order_date
show_trend_line: true
start_y_axis_at_zero: true
color_picker:
a: 1
b: 135
g: 122
r: 0
header_font_size: 0.4
subheader_font_size: 0.15
y_axis_format: SMART_NUMBER
time_format: smart_date
rolling_type: cumsum
extra_form_data: {}
dashboards:
- 9
query_context: '{"datasource":{"id":21,"type":"table"},"force":false,"queries":[{"filters":[{"col":"order_date","op":"TEMPORAL_RANGE","val":"No
filter"}],"extras":{"having":"","where":""},"applied_time_extras":{},"columns":[{"columnType":"BASE_AXIS","sqlExpression":"order_date","label":"order_date","expressionType":"SQL"}],"metrics":[{"aggregate":"SUM","column":{"column_name":"quantity_ordered","description":null,"expression":null,"filterable":true,"groupby":true,"id":914,"is_dttm":false,"python_date_format":null,"type":"BIGINT","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"SUM(Sales)","optionName":"metric_twq59hf4ej_g70qjfmehsq","sqlExpression":null}],"annotation_layers":[],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"post_processing":[{"operation":"pivot","options":{"index":["order_date"],"columns":[],"aggregates":{"SUM(Sales)":{"operator":"mean"}},"drop_missing_columns":true}},{"operation":"cum","options":{"operator":"sum","columns":{"SUM(Sales)":"SUM(Sales)"}}},{"operation":"flatten"}]}],"form_data":{"datasource":"21__table","viz_type":"big_number","slice_id":115,"x_axis":"order_date","metric":{"aggregate":"SUM","column":{"column_name":"quantity_ordered","description":null,"expression":null,"filterable":true,"groupby":true,"id":914,"is_dttm":false,"python_date_format":null,"type":"BIGINT","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"SUM(Sales)","optionName":"metric_twq59hf4ej_g70qjfmehsq","sqlExpression":null},"adhoc_filters":[{"clause":"WHERE","comparator":"No
filter","expressionType":"SIMPLE","operator":"TEMPORAL_RANGE","subject":"order_date"}],"show_trend_line":true,"start_y_axis_at_zero":true,"color_picker":{"a":1,"b":135,"g":122,"r":0},"header_font_size":0.4,"subheader_font_size":0.15,"y_axis_format":"SMART_NUMBER","time_format":"smart_date","rolling_type":"cumsum","extra_form_data":{},"dashboards":[9],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}'
cache_timeout: null
uuid: c3d643cd-fd6f-4659-a5b7-59402487a8d0
version: 1.0.0
dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005

View File

@@ -0,0 +1,80 @@
# 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.
slice_name: Items by Product Line
description: null
certified_by: null
certification_details: null
viz_type: table
params:
datasource: 21__table
viz_type: table
slice_id: 111
query_mode: aggregate
groupby:
- product_line
temporal_columns_lookup:
order_date: true
metrics:
- aggregate: SUM
column:
column_name: quantity_ordered
description: null
expression: null
filterable: true
groupby: true
id: 914
is_dttm: false
optionName: _col_QuantityOrdered
python_date_format: null
type: BIGINT
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: true
isNew: false
label: '# of Products Sold'
optionName: metric_skdbciwba6g_z1r5w1pxlqj
sqlExpression: null
all_columns: []
percent_metrics: null
adhoc_filters:
- clause: WHERE
subject: order_date
operator: TEMPORAL_RANGE
comparator: No filter
expressionType: SIMPLE
order_by_cols: []
row_limit: null
order_desc: true
table_timestamp_format: smart_date
allow_render_html: true
show_cell_bars: true
color_pn: true
comparison_color_scheme: Green
comparison_type: values
extra_form_data: {}
dashboards:
- 9
query_context: '{"datasource":{"id":21,"type":"table"},"force":false,"queries":[{"filters":[{"col":"order_date","op":"TEMPORAL_RANGE","val":"No
filter"}],"extras":{"having":"","where":""},"applied_time_extras":{},"columns":["product_line"],"metrics":[{"aggregate":"SUM","column":{"column_name":"quantity_ordered","description":null,"expression":null,"filterable":true,"groupby":true,"id":914,"is_dttm":false,"optionName":"_col_QuantityOrdered","python_date_format":null,"type":"BIGINT","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":true,"isNew":false,"label":"#
of Products Sold","optionName":"metric_skdbciwba6g_z1r5w1pxlqj","sqlExpression":null}],"orderby":[[{"aggregate":"SUM","column":{"column_name":"quantity_ordered","description":null,"expression":null,"filterable":true,"groupby":true,"id":914,"is_dttm":false,"optionName":"_col_QuantityOrdered","python_date_format":null,"type":"BIGINT","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":true,"isNew":false,"label":"#
of Products Sold","optionName":"metric_skdbciwba6g_z1r5w1pxlqj","sqlExpression":null},false]],"annotation_layers":[],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"post_processing":[],"time_offsets":[]}],"form_data":{"datasource":"21__table","viz_type":"table","slice_id":111,"query_mode":"aggregate","groupby":["product_line"],"temporal_columns_lookup":{"order_date":true},"metrics":[{"aggregate":"SUM","column":{"column_name":"quantity_ordered","description":null,"expression":null,"filterable":true,"groupby":true,"id":914,"is_dttm":false,"optionName":"_col_QuantityOrdered","python_date_format":null,"type":"BIGINT","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":true,"isNew":false,"label":"#
of Products Sold","optionName":"metric_skdbciwba6g_z1r5w1pxlqj","sqlExpression":null}],"all_columns":[],"percent_metrics":null,"adhoc_filters":[{"clause":"WHERE","subject":"order_date","operator":"TEMPORAL_RANGE","comparator":"No
filter","expressionType":"SIMPLE"}],"order_by_cols":[],"row_limit":null,"order_desc":true,"table_timestamp_format":"smart_date","allow_render_html":true,"show_cell_bars":true,"color_pn":true,"comparison_color_scheme":"Green","comparison_type":"values","extra_form_data":{},"dashboards":[9],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}'
cache_timeout: null
uuid: b8b7ca30-6291-44b0-bc64-ba42e2892b86
version: 1.0.0
dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005

View File

@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
slice_name: Number of Deals (for each Combination)
description: null
certified_by: null
certification_details: null
viz_type: heatmap_v2
params:
adhoc_filters: []
@@ -42,10 +45,12 @@ params:
viz_type: heatmap_v2
xscale_interval: null
value_bounds:
- null
- null
- null
- null
y_axis_format: SMART_NUMBER
yscale_interval: null
annotation_layers: []
query_context: null
cache_timeout: null
uuid: bd20fc69-dd51-46c1-99b5-09e37a434bf1
version: 1.0.0

View File

@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
slice_name: Overall Sales (By Product Line)
description: null
certified_by: null
certification_details: null
viz_type: pie
params:
adhoc_filters: []
@@ -61,6 +64,12 @@ params:
time_range: No filter
url_params: {}
viz_type: pie
annotation_layers: []
query_context: '{"datasource":{"id":21,"type":"table"},"force":false,"queries":[{"time_range":"No
filter","granularity":"order_date","filters":[],"extras":{"having":"","where":""},"applied_time_extras":{},"columns":["product_line"],"metrics":[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"(Sales)","optionName":"metric_3sk6pfj3m7i_64h77bs4sly","sqlExpression":null}],"annotation_layers":[],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{}}],"form_data":{"adhoc_filters":[],"annotation_layers":[],"color_scheme":"supersetColors","datasource":"21__table","donut":true,"granularity_sqla":"order_date","groupby":["product_line"],"innerRadius":41,"label_line":true,"label_type":"key","labels_outside":true,"metric":{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"(Sales)","optionName":"metric_3sk6pfj3m7i_64h77bs4sly","sqlExpression":null},"number_format":"SMART_NUMBER","outerRadius":65,"queryFields":{"groupby":"groupby","metric":"metrics"},"row_limit":null,"show_labels":true,"show_labels_threshold":2,"show_legend":false,"slice_id":120,"time_range":"No
filter","url_params":{},"viz_type":"pie","force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}'
cache_timeout: null
uuid: 09c497e0-f442-1121-c9e7-671e37750424
version: 1.0.0

View File

@@ -15,60 +15,84 @@
# specific language governing permissions and limitations
# under the License.
slice_name: Proportion of Revenue by Product Line
description: null
certified_by: null
certification_details: null
viz_type: echarts_area
params:
adhoc_filters: []
annotation_layers: []
bottom_margin: auto
color_scheme: supersetColors
comparison_type: values
contribution: true
datasource: 23__table
granularity_sqla: order_date
groupby:
- product_line
label_colors: {}
line_interpolation: linear
metrics:
- aggregate: SUM
column:
column_name: sales
description: null
expression: null
filterable: true
groupby: true
id: 917
is_dttm: false
optionName: _col_Sales
python_date_format: null
type: DOUBLE PRECISION
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: false
isNew: false
label: (Sales)
optionName: metric_3is69ofceho_6d0ezok7ry6
sqlExpression: null
order_desc: true
queryFields:
groupby: groupby
metrics: metrics
rich_tooltip: true
rolling_type: None
row_limit: null
show_brush: auto
show_legend: true
stacked_style: stack
time_grain_sqla: P1M
time_range: "2003-01-01T00:00:00 : 2005-06-01T00:00:00"
url_params: {}
datasource: 21__table
viz_type: echarts_area
x_axis_format: smart_date
x_ticks_layout: auto
y_axis_bounds:
- null
- null
slice_id: 116
x_axis: order_date
time_grain_sqla: P1M
x_axis_sort_asc: true
x_axis_sort_series: name
x_axis_sort_series_ascending: true
metrics:
- aggregate: SUM
column:
column_name: sales
description: null
expression: null
filterable: true
groupby: true
id: 917
is_dttm: false
optionName: _col_Sales
python_date_format: null
type: DOUBLE PRECISION
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: false
isNew: false
label: (Sales)
optionName: metric_3is69ofceho_6d0ezok7ry6
sqlExpression: null
groupby:
- product_line
adhoc_filters:
- clause: WHERE
subject: order_date
operator: TEMPORAL_RANGE
comparator: '2003-01-01T00:00:00 : 2005-06-01T00:00:00'
expressionType: SIMPLE
row_limit: null
truncate_metric: true
show_empty_columns: true
rolling_type: null
comparison_type: values
annotation_layers: []
forecastPeriods: 10
forecastInterval: 0.8
x_axis_title_margin: 15
y_axis_title_margin: 15
y_axis_title_position: Left
sort_series_type: sum
color_scheme: supersetColors
time_shift_color: true
seriesType: line
opacity: 0.2
stack: Stack
only_total: true
markerSize: 6
show_legend: true
legendType: scroll
legendOrientation: top
x_axis_time_format: smart_date
rich_tooltip: true
showTooltipTotal: true
tooltipTimeFormat: smart_date
y_axis_format: SMART_NUMBER
truncateXAxis: true
extra_form_data: {}
dashboards:
- 9
query_context: '{"datasource":{"id":21,"type":"table"},"force":false,"queries":[{"filters":[{"col":"order_date","op":"TEMPORAL_RANGE","val":"2003-01-01T00:00:00
: 2005-06-01T00:00:00"}],"extras":{"time_grain_sqla":"P1M","having":"","where":""},"applied_time_extras":{},"columns":[{"timeGrain":"P1M","columnType":"BASE_AXIS","sqlExpression":"order_date","label":"order_date","expressionType":"SQL"},"product_line"],"metrics":[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"(Sales)","optionName":"metric_3is69ofceho_6d0ezok7ry6","sqlExpression":null}],"orderby":[[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"(Sales)","optionName":"metric_3is69ofceho_6d0ezok7ry6","sqlExpression":null},false]],"annotation_layers":[],"series_columns":["product_line"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["order_date"],"columns":["product_line"],"aggregates":{"(Sales)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"(Sales)":null},"level":0,"inplace":true}},{"operation":"flatten"}]}],"form_data":{"datasource":"21__table","viz_type":"echarts_area","slice_id":116,"x_axis":"order_date","time_grain_sqla":"P1M","x_axis_sort_asc":true,"x_axis_sort_series":"name","x_axis_sort_series_ascending":true,"metrics":[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"(Sales)","optionName":"metric_3is69ofceho_6d0ezok7ry6","sqlExpression":null}],"groupby":["product_line"],"adhoc_filters":[{"clause":"WHERE","subject":"order_date","operator":"TEMPORAL_RANGE","comparator":"2003-01-01T00:00:00
: 2005-06-01T00:00:00","expressionType":"SIMPLE"}],"row_limit":null,"truncate_metric":true,"show_empty_columns":true,"rolling_type":null,"comparison_type":"values","annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"x_axis_title_margin":15,"y_axis_title_margin":15,"y_axis_title_position":"Left","sort_series_type":"sum","color_scheme":"supersetColors","time_shift_color":true,"seriesType":"line","opacity":0.2,"stack":"Stack","only_total":true,"markerSize":6,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","rich_tooltip":true,"showTooltipTotal":true,"tooltipTimeFormat":"smart_date","y_axis_format":"SMART_NUMBER","truncateXAxis":true,"extra_form_data":{},"dashboards":[9],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}'
cache_timeout: null
uuid: 08aff161-f60c-4cb3-a225-dc9b1140d2e3
version: 1.0.0

View File

@@ -15,71 +15,86 @@
# specific language governing permissions and limitations
# under the License.
slice_name: Quarterly Sales
description: null
certified_by: null
certification_details: null
viz_type: echarts_timeseries_bar
params:
adhoc_filters: []
annotation_layers: []
bottom_margin: auto
color_scheme: supersetColors
comparison_type: null
datasource: 23__table
granularity_sqla: order_date
groupby: []
label_colors:
Classic Cars: "#5AC189"
Motorcycles: "#666666"
Planes: "#FCC700"
QuantityOrdered: "#454E7C"
SUM(Sales): "#1FA8C9"
Ships: "#A868B7"
Trains: "#3CCCCB"
Trucks and Buses: "#E04355"
Vintage Cars: "#FF7F44"
left_margin: auto
line_interpolation: linear
metrics:
- aggregate: SUM
column:
column_name: sales
description: null
expression: null
filterable: true
groupby: true
id: 917
is_dttm: false
optionName: _col_Sales
python_date_format: null
type: DOUBLE PRECISION
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: false
isNew: false
label: SUM(Sales)
optionName: metric_tjn8bh6y44_7o4etwsqhal
sqlExpression: null
order_desc: true
queryFields:
groupby: groupby
metrics: metrics
rich_tooltip: true
rolling_type: null
row_limit: 10000
show_brush: auto
show_legend: false
slice_id: 668
time_compare: null
time_grain_sqla: P3M
time_range: No filter
url_params: {}
datasource: 21__table
viz_type: echarts_timeseries_bar
x_axis_format: "%m/%d/%Y"
x_axis_label: Quarter starting
x_ticks_layout: auto
y_axis_bounds:
- null
- null
slice_id: 118
x_axis: order_date
time_grain_sqla: P3M
x_axis_sort_asc: true
x_axis_sort_series: name
x_axis_sort_series_ascending: true
metrics:
- aggregate: SUM
column:
column_name: sales
description: null
expression: null
filterable: true
groupby: true
id: 917
is_dttm: false
optionName: _col_Sales
python_date_format: null
type: DOUBLE PRECISION
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: false
isNew: false
label: SUM(Sales)
optionName: metric_tjn8bh6y44_7o4etwsqhal
sqlExpression: null
groupby:
- status
adhoc_filters:
- clause: WHERE
subject: order_date
operator: TEMPORAL_RANGE
comparator: No filter
expressionType: SIMPLE
row_limit: 10000
truncate_metric: true
show_empty_columns: true
rolling_type: null
time_compare: null
comparison_type: null
annotation_layers: []
forecastPeriods: 10
forecastInterval: 0.8
orientation: vertical
x_axis_title_margin: 15
y_axis_title_margin: 15
y_axis_title_position: Left
sort_series_type: sum
color_scheme: supersetColors
time_shift_color: true
stack: Stack
only_total: true
show_legend: true
legendType: scroll
legendOrientation: top
x_axis_time_format: smart_date
y_axis_format: null
y_axis_label: Total Sales
y_axis_bounds:
- null
- null
truncateXAxis: true
rich_tooltip: true
showTooltipTotal: true
tooltipTimeFormat: smart_date
extra_form_data: {}
dashboards:
- 9
query_context: '{"datasource":{"id":21,"type":"table"},"force":false,"queries":[{"filters":[{"col":"order_date","op":"TEMPORAL_RANGE","val":"No
filter"}],"extras":{"time_grain_sqla":"P3M","having":"","where":""},"applied_time_extras":{},"columns":[{"timeGrain":"P3M","columnType":"BASE_AXIS","sqlExpression":"order_date","label":"order_date","expressionType":"SQL"},"status"],"metrics":[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"SUM(Sales)","optionName":"metric_tjn8bh6y44_7o4etwsqhal","sqlExpression":null}],"orderby":[[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"SUM(Sales)","optionName":"metric_tjn8bh6y44_7o4etwsqhal","sqlExpression":null},false]],"annotation_layers":[],"row_limit":10000,"series_columns":["status"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["order_date"],"columns":["status"],"aggregates":{"SUM(Sales)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"SUM(Sales)":null},"level":0,"inplace":true}},{"operation":"flatten"}]}],"form_data":{"datasource":"21__table","viz_type":"echarts_timeseries_bar","slice_id":118,"x_axis":"order_date","time_grain_sqla":"P3M","x_axis_sort_asc":true,"x_axis_sort_series":"name","x_axis_sort_series_ascending":true,"metrics":[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"SUM(Sales)","optionName":"metric_tjn8bh6y44_7o4etwsqhal","sqlExpression":null}],"groupby":["status"],"adhoc_filters":[{"clause":"WHERE","subject":"order_date","operator":"TEMPORAL_RANGE","comparator":"No
filter","expressionType":"SIMPLE"}],"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"rolling_type":null,"time_compare":null,"comparison_type":null,"annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title_margin":15,"y_axis_title_margin":15,"y_axis_title_position":"Left","sort_series_type":"sum","color_scheme":"supersetColors","time_shift_color":true,"stack":"Stack","only_total":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","y_axis_format":null,"y_axis_bounds":[null,null],"truncateXAxis":true,"rich_tooltip":true,"showTooltipTotal":true,"tooltipTimeFormat":"smart_date","extra_form_data":{},"dashboards":[9],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}'
cache_timeout: null
uuid: 692aca26-a526-85db-c94c-411c91cc1077
version: 1.0.0

View File

@@ -0,0 +1,103 @@
# 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.
slice_name: Quarterly Sales (By Product Line)
description: null
certified_by: null
certification_details: null
viz_type: echarts_timeseries_bar
params:
datasource: 21__table
viz_type: echarts_timeseries_bar
slice_id: 113
x_axis: order_date
time_grain_sqla: P3M
metrics:
- aggregate: SUM
column:
column_name: sales
description: null
expression: null
filterable: true
groupby: true
id: 917
is_dttm: false
optionName: _col_Sales
python_date_format: null
type: DOUBLE PRECISION
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: false
isNew: false
label: SUM(Sales)
optionName: metric_tjn8bh6y44_7o4etwsqhal
sqlExpression: null
groupby:
- product_line
adhoc_filters:
- expressionType: SIMPLE
subject: order_date
operator: TEMPORAL_RANGE
comparator: No filter
clause: WHERE
sqlExpression: null
isExtra: false
isNew: false
datasourceWarning: false
filterOptionName: filter_skx80xwzof_2l0t7nomekl
order_desc: true
row_limit: 10000
truncate_metric: true
show_empty_columns: true
rolling_type: null
time_compare: null
comparison_type: null
annotation_layers: []
forecastPeriods: 10
forecastInterval: 0.8
orientation: vertical
x_axis_title_margin: 15
y_axis_title_margin: 15
y_axis_title_position: Left
sort_series_type: sum
color_scheme: supersetColors
time_shift_color: true
only_total: true
show_legend: true
legendType: scroll
legendOrientation: top
x_axis_time_format: smart_date
y_axis_format: null
y_axis_bounds:
- null
- null
truncateXAxis: true
rich_tooltip: true
showTooltipTotal: true
tooltipTimeFormat: smart_date
extra_form_data: {}
dashboards:
- 9
query_context: '{"datasource":{"id":21,"type":"table"},"force":false,"queries":[{"filters":[{"col":"order_date","op":"TEMPORAL_RANGE","val":"No
filter"}],"extras":{"time_grain_sqla":"P3M","having":"","where":""},"applied_time_extras":{},"columns":[{"timeGrain":"P3M","columnType":"BASE_AXIS","sqlExpression":"order_date","label":"order_date","expressionType":"SQL"},"product_line"],"metrics":[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"SUM(Sales)","optionName":"metric_tjn8bh6y44_7o4etwsqhal","sqlExpression":null}],"orderby":[[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"SUM(Sales)","optionName":"metric_tjn8bh6y44_7o4etwsqhal","sqlExpression":null},false]],"annotation_layers":[],"row_limit":10000,"series_columns":["product_line"],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"time_offsets":[],"post_processing":[{"operation":"pivot","options":{"index":["order_date"],"columns":["product_line"],"aggregates":{"SUM(Sales)":{"operator":"mean"}},"drop_missing_columns":false}},{"operation":"rename","options":{"columns":{"SUM(Sales)":null},"level":0,"inplace":true}},{"operation":"flatten"}]}],"form_data":{"datasource":"21__table","viz_type":"echarts_timeseries_bar","slice_id":113,"x_axis":"order_date","time_grain_sqla":"P3M","metrics":[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"SUM(Sales)","optionName":"metric_tjn8bh6y44_7o4etwsqhal","sqlExpression":null}],"groupby":["product_line"],"adhoc_filters":[{"expressionType":"SIMPLE","subject":"order_date","operator":"TEMPORAL_RANGE","comparator":"No
filter","clause":"WHERE","sqlExpression":null,"isExtra":false,"isNew":false,"datasourceWarning":false,"filterOptionName":"filter_skx80xwzof_2l0t7nomekl"}],"order_desc":true,"row_limit":10000,"truncate_metric":true,"show_empty_columns":true,"rolling_type":null,"time_compare":null,"comparison_type":null,"annotation_layers":[],"forecastPeriods":10,"forecastInterval":0.8,"orientation":"vertical","x_axis_title_margin":15,"y_axis_title_margin":15,"y_axis_title_position":"Left","sort_series_type":"sum","color_scheme":"supersetColors","time_shift_color":true,"only_total":true,"show_legend":true,"legendType":"scroll","legendOrientation":"top","x_axis_time_format":"smart_date","y_axis_format":null,"y_axis_bounds":[null,null],"truncateXAxis":true,"rich_tooltip":true,"showTooltipTotal":true,"tooltipTimeFormat":"smart_date","extra_form_data":{},"dashboards":[9],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}'
cache_timeout: null
uuid: db9609e4-9b78-4a32-87a7-4d9e19d51cd8
version: 1.0.0
dataset_uuid: e8623bb9-5e00-f531-506a-19607f5f8005

View File

@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
slice_name: Revenue by Deal Size
description: null
certified_by: null
certification_details: null
viz_type: echarts_timeseries_bar
params:
adhoc_filters: []
@@ -27,30 +30,30 @@ params:
datasource: 23__table
granularity_sqla: order_date
groupby:
- deal_size
- deal_size
label_colors: {}
left_margin: auto
line_interpolation: linear
metrics:
- aggregate: SUM
column:
column_name: sales
description: null
expression: null
filterable: true
groupby: true
id: 917
is_dttm: false
optionName: _col_Sales
python_date_format: null
type: DOUBLE PRECISION
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: false
isNew: false
label: (Sales)
optionName: metric_3is69ofceho_6d0ezok7ry6
sqlExpression: null
- aggregate: SUM
column:
column_name: sales
description: null
expression: null
filterable: true
groupby: true
id: 917
is_dttm: false
optionName: _col_Sales
python_date_format: null
type: DOUBLE PRECISION
verbose_name: null
expressionType: SIMPLE
hasCustomLabel: false
isNew: false
label: (Sales)
optionName: metric_3is69ofceho_6d0ezok7ry6
sqlExpression: null
order_desc: true
queryFields:
groupby: groupby
@@ -61,15 +64,16 @@ params:
show_brush: auto
show_legend: true
time_grain_sqla: P1M
time_range: "2003-01-01T00:00:00 : 2005-06-01T00:00:00"
time_range: '2003-01-01T00:00:00 : 2005-06-01T00:00:00'
url_params: {}
viz_type: echarts_timeseries_bar
x_axis_format: smart_date
x_ticks_layout: auto
y_axis_bounds:
- null
- null
- null
- null
y_axis_format: SMART_NUMBER
query_context: null
cache_timeout: null
uuid: f065a533-2e13-42b9-bd19-801a21700dff
version: 1.0.0

View File

@@ -15,6 +15,9 @@
# specific language governing permissions and limitations
# under the License.
slice_name: Seasonality of Revenue (per Product Line)
description: null
certified_by: null
certification_details: null
viz_type: horizon
params:
adhoc_filters: []
@@ -53,6 +56,8 @@ params:
time_range: No filter
url_params: {}
viz_type: horizon
annotation_layers: []
query_context: null
cache_timeout: null
uuid: cf0da099-b3ab-4d94-ab62-cf353ac3c611
version: 1.0.0

View File

@@ -15,12 +15,15 @@
# specific language governing permissions and limitations
# under the License.
slice_name: Total Revenue
viz_type: big_number_total
description: null
certified_by: null
certification_details: null
viz_type: big_number
params:
adhoc_filters: []
datasource: 23__table
granularity_sqla: order_date
header_font_size: 0.4
datasource: 21__table
viz_type: big_number
slice_id: 114
x_axis: order_date
metric:
aggregate: SUM
column:
@@ -41,14 +44,35 @@ params:
label: (Sales)
optionName: metric_twq59hf4ej_g70qjfmehsq
sqlExpression: null
queryFields:
metric: metrics
subheader: ''
adhoc_filters:
- clause: WHERE
comparator: No filter
expressionType: SIMPLE
operator: TEMPORAL_RANGE
subject: order_date
show_trend_line: true
start_y_axis_at_zero: true
color_picker:
a: 1
b: 135
g: 122
r: 0
header_font_size: 0.4
subheader_font_size: 0.15
time_range: No filter
url_params: {}
viz_type: big_number_total
y_axis_format: $,.2f
y_axis_format: .3s
currency_format:
symbolPosition: prefix
symbol: USD
time_format: smart_date
rolling_type: cumsum
extra_form_data: {}
dashboards:
- 9
query_context: '{"datasource":{"id":21,"type":"table"},"force":false,"queries":[{"filters":[{"col":"order_date","op":"TEMPORAL_RANGE","val":"No
filter"}],"extras":{"having":"","where":""},"applied_time_extras":{},"columns":[{"columnType":"BASE_AXIS","sqlExpression":"order_date","label":"order_date","expressionType":"SQL"}],"metrics":[{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"(Sales)","optionName":"metric_twq59hf4ej_g70qjfmehsq","sqlExpression":null}],"annotation_layers":[],"series_limit":0,"order_desc":true,"url_params":{},"custom_params":{},"custom_form_data":{},"post_processing":[{"operation":"pivot","options":{"index":["order_date"],"columns":[],"aggregates":{"(Sales)":{"operator":"mean"}},"drop_missing_columns":true}},{"operation":"cum","options":{"operator":"sum","columns":{"(Sales)":"(Sales)"}}},{"operation":"flatten"}]}],"form_data":{"datasource":"21__table","viz_type":"big_number","slice_id":114,"x_axis":"order_date","metric":{"aggregate":"SUM","column":{"column_name":"sales","description":null,"expression":null,"filterable":true,"groupby":true,"id":917,"is_dttm":false,"optionName":"_col_Sales","python_date_format":null,"type":"DOUBLE
PRECISION","verbose_name":null},"expressionType":"SIMPLE","hasCustomLabel":false,"isNew":false,"label":"(Sales)","optionName":"metric_twq59hf4ej_g70qjfmehsq","sqlExpression":null},"adhoc_filters":[{"clause":"WHERE","comparator":"No
filter","expressionType":"SIMPLE","operator":"TEMPORAL_RANGE","subject":"order_date"}],"show_trend_line":true,"start_y_axis_at_zero":true,"color_picker":{"a":1,"b":135,"g":122,"r":0},"header_font_size":0.4,"subheader_font_size":0.15,"y_axis_format":".3s","currency_format":{"symbolPosition":"prefix","symbol":"USD"},"time_format":"smart_date","rolling_type":"cumsum","extra_form_data":{},"dashboards":[9],"force":false,"result_format":"json","result_type":"full"},"result_format":"json","result_type":"full"}'
cache_timeout: null
uuid: 7b12a243-88e0-4dc5-ac33-9a840bb0ac5a
version: 1.0.0

View File

@@ -16,10 +16,10 @@
# under the License.
dashboard_title: Sales Dashboard
description: null
css: ""
css: ''
slug: null
certified_by: ""
certification_details: ""
certified_by: ''
certification_details: ''
published: true
uuid: 04f79081-fb49-7bac-7f14-cc76cd2ad93b
position:
@@ -27,195 +27,195 @@ position:
children: []
id: CHART-1NOOLm5YPs
meta:
chartId: 2805
chartId: 115
height: 25
sliceName: Total Items Sold
sliceName: Items Sold
sliceNameOverride: Total Products Sold
uuid: c3d643cd-fd6f-4659-a5b7-59402487a8d0
width: 2
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
- COLUMN-8Rp54B6ikC
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
- COLUMN-8Rp54B6ikC
type: CHART
CHART-AYpv8gFi_q:
children: []
id: CHART-AYpv8gFi_q
meta:
chartId: 2810
chartId: 112
height: 70
sliceName: Number of Deals (for each Combination)
uuid: bd20fc69-dd51-46c1-99b5-09e37a434bf1
width: 6
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROW-0l1WcDzW3
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROW-0l1WcDzW3
type: CHART
CHART-KKT9BsnUst:
children: []
id: CHART-KKT9BsnUst
meta:
chartId: 2806
height: 59
chartId: 113
height: 50
sliceName: Quarterly Sales (By Product Line)
sliceNameOverride: Quarterly Revenue (By Product Line)
uuid: db9609e4-9b78-4a32-87a7-4d9e19d51cd8
width: 7
width: 6
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-oAtmu5grZ
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-oAtmu5grZ
type: CHART
CHART-OJ9aWDmn1q:
children: []
id: CHART-OJ9aWDmn1q
meta:
chartId: 2808
chartId: 116
height: 70
sliceName: Proportion of Revenue by Product Line
sliceNameOverride: Proportion of Monthly Revenue by Product Line
uuid: 08aff161-f60c-4cb3-a225-dc9b1140d2e3
width: 6
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROW-0l1WcDzW3
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROW-0l1WcDzW3
type: CHART
CHART-YFg-9wHE7s:
children: []
id: CHART-YFg-9wHE7s
meta:
chartId: 2811
chartId: 119
height: 49
sliceName: Seasonality of Revenue (per Product Line)
uuid: cf0da099-b3ab-4d94-ab62-cf353ac3c611
width: 6
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROW-E7MDSGfnm
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROW-E7MDSGfnm
type: CHART
CHART-_LMKI0D3tj:
children: []
id: CHART-_LMKI0D3tj
meta:
chartId: 2809
chartId: 117
height: 49
sliceName: Revenue by Deal Size
sliceNameOverride: Monthly Revenue by Deal SIze
uuid: f065a533-2e13-42b9-bd19-801a21700dff
width: 6
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROW-E7MDSGfnm
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROW-E7MDSGfnm
type: CHART
CHART-id4RGv80N-:
children: []
id: CHART-id4RGv80N-
meta:
chartId: 2807
height: 59
sliceName: Total Items Sold (By Product Line)
sliceNameOverride: Total Products Sold (By Product Line)
chartId: 111
height: 50
sliceName: Items by Product Line
sliceNameOverride: Products Sold By Product Line
uuid: b8b7ca30-6291-44b0-bc64-ba42e2892b86
width: 2
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-oAtmu5grZ
- COLUMN-G6_2DvG8aK
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-oAtmu5grZ
- COLUMN-G6_2DvG8aK
type: CHART
CHART-j24u8ve41b:
children: []
id: CHART-j24u8ve41b
meta:
chartId: 670
height: 59
chartId: 120
height: 50
sliceName: Overall Sales (By Product Line)
sliceNameOverride: Total Revenue (By Product Line)
sliceNameOverride: Total Revenue By Product
uuid: 09c497e0-f442-1121-c9e7-671e37750424
width: 3
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-oAtmu5grZ
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-oAtmu5grZ
type: CHART
CHART-lFanAaYKBK:
children: []
id: CHART-lFanAaYKBK
meta:
chartId: 2804
chartId: 114
height: 26
sliceName: Total Revenue
uuid: 7b12a243-88e0-4dc5-ac33-9a840bb0ac5a
width: 3
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
- COLUMN-8Rp54B6ikC
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
- COLUMN-8Rp54B6ikC
type: CHART
CHART-vomBOiI7U9:
children: []
id: CHART-vomBOiI7U9
meta:
chartId: 668
chartId: 118
height: 53
sliceName: Quarterly Sales
sliceNameOverride: Quarterly Revenue
uuid: 692aca26-a526-85db-c94c-411c91cc1077
width: 7
width: 6
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
type: CHART
COLUMN-8Rp54B6ikC:
children:
- CHART-lFanAaYKBK
- CHART-1NOOLm5YPs
- CHART-lFanAaYKBK
- CHART-1NOOLm5YPs
id: COLUMN-8Rp54B6ikC
meta:
background: BACKGROUND_TRANSPARENT
width: 2
width: 3
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
type: COLUMN
COLUMN-G6_2DvG8aK:
children:
- CHART-id4RGv80N-
- CHART-id4RGv80N-
id: COLUMN-G6_2DvG8aK
meta:
background: BACKGROUND_TRANSPARENT
width: 2
width: 3
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-oAtmu5grZ
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-oAtmu5grZ
type: COLUMN
DASHBOARD_VERSION_KEY: v2
GRID_ID:
children: []
id: GRID_ID
parents:
- ROOT_ID
- ROOT_ID
type: GRID
HEADER_ID:
id: HEADER_ID
@@ -226,10 +226,9 @@ position:
children: []
id: MARKDOWN--AtDSWnapE
meta:
code:
"# \U0001F697 Vehicle Sales Dashboard \U0001F3CD\n\nThis example dashboard\
\ provides insight into the business operations of vehicle seller. The dataset\
\ powering this dashboard can be found [here on Kaggle](https://www.kaggle.com/kyanyoga/sample-sales-data).\n\
code: "# \U0001F697 Vehicle Sales \U0001F3CD\n\nThis example dashboard provides\
\ insight into the business operations of vehicle seller. The dataset powering\
\ this dashboard can be found [here on Kaggle](https://www.kaggle.com/kyanyoga/sample-sales-data).\n\
\n### Timeline\n\nThe dataset contains data on all orders from the 2003 and\
\ 2004 fiscal years, and some orders from 2005.\n\n### Products Sold\n\nThis\
\ shop mainly sells the following products:\n\n- \U0001F697 Classic Cars\n\
@@ -239,113 +238,498 @@ position:
height: 53
width: 3
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROW-Tyv02UA_6W
type: MARKDOWN
ROOT_ID:
children:
- TABS-e5Ruro0cjP
- TABS-e5Ruro0cjP
id: ROOT_ID
type: ROOT
ROW-0l1WcDzW3:
children:
- CHART-OJ9aWDmn1q
- CHART-AYpv8gFi_q
- CHART-OJ9aWDmn1q
- CHART-AYpv8gFi_q
id: ROW-0l1WcDzW3
meta:
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
type: ROW
ROW-E7MDSGfnm:
children:
- CHART-YFg-9wHE7s
- CHART-_LMKI0D3tj
- CHART-YFg-9wHE7s
- CHART-_LMKI0D3tj
id: ROW-E7MDSGfnm
meta:
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-4fthLQmdX
type: ROW
ROW-Tyv02UA_6W:
children:
- COLUMN-8Rp54B6ikC
- CHART-vomBOiI7U9
- MARKDOWN--AtDSWnapE
- COLUMN-8Rp54B6ikC
- CHART-vomBOiI7U9
- MARKDOWN--AtDSWnapE
id: ROW-Tyv02UA_6W
meta:
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
type: ROW
ROW-oAtmu5grZ:
children:
- COLUMN-G6_2DvG8aK
- CHART-KKT9BsnUst
- CHART-j24u8ve41b
- COLUMN-G6_2DvG8aK
- CHART-KKT9BsnUst
- CHART-j24u8ve41b
id: ROW-oAtmu5grZ
meta:
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
- ROOT_ID
- TABS-e5Ruro0cjP
- TAB-d-E0Zc1cTH
type: ROW
TAB-4fthLQmdX:
children:
- ROW-0l1WcDzW3
- ROW-E7MDSGfnm
- ROW-0l1WcDzW3
- ROW-E7MDSGfnm
id: TAB-4fthLQmdX
meta:
text: "\U0001F9ED Exploratory"
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- ROOT_ID
- TABS-e5Ruro0cjP
type: TAB
TAB-d-E0Zc1cTH:
children:
- ROW-Tyv02UA_6W
- ROW-oAtmu5grZ
- ROW-Tyv02UA_6W
- ROW-oAtmu5grZ
id: TAB-d-E0Zc1cTH
meta:
text: "\U0001F3AF Sales Overview"
parents:
- ROOT_ID
- TABS-e5Ruro0cjP
- ROOT_ID
- TABS-e5Ruro0cjP
type: TAB
TABS-e5Ruro0cjP:
children:
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
id: TABS-e5Ruro0cjP
meta: {}
parents:
- ROOT_ID
- ROOT_ID
type: TABS
metadata:
timed_refresh_immune_slices: []
expanded_slices: {}
refresh_frequency: 0
default_filters: "{}"
default_filters: '{}'
color_scheme: supersetColors
label_colors:
Medium: "#1FA8C9"
Small: "#454E7C"
Large: "#5AC189"
SUM(SALES): "#1FA8C9"
Classic Cars: "#454E7C"
Vintage Cars: "#5AC189"
Motorcycles: "#FF7F44"
Trucks and Buses: "#666666"
Planes: "#E04355"
Ships: "#FCC700"
Trains: "#A868B7"
Medium: '#1FA8C9'
Small: '#454E7C'
Large: '#5AC189'
SUM(SALES): '#1FA8C9'
Classic Cars: '#454E7C'
Vintage Cars: '#5AC189'
Motorcycles: '#FF7F44'
Trucks and Buses: '#666666'
Planes: '#E04355'
Ships: '#FCC700'
Trains: '#A868B7'
native_filter_configuration:
- id: NATIVE_FILTER-HX2lV--YaAZRQfJ_yfYB2
controlValues:
enableEmptyFilter: false
defaultToFirstItem: false
multiSelect: true
searchAllOptions: false
inverseSelection: false
name: Country
filterType: filter_select
targets:
- column:
name: country
datasetUuid: e8623bb9-5e00-f531-506a-19607f5f8005
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
rootPath:
- ROOT_ID
excluded: []
type: NATIVE_FILTER
description: ''
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
tabsInScope:
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
- id: NATIVE_FILTER-oCF7UtoHuDIBg44q5peth
controlValues:
enableEmptyFilter: false
name: Order Quantity
filterType: filter_range
targets:
- column:
name: quantity_ordered
datasetUuid: e8623bb9-5e00-f531-506a-19607f5f8005
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
rootPath:
- ROOT_ID
excluded: []
type: NATIVE_FILTER
description: ''
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
tabsInScope:
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
- id: NATIVE_FILTER-V_UJOthxN8gCeYSD0id9b
controlValues:
enableEmptyFilter: false
name: Time Range
filterType: filter_time
targets:
- {}
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
rootPath:
- ROOT_ID
excluded: []
type: NATIVE_FILTER
description: ''
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
tabsInScope:
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
- id: NATIVE_FILTER-t8xOh3el1KBWWiCIF5hIN
controlValues:
enableEmptyFilter: false
name: Time Grain
filterType: filter_timegrain
targets:
- datasetUuid: e8623bb9-5e00-f531-506a-19607f5f8005
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
excluded: []
rootPath:
- ROOT_ID
type: NATIVE_FILTER
description: ''
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
tabsInScope:
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
- id: NATIVE_FILTER-9tGcTjqhNxOgX2AEPLVil
controlValues:
enableEmptyFilter: false
defaultToFirstItem: false
multiSelect: true
searchAllOptions: false
inverseSelection: false
name: Postal Code
filterType: filter_select
targets:
- column:
name: postal_code
datasetUuid: e8623bb9-5e00-f531-506a-19607f5f8005
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
rootPath:
- ROOT_ID
excluded: []
type: NATIVE_FILTER
description: ''
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
tabsInScope:
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
- id: NATIVE_FILTER-pGnu5e_bg1IGz2wdzIuCA
controlValues:
enableEmptyFilter: false
defaultToFirstItem: false
multiSelect: true
searchAllOptions: false
inverseSelection: false
name: State
filterType: filter_select
targets:
- column:
name: state
datasetUuid: e8623bb9-5e00-f531-506a-19607f5f8005
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
rootPath:
- ROOT_ID
excluded: []
type: NATIVE_FILTER
description: ''
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
tabsInScope:
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
- id: NATIVE_FILTER-EVb_e9pndL9UByuZt0z_w
controlValues:
enableEmptyFilter: false
name: MSRP
filterType: filter_range
targets:
- column:
name: msrp
datasetUuid: e8623bb9-5e00-f531-506a-19607f5f8005
defaultDataMask:
extraFormData: {}
filterState: {}
ownState: {}
cascadeParentIds: []
scope:
rootPath:
- ROOT_ID
excluded: []
type: NATIVE_FILTER
description: ''
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
tabsInScope:
- TAB-d-E0Zc1cTH
- TAB-4fthLQmdX
shared_label_colors:
- Classic Cars
- Motorcycles
- Planes
- Ships
- Trains
- Trucks and Buses
- Vintage Cars
map_label_colors:
Shipped: '#1FA8C9'
Cancelled: '#454E7C'
On Hold: '#5AC189'
Resolved: '#FF7F44'
In Process: '#666666'
Disputed: '#E04355'
color_scheme_domain:
- '#1FA8C9'
- '#454E7C'
- '#5AC189'
- '#FF7F44'
- '#666666'
- '#E04355'
- '#FCC700'
- '#A868B7'
- '#3CCCCB'
- '#A38F79'
- '#8FD3E4'
- '#A1A6BD'
- '#ACE1C4'
- '#FEC0A1'
- '#B2B2B2'
- '#EFA1AA'
- '#FDE380'
- '#D3B3DA'
- '#9EE5E5'
- '#D1C6BC'
cross_filters_enabled: true
chart_configuration:
'111':
id: 111
crossFilters:
scope: global
chartsInScope:
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
'113':
id: 113
crossFilters:
scope: global
chartsInScope:
- 111
- 112
- 114
- 115
- 116
- 117
- 118
- 119
- 120
'116':
id: 116
crossFilters:
scope: global
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 117
- 118
- 119
- 120
'117':
id: 117
crossFilters:
scope: global
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 118
- 119
- 120
'118':
id: 118
crossFilters:
scope: global
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 119
- 120
'120':
id: 120
crossFilters:
scope: global
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
global_chart_configuration:
scope:
rootPath:
- ROOT_ID
excluded: []
chartsInScope:
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
version: 1.0.0

View File

@@ -20,6 +20,7 @@ description: null
default_endpoint: null
offset: 0
cache_timeout: null
catalog: null
schema: null
sql: null
params: null
@@ -27,6 +28,8 @@ template_params: null
filter_select_enabled: true
fetch_values_predicate: null
extra: null
normalize_columns: false
always_filter_main_dttm: false
uuid: e8623bb9-5e00-f531-506a-19607f5f8005
metrics:
- metric_name: count
@@ -35,6 +38,7 @@ metrics:
expression: COUNT(*)
description: null
d3format: null
currency: null
extra: null
warning_text: null
columns:
@@ -43,251 +47,301 @@ columns:
is_dttm: true
is_active: true
type: TIMESTAMP WITHOUT TIME ZONE
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: price_each
verbose_name: null
is_dttm: false
is_active: true
type: DOUBLE PRECISION
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: sales
verbose_name: null
is_dttm: false
is_active: true
type: DOUBLE PRECISION
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: address_line1
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: address_line2
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: contact_last_name
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: contact_first_name
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: quantity_ordered
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: year
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: postal_code
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: customer_name
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: deal_size
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: state
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: status
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: order_line_number
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: quantity_ordered
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: order_number
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: month
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: quarter
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: year
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: month
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: msrp
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: contact_last_name
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: contact_first_name
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: postal_code
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: customer_name
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: deal_size
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: product_code
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: product_line
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: state
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: status
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: city
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: country
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: phone
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
- column_name: territory
verbose_name: null
is_dttm: false
is_active: true
type: TEXT
advanced_data_type: null
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
extra: null
version: 1.0.0
database_uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee
data: https://raw.githubusercontent.com/apache-superset/examples-data/lowercase_columns_examples/datasets/examples/sales.csv

View File

@@ -14,7 +14,7 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Add folder table
"""Add folders column to datasets
Revision ID: 94e7a3499973
Revises: 74ad1125881c

View File

@@ -15,4 +15,4 @@
# specific language governing permissions and limitations
# under the License.
Babel==2.9.1
jinja2==3.1.5
jinja2==3.1.6

View File

@@ -1103,6 +1103,11 @@ class TestDatabaseApi(SupersetTestCase):
assert rv.status_code == 201
assert "sqlalchemy_form" in response["result"]["configuration_method"]
# Cleanup
model = db.session.query(Database).get(response.get("id"))
db.session.delete(model)
db.session.commit()
def test_create_database_server_cert_validate(self):
"""
Database API: Test create server cert validation
@@ -3153,6 +3158,59 @@ class TestDatabaseApi(SupersetTestCase):
]
}
@mock.patch("superset.commands.database.importers.v1.utils.add_permissions")
def test_import_database_row_expansion_enabled(self, mock_add_permissions):
"""
Database API: Test import database with row expansion enabled.
"""
self.login(ADMIN_USERNAME)
uri = "api/v1/database/import/"
db_config = {
"database_name": "DB with expand rows enabled",
"allow_csv_upload": True,
"allow_ctas": True,
"allow_cvas": True,
"allow_dml": True,
"allow_run_async": False,
"cache_timeout": None,
"expose_in_sqllab": True,
"extra": {
"schema_options": {"expand_rows": True},
},
"sqlalchemy_uri": "postgresql://user:pass@host1",
"uuid": "b8a1ccd3-779d-4ab7-8ad8-9ab119d7ff90",
"version": "1.0.0",
}
buf = BytesIO()
with ZipFile(buf, "w") as bundle:
with bundle.open("database_export/metadata.yaml", "w") as fp:
fp.write(yaml.safe_dump(database_metadata_config).encode())
with bundle.open(
"database_export/databases/DB_with_expand_rows_enabled.yaml", "w"
) as fp:
fp.write(yaml.safe_dump(db_config).encode())
buf.seek(0)
form_data = {
"formData": (buf, "database_export.zip"),
"passwords": json.dumps(
{"databases/DB_with_expand_rows_enabled.yaml": "SECRET"}
),
}
rv = self.client.post(uri, data=form_data, content_type="multipart/form-data")
response = json.loads(rv.data.decode("utf-8"))
assert rv.status_code == 200
assert response == {"message": "OK"}
database = db.session.query(Database).filter_by(uuid=db_config["uuid"]).one()
assert database.extra == json.dumps({"schema_options": {"expand_rows": True}})
db.session.delete(database)
db.session.commit()
@mock.patch(
"superset.db_engine_specs.base.BaseEngineSpec.get_function_names",
)

View File

@@ -255,7 +255,7 @@ class TestDatasetApi(SupersetTestCase):
"table_name",
"uuid",
]
assert sorted(response["result"][0]) == expected_columns
assert sorted(list(response["result"][0].keys())) == expected_columns # noqa: C414
def test_get_dataset_list_gamma(self):
"""
@@ -1563,92 +1563,6 @@ class TestDatasetApi(SupersetTestCase):
db.session.delete(dataset)
db.session.commit()
@with_feature_flags(DATASET_FOLDERS=True)
def test_update_dataset_add_folders(self):
"""
Dataset API: Test adding folders to dataset
"""
self.login(username="admin")
dataset = self.insert_default_dataset()
dataset_data = {
"folders": [
{
"type": "folder",
"uuid": "b49ac3dd-c79b-42a4-9082-39ee74f3b369",
"name": "My metrics",
"children": [
{
"type": "metric",
"uuid": dataset.metrics[0].uuid,
"name": dataset.metrics[0].metric_name,
},
],
},
{
"type": "folder",
"uuid": "f5db85fa-75d6-45e5-bdce-c6194db80642",
"name": "My columns",
"children": [
{
"type": "folder",
"uuid": "b5330233-e323-4157-b767-98b16f00ca93",
"name": "Dimensions",
"children": [
{
"type": "column",
"uuid": dataset.columns[1].uuid,
"name": dataset.columns[1].column_name,
},
],
},
],
},
]
}
uri = f"api/v1/dataset/{dataset.id}"
rv = self.put_assert_metric(uri, dataset_data, "put")
assert rv.status_code == 200
model = db.session.query(SqlaTable).get(dataset.id)
assert model.folders == [
{
"uuid": "b49ac3dd-c79b-42a4-9082-39ee74f3b369",
"type": "folder",
"name": "My metrics",
"children": [
{
"uuid": str(dataset.metrics[0].uuid),
"type": "metric",
"name": "count",
}
],
},
{
"uuid": "f5db85fa-75d6-45e5-bdce-c6194db80642",
"type": "folder",
"name": "My columns",
"children": [
{
"uuid": "b5330233-e323-4157-b767-98b16f00ca93",
"type": "folder",
"name": "Dimensions",
"children": [
{
"uuid": str(dataset.columns[1].uuid),
"type": "column",
"name": "name",
}
],
}
],
},
]
db.session.delete(dataset)
db.session.commit()
def test_delete_dataset_item(self):
"""
Dataset API: Test delete dataset item

View File

@@ -335,6 +335,54 @@ def test_rename_without_catalog(
assert schema2_pvm.view_menu.name == f"[{database_without_catalog.name}].[schema2]"
def test_rename_without_catalog_with_assets(
mocker: MockerFixture,
database_without_catalog: MockerFixture,
) -> None:
"""
Test that permissions are renamed correctly when the DB connection does not support
catalogs, and it has assets associated with it.
"""
database_dao = mocker.patch("superset.commands.database.update.DatabaseDAO")
original_database = mocker.MagicMock()
original_database.database_name = "my_db"
database_without_catalog.database_name = "my_other_db"
database_without_catalog.get_all_schema_names.return_value = ["schema1"]
database_dao.update.return_value = database_without_catalog
database_dao.find_by_id.return_value = original_database
sync_db_perms_dao = mocker.patch(
"superset.commands.database.sync_permissions.DatabaseDAO"
)
sync_db_perms_dao.find_by_id.return_value = database_without_catalog
mocker.patch("superset.commands.database.update.get_username")
mocker.patch("superset.security_manager.get_user_by_username")
dataset = mocker.MagicMock()
chart = mocker.MagicMock()
sync_db_perms_dao.get_datasets.return_value = [dataset]
dataset_dao = mocker.patch("superset.commands.database.sync_permissions.DatasetDAO")
dataset_dao.get_related_objects.return_value = {"charts": [chart]}
find_permission_view_menu = mocker.patch.object(
security_manager,
"find_permission_view_menu",
)
schema_pvm = mocker.MagicMock()
schema_pvm.view_menu.name = "[my_db].[schema1]"
find_permission_view_menu.side_effect = [
"[my_db].[schema1]",
schema_pvm,
]
UpdateDatabaseCommand(1, {}).run()
assert schema_pvm.view_menu.name == f"[{database_without_catalog.name}].[schema1]"
assert dataset.schema_perm == f"[{database_without_catalog.name}].[schema1]"
assert dataset.catalog_perm is None
assert chart.catalog_perm is None
assert chart.schema_perm == f"[{database_without_catalog.name}].[schema1]"
def test_update_with_oauth2(
mocker: MockerFixture,
database_needs_oauth2: MockerFixture,

View File

@@ -14,21 +14,16 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
from typing import cast
from unittest.mock import MagicMock
import pytest
from marshmallow import ValidationError
from pytest_mock import MockerFixture
from superset import db
from superset.commands.dataset.exceptions import DatasetInvalidError
from superset.commands.dataset.update import UpdateDatasetCommand, validate_folders
from superset.commands.dataset.update import UpdateDatasetCommand
from superset.connectors.sqla.models import SqlaTable
from superset.datasets.schemas import FolderSchema
from superset.models.core import Database
from tests.unit_tests.conftest import with_feature_flags
@pytest.mark.usefixture("session")
@@ -63,350 +58,3 @@ def test_update_uniqueness_error(mocker: MockerFixture) -> None:
"schema": "qux",
},
).run()
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders(mocker: MockerFixture) -> None:
"""
Test the folder validation.
"""
metrics = [mocker.MagicMock(metric_name="metric1", uuid="uuid1")]
columns = [
mocker.MagicMock(column_name="column1", uuid="uuid2"),
mocker.MagicMock(column_name="column2", uuid="uuid3"),
]
validate_folders(folders=[], metrics=metrics, columns=columns)
folders = cast(
list[FolderSchema],
[
{
"uuid": "uuid4",
"type": "folder",
"name": "My folder",
"children": [
{
"uuid": "uuid1",
"type": "metric",
"name": "metric1",
},
{
"uuid": "uuid2",
"type": "column",
"name": "column1",
},
{
"uuid": "uuid3",
"type": "column",
"name": "column2",
},
],
},
],
)
validate_folders(folders=folders, metrics=metrics, columns=columns)
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders_cycle(mocker: MockerFixture) -> None:
"""
Test that we can detect cycles in the folder structure.
"""
folders = cast(
list[FolderSchema],
[
{
"uuid": "uuid1",
"type": "folder",
"name": "My folder",
"children": [
{
"uuid": "uuid2",
"type": "folder",
"name": "My other folder",
"children": [
{
"uuid": "uuid1",
"type": "folder",
"name": "My folder",
"children": [],
},
],
},
],
},
],
)
with pytest.raises(ValidationError) as excinfo:
validate_folders(folders=folders, metrics=[], columns=[])
assert str(excinfo.value) == "Cycle detected: uuid1 appears in its ancestry"
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders_inter_cycle(mocker: MockerFixture) -> None:
"""
Test that we can detect cycles between folders.
"""
folders = cast(
list[FolderSchema],
[
{
"uuid": "uuid1",
"type": "folder",
"name": "My folder",
"children": [
{
"uuid": "uuid2",
"type": "folder",
"name": "My other folder",
"children": [],
},
],
},
{
"uuid": "uuid2",
"type": "folder",
"name": "My other folder",
"children": [
{
"uuid": "uuid1",
"type": "folder",
"name": "My folder",
"children": [],
},
],
},
],
)
with pytest.raises(ValidationError) as excinfo:
validate_folders(folders=folders, metrics=[], columns=[])
assert str(excinfo.value) == "Duplicate UUID in folder structure: uuid2"
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders_duplicates(mocker: MockerFixture) -> None:
"""
Test that metrics and columns belong to a single folder.
"""
metrics = [mocker.MagicMock(metric_name="count", uuid="uuid2")]
folders = cast(
list[FolderSchema],
[
{
"uuid": "uuid1",
"type": "folder",
"name": "My folder",
"children": [
{
"uuid": "uuid2",
"type": "metric",
"name": "count",
},
],
},
{
"uuid": "uuid2",
"type": "folder",
"name": "My other folder",
"children": [
{
"uuid": "uuid2",
"type": "metric",
"name": "count",
},
],
},
],
)
with pytest.raises(ValidationError) as excinfo:
validate_folders(folders=folders, metrics=metrics, columns=[])
assert str(excinfo.value) == "Duplicate UUID in folder structure: uuid2"
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders_duplicate_name_not_siblings(mocker: MockerFixture) -> None:
"""
Duplicate folder names are allowed if folders are not siblings.
"""
folders = cast(
list[FolderSchema],
[
{
"uuid": "uuid1",
"type": "folder",
"name": "Sales",
"children": [
{
"uuid": "uuid2",
"type": "folder",
"name": "Core",
"children": [],
},
],
},
{
"uuid": "uuid3",
"type": "folder",
"name": "Engineering",
"children": [
{
"uuid": "uuid4",
"type": "folder",
"name": "Core",
"children": [],
},
],
},
],
)
validate_folders(folders=folders, metrics=[], columns=[])
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders_duplicate_name_siblings(mocker: MockerFixture) -> None:
"""
Duplicate folder names are not allowed if folders are siblings.
"""
folders = cast(
list[FolderSchema],
[
{
"uuid": "uuid1",
"type": "folder",
"name": "Sales",
"children": [
{
"uuid": "uuid2",
"type": "folder",
"name": "Core",
"children": [],
},
],
},
{
"uuid": "uuid3",
"type": "folder",
"name": "Sales",
"children": [
{
"uuid": "uuid4",
"type": "folder",
"name": "Other",
"children": [],
},
],
},
],
)
with pytest.raises(ValidationError) as excinfo:
validate_folders(folders=folders, metrics=[], columns=[])
assert str(excinfo.value) == "Duplicate folder name: Sales"
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders_invalid_names(mocker: MockerFixture) -> None:
"""
Test that we can detect reserved folder names.
"""
folders_with_metrics = cast(
list[FolderSchema],
[
{
"uuid": "uuid1",
"type": "folder",
"name": "Metrics",
"children": [],
},
],
)
folders_with_columns = cast(
list[FolderSchema],
[
{
"uuid": "uuid1",
"type": "folder",
"name": "Columns",
"children": [],
},
],
)
with pytest.raises(ValidationError) as excinfo:
validate_folders(folders=folders_with_metrics, metrics=[], columns=[])
assert str(excinfo.value) == "Folder cannot have name 'Metrics'"
with pytest.raises(ValidationError) as excinfo:
validate_folders(folders=folders_with_columns, metrics=[], columns=[])
assert str(excinfo.value) == "Folder cannot have name 'Columns'"
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders_invalid_uuid(mocker: MockerFixture) -> None:
"""
Test that we can detect invalid UUIDs.
"""
metrics = [mocker.MagicMock(metric_name="metric1", uuid="uuid1")]
columns = [
mocker.MagicMock(column_name="column1", uuid="uuid2"),
mocker.MagicMock(column_name="column2", uuid="uuid3"),
]
folders = cast(
list[FolderSchema],
[
{
"uuid": "uuid4",
"type": "folder",
"name": "My folder",
"children": [
{
"uuid": "uuid2",
"type": "metric",
"name": "metric1",
},
],
},
],
)
with pytest.raises(ValidationError) as excinfo:
validate_folders(folders=folders, metrics=metrics, columns=columns)
assert str(excinfo.value) == "Invalid UUID for metric 'metric1': uuid2"
@with_feature_flags(DATASET_FOLDERS=True)
def test_validate_folders_mismatched_name(mocker: MockerFixture) -> None:
"""
Test that we can detect mismatched names.
"""
metrics = [mocker.MagicMock(metric_name="metric1", uuid="uuid1")]
columns = [
mocker.MagicMock(column_name="column1", uuid="uuid2"),
mocker.MagicMock(column_name="column2", uuid="uuid3"),
]
folders = cast(
list[FolderSchema],
[
{
"uuid": "uuid4",
"type": "folder",
"name": "My folder",
"children": [
{
"uuid": "uuid1",
"type": "metric",
"name": "metric2",
},
],
},
],
)
with pytest.raises(ValidationError) as excinfo:
validate_folders(folders=folders, metrics=metrics, columns=columns)
assert str(excinfo.value) == "Mismatched name 'metric2' for UUID 'uuid1'"

View File

@@ -17,7 +17,7 @@
# pylint: disable=import-outside-toplevel, invalid-name, line-too-long
from typing import TYPE_CHECKING
from typing import Any, TYPE_CHECKING
from urllib.parse import parse_qs, urlparse
import pandas as pd
@@ -697,3 +697,23 @@ def test_get_oauth2_fresh_token(
},
timeout=30.0,
)
def test_update_params_from_encrypted_extra(mocker: MockerFixture) -> None:
"""
Test `update_params_from_encrypted_extra`.
"""
from superset.db_engine_specs.gsheets import GSheetsEngineSpec
database = mocker.MagicMock(
encrypted_extra=json.dumps(
{
"oauth2_client_info": "SECRET",
"foo": "bar",
}
)
)
params: dict[str, Any] = {}
GSheetsEngineSpec.update_params_from_encrypted_extra(database, params)
assert params == {"foo": "bar"}