Compare commits

...

37 Commits

Author SHA1 Message Date
Christine Chambers
11bace3808 Merge pull request #6805 from lyft/xtinec--fix-sticky-tooltip
Fix sticky tooltips on nvd3 vizzes
2019-02-01 15:07:06 -08:00
Maxime Beauchemin
ba64ae6974 Add missing license (#6801) 2019-02-01 14:42:37 -08:00
Christine Chambers
7846bae8e0 Merge pull request #6781 from lyft/xtinec--merge-feature-flags
Merge default feature flags and user defined feature flags
2019-02-01 14:36:25 -08:00
Christine Chambers
501340b5db Fix sticky tooltips on nvd3 vizzes
Currently, we attempt to hide the nvd3 tooltips (if any were on screen) before we draw a new viz after rerunning a query. The hiding is done by selecting the first nvtooltip element and setting the opacity to 0.

This somtimes leave behind a trail of old tooltips if a tooltip is left behind by this nvd3 bug https://github.com/novus/nvd3/issues/1262. This PR modifies the behavior of how we clean up tooltips between rerun of queries by selecting all nvd3 tooltips and removing them all from the DOM before redrawing nvd3 vizzes.
2019-02-01 14:30:13 -08:00
Christine Chambers
19b3753d2c Move feature flag utility function into superset/__init__.py 2019-02-01 13:35:50 -08:00
AlexRRR
416534add5 fix: don't remove leading new line (#6800) 2019-02-01 12:03:34 -08:00
Maxime Beauchemin
713b0ae4f4 [cosmetic] TableSelector use <i> instead of <Button> for refresh (#6783)
* [cosmetic] TableSelector use <i> instead of <Button> for refresh

* Add ASF licenses

* css hover

* missing license

* remove license header
2019-02-01 12:01:30 -08:00
Christine Chambers
5669a82350 Merge pull request #6797 from lyft/VIZ-187
Fix play slider in deck.gl visualizations
2019-01-31 22:02:56 -08:00
Beto Dealmeida
a09348d0ec Fix playslider 2019-01-31 17:23:59 -08:00
Wonjin Kim
adc9a6b495 Add Korean Translation (#6790)
* fix #6760

* start adding Korean translation

* Add Korean translation
2019-01-31 14:13:43 -08:00
John Bodley
2631558ac4 Revert "Removing uniqueness constraints on tables table" (#6777)
* Revert "creating new circular-json safe stringify and replacing one call (#6772)"

This reverts commit 11a7ad00b7.

* Revert "Improve Unicode support for MSSQL (#6690)"

This reverts commit c44ae612df.

* Revert "Fix uniqueness constraints on tables table (#6718)"

This reverts commit c4fb7a0a87.
2019-01-31 09:57:51 -08:00
Christine Chambers
b70a9ae524 Merge default feature flags and user defined feature flags
- Rename the default feature flags key in `config.py` to DEFAULT_FEATURE_FLAGS
- Merge default feature flags with user defined ones allowing the latter to overwrite the former
- Expose feature_flags for both server and client to use
- Add a utility method for checking whether a feature flag is on on server side
2019-01-30 22:23:27 -08:00
agrawaldevesh
817783f466 Allow any column to be filterable or groupable (#6764)
This is not configurable and will be the default (and only !) behavior
going forward !
2019-01-30 11:18:49 -08:00
Maxime Beauchemin
36176f3e20 [sql lab] fix stuck offline (#6782) 2019-01-30 11:12:31 -08:00
Michael McDuffee
11a7ad00b7 creating new circular-json safe stringify and replacing one call (#6772) 2019-01-29 10:59:42 -08:00
Ville Brofeldt
c44ae612df Improve Unicode support for MSSQL (#6690)
* Implement unicode where cluases for mssql queries

* Add comment about unicode support on sqla 1.3+
2019-01-28 22:56:46 -08:00
agrawaldevesh
c4fb7a0a87 Fix uniqueness constraints on tables table (#6718)
Summary: Superset code enforces (in Table crud view pre_add) that the
table is unique within <database, schema, table_name). Indeed in commit
15b67b2c6c (in 2016), the model was
updated to reflect that. However, it was never ported over to a
migration.

I am fixing that in this diff. I am choosing to make this be a new
migration instead of fixing an existing one since I want to fix existing
installations also cleanly.

I also considered removing the uniqueness constraint, but that won't
work: First because anyway there are other places where the <database,
schema, table> uniqueness is enforced in code. But also, the .sql field
isn't a first citizen yet: The schema of the table is picked up from the
table-name and the sql part is only used when creating the explore
query. So indeed we want this uniqueness constraint. (Also it breaks the
unit tests in dict_import_export_tests.py)
[Perhaps it can be removed when we have true .sql support, but for now
the user would have to create a database view and he can use that as the
'table name'. That way he gets schema inference also]

Also added INFO logging to the alembic migration.
2019-01-28 22:49:31 -08:00
Wonjin Kim
5938ac30d6 fix #6760 (#6762) 2019-01-28 22:45:16 -08:00
Maxime Beauchemin
7c02587924 [docs] improve upgrading instructions (#6766) 2019-01-28 22:14:02 -08:00
Grace Guo
744135c7fe [fix] Add action for update chart id (#6769) 2019-01-28 17:42:35 -08:00
ziheng
83ee917832 [fix] pandas>=0.24.0 datetimelike API changes (#6765) 2019-01-27 17:29:56 -08:00
Maxime Beauchemin
a62a8d3d98 [docs] bump cryptography lib version in docs (#6755) 2019-01-25 14:26:08 -08:00
Maxime Beauchemin
a93219f291 [cosmetic] Align table sort icons to the left (#6754) 2019-01-25 14:25:41 -08:00
michellethomas
8100a8fa97 Fixing sort issue with area chart and adding tests (#6358) 2019-01-25 10:30:31 -08:00
Hugh A. Miles II
6b0ab2100d fix dashboard links in welcome page (#6756) 2019-01-25 08:20:00 -08:00
davidkohcw
f38cea3ee3 Update User List in README.md (#6758)
Added a new company into the company list
2019-01-25 08:19:19 -08:00
Marcus
01689c38ea bump PyHive version for prod requirements.txt (#6751) 2019-01-24 23:11:22 -08:00
Grace Guo
879c553b0a [fix] JS error out when rename a new chart (#6752) 2019-01-24 21:31:21 -08:00
michellethomas
97cb10dbc8 Make it easier to select dropdown options in control tests (#6546) 2019-01-24 10:10:22 -08:00
agrawaldevesh
594cd70960 A few fixes for Presto connection (#6720)
* Preserve existing configuration when modifying it in case of user impersonation

* Add logging when a table cannot be added due to some schema issues
2019-01-23 21:12:27 -08:00
Maxime Beauchemin
1ffee8b236 [cosmetic] remove 'List' prefix from list headers (#6725)
* [cosmetic] remove 'List' prefix from list headers

* Same change to connectors
2019-01-23 21:06:56 -08:00
Krist Wongsuphasawat
3ae7d32caa Add iframe and markup legacy plugin (#6741)
* Add iframe plugin

* Use lazy load and add description

* remove unintended files

* Add markup

* minor adjustment
2019-01-23 10:21:57 -08:00
Krist Wongsuphasawat
954e42bafe Update gitignore (#6742)
internal: update gitignore
2019-01-23 10:21:41 -08:00
michellethomas
bab7ee7ecf Adding a note about 0.30 to updating (#6730) 2019-01-22 14:38:01 -08:00
Beto Dealmeida
bbd781b66e Remove test URL (#6740) 2019-01-22 14:27:51 -08:00
Beto Dealmeida
cf1a35b94b Allow specifying custom width for logo (#6739) 2019-01-22 14:25:49 -08:00
bolkedebruin
d65059b06a Add disclaimer and remove counter (#6738) 2019-01-22 14:18:16 -08:00
62 changed files with 5753 additions and 258 deletions

1
.gitignore vendored
View File

@@ -33,6 +33,7 @@ _modules
_static
build
app.db
apache_superset.egg-info/
changelog.sh
dist
dump.rdb

View File

@@ -12,6 +12,7 @@
.*pyc
.*lock
.*geojson
DISCLAIMER
licenses/*
node_modules/*
rat-results.txt

View File

@@ -35,7 +35,7 @@ little bit helps, and credit will always be given.
- [Pull Request Guidelines](#pull-request-guidelines)
- [Protocol](#protocol)
- [Managing Issues and PRs](#managing-issues-and-prs)
- [Local development](#local-development)
- [Setup Local Environment for Development](#setup-local-environment-for-development)
- [Documentation](#documentation)
- [Flask server](#flask-server)
- [Frontend assets](#frontend-assets)
@@ -410,6 +410,10 @@ export enum FeatureFlag {
}
```
`superset/config.py` contains `DEFAULT_FEATURE_FLAGS` which will be overwritten by
those specified under FEATURE_FLAGS in `superset_config.py`. For example, `DEFAULT_FEATURE_FLAGS = { 'FOO': True, 'BAR': False }` in `superset/config.py` and `FEATURE_FLAGS = { 'BAR': True, 'BAZ': True }` in `superset_config.py` will result
in combined feature flags of `{ 'FOO': True, 'BAR': True, 'BAZ': True }`.
## Linting
Lint the project with:

11
DISCLAIMER Normal file
View File

@@ -0,0 +1,11 @@
DISCLAIMER
Apache Superset (incubating) is an effort undergoing incubation at the Apache
Software Foundation (ASF), sponsored by the Apache Incubator PMC.
Incubation is required of all newly accepted projects until a further review
indicates that the infrastructure, communications, and decision making process
have stabilized in a manner consistent with other successful ASF projects.
While incubation status is not necessarily a reflection of the completeness or
stability of the code, it does indicate that the project has yet to be fully
endorsed by the ASF.

View File

@@ -195,6 +195,7 @@ the world know they are using Superset. Join our growing community!
1. [Lyft](https://www.lyft.com/)
1. [Maieutical Labs](https://maieuticallabs.it)
1. [Myra Labs](http://www.myralabs.com/)
1. [Now](https://www.now.vn/)
1. [PeopleDoc](https://www.people-doc.com)
1. [Ona](https://ona.io)
1. [Pronto Tools](http://www.prontotools.io)

View File

@@ -37,6 +37,12 @@ favor of good old `npm install`. While yarn should still work just fine,
you should probably align to guarantee builds similar to the ones we
use in testing and across the community in general.
## Superset 0.30.0
* 0.30.0 includes a db_migration that removes allow_run_sync. This may
require downtime because during the migration if the db is migrated first,
superset will get 500 errors when the code can't find the field (until
the deploy finishes).
## Superset 0.29.0
* India was removed from the "Country Map" visualization as the geojson
file included in the package was very large

View File

@@ -117,7 +117,7 @@ that the required dependencies are installed: ::
**OSX**, system python is not recommended. brew's python also ships with pip ::
brew install pkg-config libffi openssl python
env LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include" pip install cryptography==1.9
env LDFLAGS="-L$(brew --prefix openssl)/lib" CFLAGS="-I$(brew --prefix openssl)/include" pip install cryptography==2.4.2
**Windows** isn't officially supported at this point, but if you want to
attempt it, download `get-pip.py <https://bootstrap.pypa.io/get-pip.py>`_, and run ``python get-pip.py`` which may need admin access. Then run the following: ::
@@ -405,6 +405,11 @@ You can also use `PyAthena` library(no java required) like this ::
See `PyAthena <https://github.com/laughingman7743/PyAthena#sqlalchemy>`_.
MSSQL
-----
Full Unicode support requires SQLAlchemy 1.3 or later.
Snowflake
---------
@@ -616,6 +621,18 @@ Upgrading should be as straightforward as running::
superset db upgrade
superset init
We recommend to follow standard best practices when upgrading Superset, such
as taking a database backup prior to the upgrade, upgrading a staging
environment prior to upgrading production, and upgrading production while less
users are active on the platform.
.. note ::
Some upgrades may contain backward-incompatible changes, or require
scheduling downtime, when that is the case, contributors attach notes in
``UPDATING.md`` in the repository. It's recommended to review this
file prior to running an upgrade.
Celery Tasks
------------
On large analytic databases, it's common to run background jobs, reports

View File

@@ -14,7 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
{{- if .Values.ingress.enabled -}}
{{ if .Values.ingress.enabled -}}
{{- $fullName := include "superset.fullname" . -}}
{{- $ingressPath := .Values.ingress.path -}}
apiVersion: extensions/v1beta1

View File

@@ -42,8 +42,8 @@ kombu==4.2.1 # via celery
mako==1.0.7 # via alembic
markdown==3.0
markupsafe==1.0 # via jinja2, mako
numpy==1.15.2 # via pandas
pandas==0.23.1
numpy==1.15.2 # via numpy
pandas==0.23.4 # via pandas
parsedatetime==2.0.0
pathlib2==2.3.0
polyline==1.3.2

View File

@@ -88,20 +88,7 @@ ERRORS="$(cat rat-results.txt | grep -e "??")"
if test ! -z "$ERRORS"; then
echo >&2 "Could not find Apache license headers in the following files:"
echo >&2 "$ERRORS"
COUNT=`echo "${ERRORS}" | wc -l`
if [ ! -f ${TRAVIS_CACHE}/rat-error-count-builds ]; then
[ "${TRAVIS_PULL_REQUEST}" = "false" ] && echo ${COUNT} > ${TRAVIS_CACHE}/rat-error-count-builds
OLD_COUNT=${COUNT}
else
typeset -i OLD_COUNT=$(cat ${TRAVIS_CACHE}/rat-error-count-builds)
fi
if [ ${COUNT} -gt ${OLD_COUNT} ]; then
echo "New missing licenses (${COUNT} vs ${OLD_COUNT}) detected. Please correct them by adding them to to header of your files"
exit 1
else
[ "${TRAVIS_PULL_REQUEST}" = "false" ] && echo ${COUNT} > ${TRAVIS_CACHE}/rat-error-count-builds
fi
exit 0
exit 1
else
echo -e "RAT checks passed."
fi

View File

@@ -71,7 +71,7 @@ setup(
install_requires=[
'bleach>=3.0.2, <4.0.0',
'celery>=4.2.0, <5.0.0',
'click>=6.0, <7.0.0', # click >=7 forces "-" instead of "_"
'click>=6.0, <7.0.0', # `click`>=7 forces "-" instead of "_"
'colorama',
'contextlib2',
'croniter>=0.3.26',
@@ -88,7 +88,7 @@ setup(
'idna',
'isodate',
'markdown>=3.0',
'pandas>=0.18.0',
'pandas>=0.18.0, <0.24.0', # `pandas`>=0.24.0 changes datetimelike API
'parsedatetime',
'pathlib2',
'polyline',
@@ -109,7 +109,7 @@ setup(
'cors': ['flask-cors>=2.0.0'],
'console_log': ['console_log==0.2.10'],
'hive': [
'pyhive>=0.4.0',
'pyhive>=0.6.1',
'tableschema',
'thrift-sasl>=0.2.1',
'thrift>=0.9.3',

View File

@@ -208,6 +208,18 @@ security_manager = appbuilder.sm
results_backend = app.config.get('RESULTS_BACKEND')
# Merge user defined feature flags with default feature flags
feature_flags = app.config.get('DEFAULT_FEATURE_FLAGS')
feature_flags.update(app.config.get('FEATURE_FLAGS') or {})
def is_feature_enabled(feature):
"""
Utility function for checking whether a feature is turned on
"""
return feature_flags.get(feature)
# Registering sources
module_datasource_map = app.config.get('DEFAULT_MODULE_DS_MAP')
module_datasource_map.update(app.config.get('ADDITIONAL_MODULE_DS_MAP'))

View File

@@ -89,8 +89,8 @@ describe('AdhocMetrics', () => {
cy.get('[data-test=metrics]').within(() => {
cy.get('.select-clear').click();
cy.get('.Select-control').click({ force: true });
cy.get('input').type('num{downarrow}', { force: true });
cy.get('.VirtualizedSelectFocusedOption')
cy.get('input').type('num', { force: true });
cy.get('.VirtualizedSelectOption[data-test=_col_num]')
.trigger('mousedown')
.click();
});

View File

@@ -16,6 +16,8 @@
* specific language governing permissions and limitations
* under the License.
*/
import readResponseBlob from '../../../utils/readResponseBlob';
export default () => describe('Area', () => {
const AREA_FORM_DATA = {
datasource: '2__table',
@@ -71,11 +73,12 @@ export default () => describe('Area', () => {
...AREA_FORM_DATA,
groupby: ['region'],
});
cy.get('.nv-area').should('have.length', 7);
});
it('should work with groupby and filter', () => {
verify({
cy.visitChartByParams(JSON.stringify({
...AREA_FORM_DATA,
groupby: ['region'],
adhoc_filters: [{
@@ -88,6 +91,18 @@ export default () => describe('Area', () => {
fromFormData: true,
filterOptionName: 'filter_txje2ikiv6_wxmn0qwd1xo',
}],
}));
cy.wait('@getJson').then(async (xhr) => {
cy.verifyResponseCodes(xhr);
const responseBody = await readResponseBlob(xhr.response.body);
// Make sure data is sorted correctly
const firstRow = responseBody.data[0].values;
const secondRow = responseBody.data[1].values;
expect(firstRow[firstRow.length - 1].y).to.be.greaterThan(secondRow[secondRow.length - 1].y);
cy.verifySliceContainer('svg');
});
cy.get('.nv-area').should('have.length', 2);
});

View File

@@ -98,10 +98,40 @@ export default () => describe('Line', () => {
metrics,
time_compare: ['1+year'],
comparison_type: 'values',
groupby: ['gender'],
};
cy.visitChartByParams(JSON.stringify(formData));
cy.verifySliceSuccess({ waitAlias: '@getJson', chartSelector: 'svg' });
// Offset color should match original line color
cy.get('.nv-legend-text')
.contains('boy')
.siblings()
.first()
.should('have.attr', 'style')
.then((style) => {
cy.get('.nv-legend-text')
.contains('boy, 1 year offset')
.siblings()
.first()
.should('have.attr', 'style')
.and('eq', style);
});
cy.get('.nv-legend-text')
.contains('girl')
.siblings()
.first()
.should('have.attr', 'style')
.then((style) => {
cy.get('.nv-legend-text')
.contains('girl, 1 year offset')
.siblings()
.first()
.should('have.attr', 'style')
.and('eq', style);
});
});
it('Test line chart with time shift yoy', () => {

View File

@@ -2228,7 +2228,7 @@
"dependencies": {
"whatwg-fetch": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
"resolved": "http://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz",
"integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng=="
}
}

View File

@@ -0,0 +1,110 @@
/**
* 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 { safeStringify } from '../../../src/utils/safeStringify';
class Noise {
public next: Noise;
}
describe('Stringify utility testing', () => {
it('correctly parses a simple object just like JSON', () => {
const noncircular = {
b: 'foo',
c: 'bar',
d: [
{
e: 'hello',
f: ['world'],
},
{
e: 'hello',
f: ['darkness', 'my', 'old', 'friend'],
},
],
};
expect(safeStringify(noncircular)).toEqual(JSON.stringify(noncircular));
// Checking that it works with quick-deepish-copies as well.
expect(JSON.parse(safeStringify(noncircular))).toEqual(JSON.parse(JSON.stringify(noncircular)));
});
it('handles simple circular json as expected', () => {
const ping = new Noise();
const pong = new Noise();
const pang = new Noise();
ping.next = pong;
pong.next = ping;
// ping.next is pong (the circular reference) now
const safeString = safeStringify(ping);
ping.next = pang;
// ping.next is pang now, which has no circular reference, so it's safe to use JSON.stringify
const ordinaryString = JSON.stringify(ping);
expect(safeString).toEqual(ordinaryString);
});
it('creates a parseable object even when the input is circular', () => {
const ping = new Noise();
const pong = new Noise();
ping.next = pong;
pong.next = ping;
const newNoise: Noise = JSON.parse(safeStringify(ping));
expect(newNoise).toBeTruthy();
expect(newNoise.next).toEqual({});
});
it('does not remove noncircular duplicates', () => {
const a = {
foo: 'bar',
};
const repeating = {
first: a,
second: a,
third: a,
};
expect(safeStringify(repeating)).toEqual(JSON.stringify(repeating));
});
it('does not remove nodes with empty objects', () => {
const emptyObjectValues = {
a: {},
b: 'foo',
c: {
d: 'good data here',
e: {},
},
};
expect(safeStringify(emptyObjectValues)).toEqual(JSON.stringify(emptyObjectValues));
});
it('does not remove nested same keys', () => {
const nestedKeys = {
a: 'b',
c: {
a: 'd',
x: 'y',
},
};
expect(safeStringify(nestedKeys)).toEqual(JSON.stringify(nestedKeys));
});
});

View File

@@ -72,6 +72,8 @@ class QueryAutoRefresh extends React.PureComponent {
}).catch(() => {
this.props.actions.setUserOffline(true);
});
} else {
this.props.actions.setUserOffline(false);
}
}
render() {

View File

@@ -152,6 +152,13 @@ export function updateQueryFormData(value, key) {
return { type: UPDATE_QUERY_FORM_DATA, value, key };
}
// in the sql lab -> explore flow, user can inline edit chart title,
// then the chart will be assigned a new slice_id
export const UPDATE_CHART_ID = 'UPDATE_CHART_ID';
export function updateChartId(newId, key = 0) {
return { type: UPDATE_CHART_ID, newId, key };
}
export const ADD_CHART = 'ADD_CHART';
export function addChart(chart, key) {
return { type: ADD_CHART, chart, key };

View File

@@ -168,6 +168,14 @@ export default function chartReducer(charts = {}, action) {
if (action.type === actions.REMOVE_CHART) {
delete charts[action.key];
return charts;
} else if (action.type === actions.UPDATE_CHART_ID) {
const { newId, key } = action;
charts[newId] = {
...charts[key],
id: newId,
};
delete charts[key];
return charts;
}
if (action.type in actionHandlers) {

View File

@@ -18,49 +18,26 @@
*/
import React from 'react';
import PropTypes from 'prop-types';
import { Label } from 'react-bootstrap';
import TooltipWrapper from './TooltipWrapper';
import './RefreshLabel.less';
const propTypes = {
onClick: PropTypes.func,
className: PropTypes.string,
tooltipContent: PropTypes.string.isRequired,
};
class RefreshLabel extends React.PureComponent {
constructor(props) {
super(props);
this.state = {
hovered: false,
};
}
mouseOver() {
this.setState({ hovered: true });
}
mouseOut() {
this.setState({ hovered: false });
}
render() {
const labelStyle = this.state.hovered ? 'primary' : 'default';
const tooltip = 'Click to ' + this.props.tooltipContent;
return (
<TooltipWrapper
tooltip={tooltip}
tooltip={this.props.tooltipContent}
label="cache-desc"
>
<Label
className={this.props.className}
bsStyle={labelStyle}
style={{ fontSize: '13px', marginRight: '5px', cursor: 'pointer' }}
<i
className="RefreshLabel fa fa-refresh pointer"
onClick={this.props.onClick}
onMouseOver={this.mouseOver.bind(this)}
onMouseOut={this.mouseOut.bind(this)}
>
<i className="fa fa-refresh" />
</Label>
/>
</TooltipWrapper>);
}
}

View File

@@ -0,0 +1,27 @@
/**
* 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 "../../stylesheets/less/cosmo/variables.less";
.RefreshLabel:hover {
color: @brand-primary;
}
.RefreshLabel {
color: @gray-light;
}

View File

@@ -0,0 +1,24 @@
/**
* 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.
*/
.TableSelector .fa-refresh {
padding-top: 7px
}
.TableSelector .refresh-col {
padding-left: 0px;
}

View File

@@ -20,12 +20,13 @@ import React from 'react';
import PropTypes from 'prop-types';
import Select from 'react-virtualized-select';
import createFilterOptions from 'react-select-fast-filter-options';
import { ControlLabel, Col, Label } from 'react-bootstrap';
import { ControlLabel, Col, Label, Row } from 'react-bootstrap';
import { t } from '@superset-ui/translation';
import { SupersetClient } from '@superset-ui/connection';
import AsyncSelect from './AsyncSelect';
import RefreshLabel from './RefreshLabel';
import './TableSelector.css';
const propTypes = {
dbId: PropTypes.number.isRequired,
@@ -196,8 +197,16 @@ export default class TableSelector extends React.PureComponent {
{db.database_name}
</span>);
}
renderDatabaseSelect() {
renderSelectRow(select, refreshBtn) {
return (
<Row>
<Col md={11}>{select}</Col>
<Col md={1} className="refresh-col">{refreshBtn}</Col>
</Row>
);
}
renderDatabaseSelect() {
return this.renderSelectRow(
<AsyncSelect
dataEndpoint={
'/databaseasync/api/' +
@@ -225,30 +234,26 @@ export default class TableSelector extends React.PureComponent {
renderSchema() {
return (
<div className="m-t-5">
<div className="row">
<div className="col-md-11 col-xs-11 p-r-2">
<Select
name="select-schema"
placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
options={this.state.schemaOptions}
value={this.props.schema}
valueRenderer={o => (
<div>
<span className="text-muted">{t('Schema:')}</span> {o.label}
</div>
)}
isLoading={this.state.schemaLoading}
autosize={false}
onChange={this.changeSchema}
/>
</div>
<div className="col-md-1 col-xs-1 p-l-0 p-t-8">
<RefreshLabel
onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
tooltipContent={t('force refresh schema list')}
/>
</div>
</div>
{this.renderSelectRow(
<Select
name="select-schema"
placeholder={t('Select a schema (%s)', this.state.schemaOptions.length)}
options={this.state.schemaOptions}
value={this.props.schema}
valueRenderer={o => (
<div>
<span className="text-muted">{t('Schema:')}</span> {o.label}
</div>
)}
isLoading={this.state.schemaLoading}
autosize={false}
onChange={this.changeSchema}
/>,
<RefreshLabel
onClick={() => this.onDatabaseChange({ id: this.props.dbId }, true)}
tooltipContent={t('Force refresh schema list')}
/>,
)}
</div>
);
}
@@ -262,43 +267,37 @@ export default class TableSelector extends React.PureComponent {
tableSelectDisabled = true;
}
const options = this.addOptionIfMissing(this.state.tableOptions, this.state.tableName);
const select = this.props.schema ? (
<Select
name="select-table"
ref="selectTable"
isLoading={this.state.tableLoading}
placeholder={t('Select table or type table name')}
autosize={false}
onChange={this.changeTable}
filterOptions={this.state.filterOptions}
options={options}
value={this.state.tableName}
/>) : (
<Select
async
name="async-select-table"
ref="selectTable"
placeholder={tableSelectPlaceholder}
disabled={tableSelectDisabled}
autosize={false}
onChange={this.changeTable}
value={this.state.tableName}
loadOptions={this.getTableNamesBySubStr}
/>);
return (
<div className="m-t-5">
<div className="row">
<div className="col-md-11 col-xs-11 p-r-2">
{this.props.schema ? (
<Select
name="select-table"
ref="selectTable"
isLoading={this.state.tableLoading}
placeholder={t('Select table or type table name')}
autosize={false}
onChange={this.changeTable}
filterOptions={this.state.filterOptions}
options={options}
value={this.state.tableName}
/>
) : (
<Select
async
name="async-select-table"
ref="selectTable"
placeholder={tableSelectPlaceholder}
disabled={tableSelectDisabled}
autosize={false}
onChange={this.changeTable}
value={this.state.tableName}
loadOptions={this.getTableNamesBySubStr}
/>
)}
</div>
<div className="col-md-1 col-xs-1 p-l-0 p-t-8">
<RefreshLabel
onClick={() => this.changeSchema({ value: this.props.schema }, true)}
tooltipContent={t('force refresh table list')}
/>
</div>
</div>
{this.renderSelectRow(
select,
<RefreshLabel
onClick={() => this.changeSchema({ value: this.props.schema }, true)}
tooltipContent={t('Force refresh table list')}
/>)}
</div>);
}
renderSeeTableLabel() {
@@ -318,21 +317,22 @@ export default class TableSelector extends React.PureComponent {
</div>);
}
render() {
if (this.props.horizontal) {
return (
<div>
<Col md={4}>{this.renderDatabaseSelect()}</Col>
<Col md={4}>{this.renderSchema()}</Col>
<Col md={4}>{this.renderTable()}</Col>
</div>);
}
return (
<div>
<div>{this.renderDatabaseSelect()}</div>
<div className="m-t-5">{this.renderSchema()}</div>
{this.props.sqlLabMode && this.renderSeeTableLabel()}
<div className="m-t-5">{this.renderTable()}</div>
</div>);
<div className="TableSelector">
{this.props.horizontal ?
<div>
<Col md={4}>{this.renderDatabaseSelect()}</Col>
<Col md={4}>{this.renderSchema()}</Col>
<Col md={4}>{this.renderTable()}</Col>
</div> :
<div>
<div>{this.renderDatabaseSelect()}</div>
<div className="m-t-5">{this.renderSchema()}</div>
{this.props.sqlLabMode && this.renderSeeTableLabel()}
<div className="m-t-5">{this.renderTable()}</div>
</div>}
</div>
);
}
}
TableSelector.propTypes = propTypes;

View File

@@ -55,6 +55,7 @@ export default function VirtualizedRendererWrap(renderer) {
key={key}
style={{ ...(option.style || {}), ...style }}
title={option.title}
data-test={option.optionName}
{...events}
>
{renderer(option)}

View File

@@ -65,6 +65,7 @@ class ExploreChartHeader extends React.PureComponent {
.then((json) => {
const { data } = json;
if (isNewSlice) {
this.props.actions.updateChartId(data.slice.slice_id, 0);
this.props.actions.createNewSlice(
data.can_add, data.can_download, data.can_overwrite,
data.slice, data.form_data);

View File

@@ -109,6 +109,9 @@ class ExploreViewContainer extends React.Component {
const wasRendered =
['rendered', 'failed', 'stopped'].indexOf(this.props.chart.chartStatus) > -1;
const isRendered = ['rendered', 'failed', 'stopped'].indexOf(nextProps.chart.chartStatus) > -1;
if (nextProps.chart.id !== this.props.chart.id) {
this.loadingLog.sourceId = nextProps.chart.id;
}
if (!wasRendered && isRendered) {
Logger.send(this.loadingLog);
}

View File

@@ -176,10 +176,12 @@ AnnotationLayerControl.defaultProps = defaultProps;
// directly, could not figure out how to get access to the color_scheme
function mapStateToProps({ charts, explore }) {
const chartKey = getChartKey(explore);
const chart = charts[chartKey] || charts[0] || {};
return {
colorScheme: (explore.controls || {}).color_scheme.value,
annotationError: charts[chartKey].annotationError,
annotationQuery: charts[chartKey].annotationQuery,
annotationError: chart.annotationError,
annotationQuery: chart.annotationQuery,
vizType: explore.controls.viz_type.value,
};
}

View File

@@ -19,6 +19,7 @@
/* eslint camelcase: 0 */
import URI from 'urijs';
import { availableDomains } from '../utils/hostNamesConfig';
import { safeStringify } from '../utils/safeStringify';
const MAX_URL_LENGTH = 8000;
@@ -71,7 +72,7 @@ export function getExploreLongUrl(formData, endpointType, allowOverflow = true,
Object.keys(extraSearch).forEach((key) => {
search[key] = extraSearch[key];
});
search.form_data = JSON.stringify(formData);
search.form_data = safeStringify(formData);
if (endpointType === 'standalone') {
search.standalone = 'true';
}

View File

@@ -0,0 +1,45 @@
/**
* 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.
*/
/**
* A Stringify function that will not crash when it runs into circular JSON references,
* unlike JSON.stringify. Any circular references are simply omitted, as if there had
* been no data present
* @param object any JSON object to be stringified
*/
export function safeStringify(object: any): string {
const cache = new Set();
return JSON.stringify(object, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (cache.has(value)) {
// We've seen this object before
try {
// Quick deep copy to duplicate if this is a repeat rather than a circle.
return JSON.parse(JSON.stringify(value));
} catch (err) {
// Discard key if value cannot be duplicated.
return;
}
}
// Store the value in our cache.
cache.add(value);
}
return value;
});
}

View File

@@ -0,0 +1,58 @@
/**
* 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 Mustache from 'mustache';
import React from 'react';
import PropTypes from 'prop-types';
const propTypes = {
className: PropTypes.string,
width: PropTypes.number.isRequired,
height: PropTypes.number.isRequired,
url: PropTypes.string,
};
const defaultProps = {
className: '',
};
class Iframe extends React.PureComponent {
render() {
const { className, url, width, height } = this.props;
const completeUrl = Mustache.render(url, {
width,
height,
});
return (
<iframe
className={className}
title="superset-iframe"
src={completeUrl}
style={{
width: '100%',
height,
}}
/>
);
}
}
Iframe.propTypes = propTypes;
Iframe.defaultProps = defaultProps;
export default Iframe;

View File

@@ -0,0 +1,38 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import thumbnail from './images/thumbnail.png';
import transformProps from './transformProps';
const metadata = new ChartMetadata({
name: t('IFrame'),
description: 'HTML Inline Frame',
thumbnail,
});
export default class IframeChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Iframe.jsx'),
transformProps,
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@@ -16,23 +16,13 @@
* specific language governing permissions and limitations
* under the License.
*/
import Mustache from 'mustache';
export default function iframeWidget(slice) {
const { selector, formData } = slice;
export default function transformProps(chartProps) {
const { width, height, formData } = chartProps;
const { url } = formData;
const width = slice.width();
const height = slice.height();
const container = document.querySelector(selector);
const completedUrl = Mustache.render(url, {
return {
width,
height,
});
const iframe = document.createElement('iframe');
iframe.style.width = '100%';
iframe.style.height = height;
iframe.setAttribute('src', completedUrl);
container.appendChild(iframe);
url,
};
}

View File

@@ -17,16 +17,16 @@
* under the License.
*/
.markup.slice_container {
margin: 10px;
margin: 10px;
}
.separator {
background-color: transparent !important;
background-color: transparent !important;
}
.separator hr {
border: 0;
height: 1px;
background-image: linear-gradient(to right, rgba(0, 0, 0, 1), rgba(0, 0, 0, 1), rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
border: 0;
height: 1px;
background-image: linear-gradient(to right, rgba(0, 0, 0, 1), rgba(0, 0, 0, 1), rgba(0, 0, 0, 1), rgba(0, 0, 0, 0));
}
.separator .chart-header {
border: none !important;
border: none !important;
}

View File

@@ -0,0 +1,76 @@
/**
* 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 React from 'react';
import PropTypes from 'prop-types';
import './Markup.css';
const propTypes = {
className: PropTypes.string,
height: PropTypes.number.isRequired,
isSeparator: PropTypes.bool,
html: PropTypes.string,
cssFiles: PropTypes.arrayOf(PropTypes.string),
};
const defaultProps = {
className: '',
isSeparator: false,
html: '',
};
const CONTAINER_STYLE = {
position: 'relative',
overflow: 'auto',
};
class Markup extends React.PureComponent {
render() {
const { className, height, isSeparator, html, cssFiles } = this.props;
return (
<div
className={className}
style={CONTAINER_STYLE}
>
<iframe
title="superset-markup"
frameBorder={0}
height={isSeparator ? height - 20 : height}
sandbox="allow-forms allow-popups allow-same-origin allow-scripts allow-top-navigation"
srcDoc={`
<html>
<head>
${cssFiles.map(
href => `<link rel="stylesheet" type="text/css" href="${href}" />`,
)}
</head>
<body style="background-color: transparent;">
${html}
</body>
</html>`
}
/>
</div>
);
}
}
Markup.propTypes = propTypes;
Markup.defaultProps = defaultProps;
export default Markup;

View File

@@ -0,0 +1,38 @@
/**
* 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 { t } from '@superset-ui/translation';
import { ChartMetadata, ChartPlugin } from '@superset-ui/chart';
import thumbnail from './images/thumbnail.png';
import transformProps from './transformProps';
const metadata = new ChartMetadata({
name: t('Markup'),
description: 'HTML Markup',
thumbnail,
});
export default class IframeChartPlugin extends ChartPlugin {
constructor() {
super({
metadata,
loadChart: () => import('./Markup.jsx'),
transformProps,
});
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@@ -0,0 +1,33 @@
/**
* 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.
*/
export default function transformProps(chartProps) {
const { height, payload, formData } = chartProps;
const { vizType } = formData;
const {
theme_css: cssFiles,
html,
} = payload.data;
return {
height,
cssFiles,
html,
isSeparator: vizType === 'separator',
};
}

View File

@@ -70,7 +70,7 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
*/
constructor(props) {
super(props);
this.state = this.getInitialStateFromProps(props);
this.state = this.getStateFromProps(props);
this.getLayers = this.getLayers.bind(this);
this.onValuesChange = this.onValuesChange.bind(this);
@@ -78,6 +78,11 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
this.toggleCategory = this.toggleCategory.bind(this);
this.showSingleCategory = this.showSingleCategory.bind(this);
}
componentWillReceiveProps(nextProps) {
if (nextProps.payload.form_data !== this.state.formData) {
this.setState({ ...this.getStateFromProps(nextProps) });
}
}
onValuesChange(values) {
this.setState({
values: Array.isArray(values)
@@ -88,7 +93,7 @@ export default class CategoricalDeckGLContainer extends React.PureComponent {
onViewportChange(viewport) {
this.setState({ viewport });
}
getInitialStateFromProps(props, state) {
getStateFromProps(props, state) {
const features = props.payload.data.features || [];
const timestamps = features.map(f => f.__timestamp);
const categories = getCategories(props.formData, features);

View File

@@ -1,58 +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 srcdoc from 'srcdoc-polyfill';
import './markup.css';
function markupWidget(slice, payload) {
const { selector } = slice;
const height = slice.height();
const headerHeight = slice.headerHeight();
const vizType = slice.props.vizType;
const { data } = payload;
const container = document.querySelector(selector);
container.style.overflow = 'auto';
// markup height is slice height - (marginTop + marginBottom)
const iframeHeight = vizType === 'separator'
? height - 20
: height + headerHeight;
const html = `
<html>
<head>
${data.theme_css.map(
href => `<link rel="stylesheet" type="text/css" href="${href}" />`,
)}
</head>
<body style="background-color: transparent;">
${data.html}
</body>
</html>`;
const iframe = document.createElement('iframe');
iframe.setAttribute('frameborder', 0);
iframe.setAttribute('height', iframeHeight);
iframe.setAttribute('sandbox', 'allow-forms allow-popups allow-same-origin allow-scripts allow-top-navigation');
container.appendChild(iframe);
srcdoc.set(iframe, html);
}
export default markupWidget;

View File

@@ -166,9 +166,9 @@ export function generateBubbleTooltipContent({
}
export function hideTooltips() {
const target = document.querySelector('.nvtooltip');
if (target) {
target.style.opacity = 0;
const targets = document.querySelectorAll('.nvtooltip');
if (targets.length > 0) {
targets.forEach(t => t.remove());
}
}

View File

@@ -30,7 +30,9 @@ import EventFlowChartPlugin from '../EventFlow/EventFlowChartPlugin';
import ForceDirectedChartPlugin from '../ForceDirected/ForceDirectedChartPlugin';
import HeatmapChartPlugin from '../Heatmap/HeatmapChartPlugin';
import HorizonChartPlugin from '../Horizon/HorizonChartPlugin';
import IframeChartPlugin from '../Iframe/IframeChartPlugin';
import LineMultiChartPlugin from '../nvd3/LineMulti/LineMultiChartPlugin';
import MarkupChartPlugin from '../Markup/MarkupChartPlugin';
import PairedTTestChartPlugin from '../PairedTTest/PairedTTestChartPlugin';
import ParallelCoordinatesChartPlugin from '../ParallelCoordinates/ParallelCoordinatesChartPlugin';
import RoseChartPlugin from '../Rose/RoseChartPlugin';
@@ -57,7 +59,10 @@ export default class LegacyChartPreset extends Preset {
new ForceDirectedChartPlugin().configure({ key: 'directed_force' }),
new HeatmapChartPlugin().configure({ key: 'heatmap' }),
new HorizonChartPlugin().configure({ key: 'horizon' }),
new IframeChartPlugin().configure({ key: 'iframe' }),
new LineMultiChartPlugin().configure({ key: 'line_multi' }),
new MarkupChartPlugin().configure({ key: 'markup' }),
new MarkupChartPlugin().configure({ key: 'separator' }),
new PairedTTestChartPlugin().configure({ key: 'paired_ttest' }),
new ParallelCoordinatesChartPlugin().configure({ key: 'para' }),
new RoseChartPlugin().configure({ key: 'rose' }),

View File

@@ -71,7 +71,7 @@ class DashboardTable extends React.PureComponent {
{this.state.dashboards.map(o => (
<Tr key={o.id}>
<Td column="dashboard" value={o.dashboard_title}>
{o.dashboard_title}
<a href={o.url}>{o.dashboard_title}</a>
</Td>
<Td column="creator" value={o.changed_by_name}>
{unsafe(o.creator)}

View File

@@ -475,20 +475,17 @@ g.annotation-container {
position:relative;
padding-right: 40px;
}
.reactable-header-sortable::before{
.reactable-header-sortable::after {
font: normal normal normal 14px/1 FontAwesome;
content: "\f0dc";
position: absolute;
top: 6px;
right: 5px;
margin-left: 10px;
color: @brand-primary;
}
.reactable-header-sort-asc::before{
.reactable-header-sort-asc::after{
content: "\f0de";
color: @brand-primary;
}
.reactable-header-sort-desc::before{
.reactable-header-sort-desc::after{
content: "\f0dd";
color: @brand-primary;
}

View File

@@ -110,6 +110,7 @@ APP_NAME = 'Superset'
# Uncomment to setup an App icon
APP_ICON = '/static/assets/images/superset-logo@2x.png'
APP_ICON_WIDTH = 126
# Druid query timezone
# tz.tzutc() : Using utc timezone
@@ -180,14 +181,18 @@ LANGUAGES = {
'pt': {'flag': 'pt', 'name': 'Portuguese'},
'pt_BR': {'flag': 'br', 'name': 'Brazilian Portuguese'},
'ru': {'flag': 'ru', 'name': 'Russian'},
'ko': {'flag': 'kr', 'name': 'Korean'},
}
# ---------------------------------------------------
# Feature flags
# ---------------------------------------------------
# Feature flags that are on by default go here. Their
# values can be overridden by those in super_config.py
FEATURE_FLAGS = {}
# Feature flags that are set by default go here. Their values can be
# overwritten by those specified under FEATURE_FLAGS in super_config.py
# For example, DEFAULT_FEATURE_FLAGS = { 'FOO': True, 'BAR': False } here
# and FEATURE_FLAGS = { 'BAR': True, 'BAZ': True } in superset_config.py
# will result in combined feature flags of { 'FOO': True, 'BAR': True, 'BAZ': True }
DEFAULT_FEATURE_FLAGS = {}
# ---------------------------------------------------
# Image and file configuration
@@ -558,6 +563,9 @@ WEBDRIVER_CONFIGURATION = {}
# The base URL to query for accessing the user interface
WEBDRIVER_BASEURL = 'http://0.0.0.0:8080/'
# Send user to a link where they can report bugs
BUG_REPORT_URL = None
try:
if CONFIG_PATH_ENV_VAR in os.environ:

View File

@@ -41,7 +41,7 @@ from . import models
class DruidColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa
datamodel = SQLAInterface(models.DruidColumn)
list_title = _('List Druid Column')
list_title = _('Columns')
show_title = _('Show Druid Column')
add_title = _('Add Druid Column')
edit_title = _('Edit Druid Column')
@@ -109,7 +109,7 @@ appbuilder.add_view_no_menu(DruidColumnInlineView)
class DruidMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa
datamodel = SQLAInterface(models.DruidMetric)
list_title = _('List Druid Metric')
list_title = _('Metrics')
show_title = _('Show Druid Metric')
add_title = _('Add Druid Metric')
edit_title = _('Edit Druid Metric')
@@ -160,7 +160,7 @@ appbuilder.add_view_no_menu(DruidMetricInlineView)
class DruidClusterModelView(SupersetModelView, DeleteMixin, YamlExportMixin): # noqa
datamodel = SQLAInterface(models.DruidCluster)
list_title = _('List Druid Cluster')
list_title = _('Druid Clusters')
show_title = _('Show Druid Cluster')
add_title = _('Add Druid Cluster')
edit_title = _('Edit Druid Cluster')
@@ -212,7 +212,7 @@ appbuilder.add_view(
class DruidDatasourceModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
datamodel = SQLAInterface(models.DruidDatasource)
list_title = _('List Druid Datasource')
list_title = _('Druid Datasources')
show_title = _('Show Druid Datasource')
add_title = _('Add Druid Datasource')
edit_title = _('Edit Druid Datasource')

View File

@@ -115,7 +115,9 @@ class TableColumn(Model, BaseColumn):
label = label if label else self.column_name
label = self.table.get_label(label)
if not self.expression:
col = column(self.column_name).label(label)
db_engine_spec = self.table.database.db_engine_spec
type_ = db_engine_spec.get_sqla_column_type(self.type)
col = column(self.column_name, type_=type_).label(label)
else:
col = literal_column(self.expression).label(label)
return col
@@ -856,13 +858,13 @@ class SqlaTable(Model, BaseDatasource):
dbcol = dbcols.get(col.name, None)
if not dbcol:
dbcol = TableColumn(column_name=col.name, type=datatype)
dbcol.groupby = dbcol.is_string
dbcol.filterable = dbcol.is_string
dbcol.sum = dbcol.is_num
dbcol.avg = dbcol.is_num
dbcol.is_dttm = dbcol.is_time
else:
dbcol.type = datatype
dbcol.groupby = True
dbcol.filterable = True
self.columns.append(dbcol)
if not any_date_col and dbcol.is_time:
any_date_col = col.name

View File

@@ -16,6 +16,8 @@
# under the License.
# pylint: disable=C,R,W
"""Views used by the SqlAlchemy connector"""
import logging
from flask import flash, Markup, redirect
from flask_appbuilder import CompactCRUDMixin, expose
from flask_appbuilder.actions import action
@@ -33,11 +35,13 @@ from superset.views.base import (
)
from . import models
logger = logging.getLogger(__name__)
class TableColumnInlineView(CompactCRUDMixin, SupersetModelView): # noqa
datamodel = SQLAInterface(models.TableColumn)
list_title = _('List Columns')
list_title = _('Columns')
show_title = _('Show Column')
add_title = _('Add Column')
edit_title = _('Edit Column')
@@ -110,7 +114,7 @@ appbuilder.add_view_no_menu(TableColumnInlineView)
class SqlMetricInlineView(CompactCRUDMixin, SupersetModelView): # noqa
datamodel = SQLAInterface(models.SqlMetric)
list_title = _('List Metrics')
list_title = _('Metrics')
show_title = _('Show Metric')
add_title = _('Add Metric')
edit_title = _('Edit Metric')
@@ -164,7 +168,7 @@ appbuilder.add_view_no_menu(SqlMetricInlineView)
class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
datamodel = SQLAInterface(models.SqlaTable)
list_title = _('List Tables')
list_title = _('Tables')
show_title = _('Show Table')
add_title = _('Import a table definition')
edit_title = _('Edit Table')
@@ -269,12 +273,13 @@ class TableModelView(DatasourceModelView, DeleteMixin, YamlExportMixin): # noqa
# Fail before adding if the table can't be found
try:
table.get_sqla_table_object()
except Exception:
except Exception as e:
logger.exception(f'Got an error in pre_add for {table.name}')
raise Exception(_(
'Table [{}] could not be found, '
'please double check your '
'database connection, schema, and '
'table name').format(table.name))
'table name, error: {}').format(table.name, str(e)))
def post_add(self, table, flash_message=True):
table.fetch_metadata()

View File

@@ -46,6 +46,7 @@ from sqlalchemy.engine import create_engine
from sqlalchemy.engine.url import make_url
from sqlalchemy.sql import quoted_name, text
from sqlalchemy.sql.expression import TextAsFrom
from sqlalchemy.types import UnicodeText
import sqlparse
from werkzeug.utils import secure_filename
@@ -400,6 +401,15 @@ class BaseEngineSpec(object):
label = cls.mutate_label(label)
return quoted_name(label, True) if cls.force_column_alias_quotes else label
@classmethod
def get_sqla_column_type(cls, type_):
"""
Return a sqlalchemy native column type that corresponds to the column type
defined in the data source (optional). Needs to be overridden if column requires
special handling (see MSSQL for example of NCHAR/NVARCHAR handling).
"""
return None
@staticmethod
def mutate_label(label):
"""
@@ -1362,6 +1372,12 @@ class MssqlEngineSpec(BaseEngineSpec):
data = [[elem for elem in r] for r in data]
return data
@classmethod
def get_sqla_column_type(cls, type_):
if isinstance(type_, str) and re.match(r'^N(VAR){0-1}CHAR', type_):
return UnicodeText()
return None
class AthenaEngineSpec(BaseEngineSpec):
engine = 'awsathena'

View File

@@ -1,3 +1,19 @@
# 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.
"""Add extra column to SavedQuery
Licensed to the Apache Software Foundation (ASF) under one or more

View File

@@ -801,7 +801,9 @@ class Database(Model, AuditMixinNullable, ImportMixin):
self.impersonate_user,
effective_username))
if configuration:
params['connect_args'] = {'configuration': configuration}
d = params.get('connect_args', {})
d['configuration'] = configuration
params['connect_args'] = d
DB_CONNECTION_MUTATOR = config.get('DB_CONNECTION_MUTATOR')
if DB_CONNECTION_MUTATOR:

View File

@@ -19,6 +19,7 @@
{% set menu = appbuilder.menu %}
{% set languages = appbuilder.languages %}
{% set WARNING_MSG = appbuilder.app.config.get('WARNING_MSG') %}
{% set app_icon_width = appbuilder.app.config.get('APP_ICON_WIDTH', 126) %}
<div class="navbar navbar-static-top {{menu.extra_classes}}" role="navigation">
<div class="container-fluid">
@@ -30,7 +31,7 @@
</button>
<a class="navbar-brand" href="/superset/profile/{{ current_user.username }}/">
<img
width="126"
width="{{ app_icon_width }}"
src="{{ appbuilder.app_icon }}"
alt="{{ appbuilder.app_name }}"
/>

View File

@@ -17,6 +17,7 @@
under the License.
#}
{% set bug_report_url = appbuilder.app.config.get('BUG_REPORT_URL') %}
{% set locale = session['locale'] %}
{% if not locale %}
{% set locale = 'en' %}
@@ -34,6 +35,17 @@
</ul>
</li>
{% endif %}
{% if bug_report_url %}
<li>
<a
tabindex="-1"
href="{{ bug_report_url }}"
title="Report a bug"
>
<i class="fa fa-bug"></i>&nbsp;
</a>
</li>
{% endif %}
{% if languages.keys()|length > 1 %}
<li class="dropdown">
<a class="dropdown-toggle" data-toggle="dropdown" href="javascript:void(0)">

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -31,7 +31,7 @@ from flask_babel import lazy_gettext as _
import simplejson as json
import yaml
from superset import conf, db, security_manager
from superset import conf, db, feature_flags, security_manager
from superset.exceptions import SupersetException, SupersetSecurityException
from superset.translations.utils import get_language_pack
from superset.utils import core as utils
@@ -157,7 +157,7 @@ class BaseSupersetView(BaseView):
'conf': {k: conf.get(k) for k in FRONTEND_CONF_KEYS},
'locale': locale,
'language_pack': get_language_pack(locale),
'feature_flags': conf.get('FEATURE_FLAGS'),
'feature_flags': feature_flags,
}

View File

@@ -146,7 +146,7 @@ class DashboardFilter(SupersetFilter):
class DatabaseView(SupersetModelView, DeleteMixin, YamlExportMixin): # noqa
datamodel = SQLAInterface(models.Database)
list_title = _('List Databases')
list_title = _('Databases')
show_title = _('Show Database')
add_title = _('Add Database')
edit_title = _('Edit Database')
@@ -446,7 +446,7 @@ class SliceModelView(SupersetModelView, DeleteMixin): # noqa
route_base = '/chart'
datamodel = SQLAInterface(models.Slice)
list_title = _('List Charts')
list_title = _('Charts')
show_title = _('Show Chart')
add_title = _('Add Chart')
edit_title = _('Edit Chart')
@@ -562,7 +562,7 @@ class DashboardModelView(SupersetModelView, DeleteMixin): # noqa
route_base = '/dashboard'
datamodel = SQLAInterface(models.Dashboard)
list_title = _('List Dashboards')
list_title = _('Dashboards')
show_title = _('Show Dashboard')
add_title = _('Add Dashboard')
edit_title = _('Edit Dashboard')
@@ -694,7 +694,7 @@ appbuilder.add_view_no_menu(DashboardAddView)
class LogModelView(SupersetModelView):
datamodel = SQLAInterface(models.Log)
list_title = _('List Log')
list_title = _('Logs')
show_title = _('Show Log')
add_title = _('Add Log')
edit_title = _('Edit Log')
@@ -1768,7 +1768,7 @@ class Superset(BaseSupersetView):
.get('engine_params', {}))
connect_args = engine_params.get('connect_args')
if configuration:
if configuration and connect_args is not None:
connect_args['configuration'] = configuration
engine = create_engine(uri, **engine_params)
@@ -2883,10 +2883,10 @@ appbuilder.add_view_no_menu(Superset)
class CssTemplateModelView(SupersetModelView, DeleteMixin):
datamodel = SQLAInterface(models.CssTemplate)
list_title = _('List Css Template')
show_title = _('Show Css Template')
add_title = _('Add Css Template')
edit_title = _('Edit Css Template')
list_title = _('CSS Templates')
show_title = _('Show CSS Template')
add_title = _('Add CSS Template')
edit_title = _('Edit CSS Template')
list_columns = ['template_name']
edit_columns = ['template_name', 'css']

View File

@@ -1254,7 +1254,9 @@ class NVD3TimeSeriesViz(NVD3Viz):
self.to_series(
diff, classed='time-shift-{}'.format(i), title_suffix=label))
return sorted(chart_data, key=lambda x: tuple(x['key']))
if not self.sort_series:
chart_data = sorted(chart_data, key=lambda x: tuple(x['key']))
return chart_data
class MultiLineViz(NVD3Viz):

View File

@@ -19,10 +19,10 @@ import json
import unittest
from flask_appbuilder.security.sqla import models as ab_models
from mock import Mock
from mock import Mock, patch
import pandas as pd
from superset import app, db, security_manager
from superset import app, db, is_feature_enabled, security_manager
from superset.connectors.druid.models import DruidCluster, DruidDatasource
from superset.connectors.sqla.models import SqlaTable
from superset.models import core as models
@@ -185,3 +185,11 @@ class SupersetTestCase(unittest.TestCase):
if raise_on_error and 'error' in resp:
raise Exception('run_sql failed')
return resp
@patch.dict('superset.feature_flags', {'FOO': True}, clear=True)
def test_existing_feature_flags(self):
self.assertTrue(is_feature_enabled('FOO'))
@patch.dict('superset.feature_flags', {}, clear=True)
def test_nonexistent_feature_flags(self):
self.assertFalse(is_feature_enabled('FOO'))

View File

@@ -462,8 +462,8 @@ class CoreTests(SupersetTestCase):
def test_gamma(self):
self.login(username='gamma')
assert 'List Charts' in self.get_resp('/chart/list/')
assert 'List Dashboard' in self.get_resp('/dashboard/list/')
assert 'Charts' in self.get_resp('/chart/list/')
assert 'Dashboards' in self.get_resp('/dashboard/list/')
def test_csv_endpoint(self):
self.login('admin')