Compare commits

...

43 Commits

Author SHA1 Message Date
Kamil Gabryjelski
de615eb79e fix(explore): adhoc metrics popover resets label after hovering outside (#16196)
* fix(explore): adhoc metrics popover resets label after hovering outside

* Remove irrelevant tests and skip rest

* Use ensureIsArray

(cherry picked from commit ccfc95fbe6)
2021-08-11 09:40:25 -07:00
Junlin Chen
9fb638da6f chore: switch back tag name to popular from highly-used (#16174)
* chore: switch back tag name to popular from highly-used

* new package lock

* new package lock with npm 7

* fix lint

* remove package changes

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
(cherry picked from commit 9841c78967)
2021-08-11 09:39:15 -07:00
Phillip Kelley-Dotson
60ceb9213f fix: ensure created user entities do not show inside examples (#16176)
* initial commit

* fix lint

* Update superset-frontend/src/views/CRUD/utils.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* Update superset-frontend/src/views/CRUD/utils.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

* Update superset-frontend/src/views/CRUD/utils.tsx

Co-authored-by: Evan Rusackas <evan@preset.io>

Co-authored-by: Evan Rusackas <evan@preset.io>
(cherry picked from commit a0c9b9d9c2)
2021-08-11 09:38:59 -07:00
Phillip Kelley-Dotson
7458292ca9 fix: change listivew card layouts to the new homepage card layout (#16171)
* initial commit

* removing CardStylesOverrides (unused)

Co-authored-by: Evan Rusackas <evan@preset.io>
(cherry picked from commit a30d884cfc)
2021-08-11 09:38:36 -07:00
AAfghahi
adb3ebbba3 feat: Changing Dataset names (#16199)
* added google alert

* changing Dataset Names

(cherry picked from commit 6c304b83a9)
2021-08-11 09:38:17 -07:00
Elizabeth Thompson
c5f07bc6c1 update covid dashboard (#16183)
(cherry picked from commit 3aefa6925b)
2021-08-11 09:38:00 -07:00
AAfghahi
81d2d32dbf feat: CLI cleanup (#16178)
* added google alert

* removing datasets from cli

(cherry picked from commit 6df16c4b1f)
2021-08-11 09:37:37 -07:00
Elizabeth Thompson
cce369ee00 feat: change query predicate to text (#16160)
* change query predicate to text

* make input multiline

* remove value that is too long for the downgrade

* keep logging lint rule

(cherry picked from commit 628169a171)
2021-08-11 09:37:14 -07:00
David Aaron Suddjian
d95721cb14 fix(dashboard): user id can be null when there is an anonymous user (#15592)
(cherry picked from commit 23072161e2)
2021-08-11 09:36:56 -07:00
Kamil Gabryjelski
0a91bc8c3f fix(explore): revert dnd column dependency array change to fix infinite rerenders (#16115)
* fix(explore): revert dnd column dependency array change to fix infinite rerenders

* Remove console.log

* Remove comment

(cherry picked from commit 772da8de63)
2021-08-11 09:34:09 -07:00
Beto Dealmeida
13f01ac2ab fix: isDynamic function (#16175)
* fix: isDynamic function

* trigger tests

(cherry picked from commit 9f52c103ac)
2021-08-11 09:33:49 -07:00
Beto Dealmeida
705bad9792 fix: revert data endpoint name (#16162)
(cherry picked from commit 7b3fce7e81)
2021-08-11 09:33:23 -07:00
Elizabeth Thompson
86d079b31c add config to hide some user menu items (#16156)
(cherry picked from commit 5488a8a948)
2021-08-11 09:33:05 -07:00
Elizabeth Thompson
b4d4d1cc88 feat: add chart image info to reports from charts (#16158)
* refetch reports on props update

* add chart types to reports

(cherry picked from commit a3102488a1)
2021-08-11 09:32:45 -07:00
Phillip Kelley-Dotson
7c5546586c fix: ensure that users viewing chart does not automatically save edit data (#16077)
* add last_change_at migration

* add last_saved_by db migration

* finish rest of api migration

* run precommit

* fix name

* run precommitt

* remove unused mods

* merge migrations

* Update superset/migrations/versions/6d20ba9ecb33_add_last_saved_at_to_slice_model.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* Update superset/migrations/versions/6d20ba9ecb33_add_last_saved_at_to_slice_model.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* Update superset/migrations/versions/f6196627326f_update_chart_permissions.py

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>

* fix test

* precommit

* remove print

* fix test

* change test

* test commit

* test 2

* test 3

* third time the charm

* fix put req

Co-authored-by: Beto Dealmeida <roberto@dealmeida.net>
(cherry picked from commit f0e3b68cc2)
2021-08-11 09:32:28 -07:00
Phillip Kelley-Dotson
5895af50f5 feat: add sticky state to tables and loadingcards state. (#16102)
* initial feat commit

* fix chart and dash rendering onload

* Update superset-frontend/src/views/CRUD/welcome/Welcome.tsx

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>

* fix jumpyness and add const

Co-authored-by: Michael S. Molina <70410625+michael-s-molina@users.noreply.github.com>
(cherry picked from commit a70248736f)
2021-08-11 09:32:10 -07:00
Michael S. Molina
baeac9dc97 fix: Safari is not showing scroll bars in Explore (#16089)
(cherry picked from commit 273ab3d257)
2021-08-11 09:31:52 -07:00
Ville Brofeldt
9e21009db3 feat(cross-filters): add support for temporal filters (#16139)
* feat(cross-filters): add support for temporal filters

* fix test

* make filter optional

* remove mocks

* fix more tests

* remove unnecessary optionality

* fix even more tests

* bump superset-ui

* add isExtra to schema

* address comments

* fix presto test

(cherry picked from commit 63ace7b288)
2021-08-11 09:31:32 -07:00
Yongjie Zhao
79e4253230 fix: boolean type into SQL 'in' operator (#16107)
* fix: boolean type into SQL 'in' operator

* fix ut

* fix ut again

* update url

* remove blank line

(cherry picked from commit bb1d8fe4ef)
2021-08-11 09:31:14 -07:00
Hugh A. Miles II
520284a4a4 fix: turn on SSL in database edit form show 500 error (#16151)
* fix error for query.update

* converrt before making request

* fix query params

* remove unchanged files

* this

* update tsconfig

(cherry picked from commit 3f86a54ac1)
2021-08-11 09:30:55 -07:00
Lyndsi Kay Williams
bb78d492e9 additional params field fixed (#16161)
(cherry picked from commit 3712ee02fa)
2021-08-11 09:30:36 -07:00
Kamil Gabryjelski
1e9f1de563 chore(explore): bump deckgl to 0.4.9 (#16086)
(cherry picked from commit af204ff449)
2021-08-11 09:30:17 -07:00
Michael S. Molina
d00a2c2899 fix: Fix the Select unselect for object values (#16062)
(cherry picked from commit 1917464d2b)
2021-08-11 09:29:59 -07:00
Kamil Gabryjelski
0b07566346 fix(explore): dnd error when dragging metric if multi: false (#16088)
* fix(explore): dnd error when dragging metric if multi: false

* Fix error for non-dnd controls

(cherry picked from commit b7cc89c6d4)
2021-08-11 09:29:40 -07:00
AAfghahi
c0572c5302 feat: added google alert to DB Connection Form (#16095)
* added google alert

* using superset_text

* made google alert public and others private

* Hugh revisions

(cherry picked from commit a51851308b)
2021-08-11 09:29:14 -07:00
Yongjie Zhao
4b4f6b9c1d fix: virtual dataset wont work (#16132)
(cherry picked from commit 3bbcc30d69)
2021-08-11 09:28:57 -07:00
AAfghahi
ffa4226f10 fix: change Alert Permissions (#16118)
* added google alert

* reworked permissions

(cherry picked from commit 606a7bf429)
2021-08-11 09:28:37 -07:00
AAfghahi
64d54d6fdd feat: better errors for report in charts and dashboard (#16131)
* added google alert

* better errors and report actions

(cherry picked from commit 5ce38839e7)
2021-08-11 09:28:15 -07:00
Geido
fa7a5249ea Adjust width (#16092)
(cherry picked from commit b07c80a839)
2021-08-11 09:27:55 -07:00
Maxime Beauchemin
f8f3b7abbb chore: add stats logging to thumbnail api (#16133)
(cherry picked from commit df50a47777)
2021-08-11 09:27:19 -07:00
Kamil Gabryjelski
83661aee99 chore(explore): change dnd placeholders (#16116)
* chore(explore): change dnd placeholders

* Fix tests and lint

(cherry picked from commit 6ac4f4ef2f)
2021-08-11 09:26:48 -07:00
ʈᵃᵢ
9976250954 fix: move watermark to about section (#16097)
(cherry picked from commit b80f018691)
2021-08-11 09:26:26 -07:00
David Aaron Suddjian
08e64048fa fix(explore): drag & drop column select component triggering onChange unnecessarily (#16073)
* fix(explore): avoid sync until after first render

* fix example

(cherry picked from commit e6292a89bb)
2021-08-11 09:25:57 -07:00
Beto Dealmeida
18f551580e fix: migrate_roles (#16098)
(cherry picked from commit 28c383af68)
2021-08-06 10:08:54 -07:00
Beto Dealmeida
d85875d962 fix: load tabbed dash only for tests (#16091)
(cherry picked from commit b72fd7b9f4)
2021-08-06 10:08:54 -07:00
AAfghahi
31f994b090 change button color (#16093)
(cherry picked from commit e6274e0764)
2021-08-06 10:08:54 -07:00
Beto Dealmeida
a16605cf01 chore: simplify chart permissions (#16078)
(cherry picked from commit 1dbd1e9f02)
2021-08-06 10:08:54 -07:00
Kamil Gabryjelski
e70bcc7283 chore(explore): Create new entrypoints for Echarts Timeseries (#15942)
* feat(explore): Create new entrypoints for Echarts Timeseries

* Change order of some charts

* bump superset-ui

* also bump echarts package

* fix UT

Co-authored-by: Ville Brofeldt <ville.v.brofeldt@gmail.com>
(cherry picked from commit a59d458e41)
2021-08-06 10:08:54 -07:00
Hugh A. Miles II
980c38d1b7 fix: Remove grey bar for TableElement component when metadata is empty (#16054)
* create serialize json function

* remove grey space with no metadata

* remove console log

(cherry picked from commit 11a2d4dfdd)
2021-08-06 10:08:54 -07:00
Ville Brofeldt
87fad13f89 feat(explore): add automatic conditional formatter to pivot table v2 (#16045)
(cherry picked from commit 7ef97a54e2)
2021-08-06 10:08:54 -07:00
AAfghahi
a1a71ee6e1 fix: Adding report bug (#16065)
* report add fix

* added theme

(cherry picked from commit 4359650b7d)
2021-08-06 10:08:54 -07:00
Ville Brofeldt
feab690b8d fix(native-filters): add support for boolean cols to select (#16061)
(cherry picked from commit 86cecaeec5)
2021-08-06 10:08:54 -07:00
Ville Brofeldt
b7d9be449d chore: bump superset-ui to 0.17.78 (#16058)
(cherry picked from commit 7332055ff6)
2021-08-06 10:08:53 -07:00
111 changed files with 2386 additions and 1803 deletions

View File

@@ -118,6 +118,9 @@ services:
depends_on: *superset-depends-on
user: *superset-user
volumes: *superset-volumes
# Bump memory limit if processing selenium / thumbails on superset-worker
# mem_limit: 2038m
# mem_reservation: 128M
superset-worker-beat:
image: *superset-image

View File

@@ -66,7 +66,7 @@ Navigate to **Data ‣ Datasets** and select the **+ Dataset** button in the top
A modal window should pop up in front of you. Select your **Database**,
**Schema**, and **Table** using the drop downs that appear. In the following example,
we register the **cleaned_sales_data** table from the **examples** database.
we register the **Vehicle Sales** table from the **examples** database.
<img src="/images/tutorial_09_add_new_table.png" />

File diff suppressed because it is too large Load Diff

View File

@@ -67,35 +67,35 @@
"@emotion/babel-preset-css-prop": "^11.2.0",
"@emotion/cache": "^11.1.3",
"@emotion/react": "^11.1.5",
"@superset-ui/chart-controls": "^0.17.77",
"@superset-ui/core": "^0.17.75",
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.77",
"@superset-ui/legacy-plugin-chart-chord": "^0.17.77",
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.77",
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.77",
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.77",
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.77",
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.77",
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.77",
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.77",
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.77",
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.77",
"@superset-ui/legacy-plugin-chart-partition": "^0.17.77",
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.77",
"@superset-ui/legacy-plugin-chart-rose": "^0.17.77",
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.77",
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.77",
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.77",
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.77",
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.77",
"@superset-ui/legacy-preset-chart-big-number": "^0.17.77",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.7",
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.77",
"@superset-ui/plugin-chart-echarts": "^0.17.77",
"@superset-ui/plugin-chart-pivot-table": "^0.17.77",
"@superset-ui/plugin-chart-table": "^0.17.77",
"@superset-ui/plugin-chart-word-cloud": "^0.17.77",
"@superset-ui/preset-chart-xy": "^0.17.77",
"@superset-ui/chart-controls": "^0.17.80",
"@superset-ui/core": "^0.17.80",
"@superset-ui/legacy-plugin-chart-calendar": "^0.17.80",
"@superset-ui/legacy-plugin-chart-chord": "^0.17.80",
"@superset-ui/legacy-plugin-chart-country-map": "^0.17.80",
"@superset-ui/legacy-plugin-chart-event-flow": "^0.17.80",
"@superset-ui/legacy-plugin-chart-force-directed": "^0.17.80",
"@superset-ui/legacy-plugin-chart-heatmap": "^0.17.80",
"@superset-ui/legacy-plugin-chart-histogram": "^0.17.80",
"@superset-ui/legacy-plugin-chart-horizon": "^0.17.80",
"@superset-ui/legacy-plugin-chart-map-box": "^0.17.80",
"@superset-ui/legacy-plugin-chart-paired-t-test": "^0.17.80",
"@superset-ui/legacy-plugin-chart-parallel-coordinates": "^0.17.80",
"@superset-ui/legacy-plugin-chart-partition": "^0.17.80",
"@superset-ui/legacy-plugin-chart-pivot-table": "^0.17.80",
"@superset-ui/legacy-plugin-chart-rose": "^0.17.80",
"@superset-ui/legacy-plugin-chart-sankey": "^0.17.80",
"@superset-ui/legacy-plugin-chart-sankey-loop": "^0.17.80",
"@superset-ui/legacy-plugin-chart-sunburst": "^0.17.80",
"@superset-ui/legacy-plugin-chart-treemap": "^0.17.80",
"@superset-ui/legacy-plugin-chart-world-map": "^0.17.80",
"@superset-ui/legacy-preset-chart-big-number": "^0.17.80",
"@superset-ui/legacy-preset-chart-deckgl": "^0.4.9",
"@superset-ui/legacy-preset-chart-nvd3": "^0.17.80",
"@superset-ui/plugin-chart-echarts": "^0.17.80",
"@superset-ui/plugin-chart-pivot-table": "^0.17.80",
"@superset-ui/plugin-chart-table": "^0.17.80",
"@superset-ui/plugin-chart-word-cloud": "^0.17.80",
"@superset-ui/preset-chart-xy": "^0.17.80",
"@vx/responsive": "^0.0.195",
"abortcontroller-polyfill": "^1.1.9",
"antd": "^4.9.4",

View File

@@ -67,60 +67,14 @@ const sumValueAdhocMetric = new AdhocMetric({
label: 'SUM(value)',
});
describe('MetricsControl', () => {
// TODO: rewrite the tests to RTL
describe.skip('MetricsControl', () => {
it('renders Select', () => {
const { component } = setup();
expect(component.find(LabelsContainer)).toExist();
});
describe('constructor', () => {
it('unifies options for the dropdown select with aggregates', () => {
const { component } = setup();
expect(component.state('options')).toEqual([
{
optionName: '_col_source',
type: 'VARCHAR(255)',
column_name: 'source',
},
{
optionName: '_col_target',
type: 'VARCHAR(255)',
column_name: 'target',
},
{ optionName: '_col_value', type: 'DOUBLE', column_name: 'value' },
...Object.keys(AGGREGATES).map(aggregate => ({
aggregate_name: aggregate,
optionName: `_aggregate_${aggregate}`,
})),
{
optionName: 'sum__value',
metric_name: 'sum__value',
expression: 'SUM(energy_usage.value)',
},
{
optionName: 'avg__value',
metric_name: 'avg__value',
expression: 'AVG(energy_usage.value)',
},
]);
});
it('does not show aggregates in options if no columns', () => {
const { component } = setup({ columns: [] });
expect(component.state('options')).toEqual([
{
optionName: 'sum__value',
metric_name: 'sum__value',
expression: 'SUM(energy_usage.value)',
},
{
optionName: 'avg__value',
metric_name: 'avg__value',
expression: 'AVG(energy_usage.value)',
},
]);
});
it('coerces Adhoc Metrics from form data into instances of the AdhocMetric class and leaves saved metrics', () => {
const { component } = setup({
value: [
@@ -178,194 +132,7 @@ describe('MetricsControl', () => {
});
});
describe('checkIfAggregateInInput', () => {
it('handles an aggregate in the input', () => {
const { component } = setup();
expect(component.state('aggregateInInput')).toBeNull();
component.instance().checkIfAggregateInInput('AVG(');
expect(component.state('aggregateInInput')).toBe(AGGREGATES.AVG);
});
it('handles no aggregate in the input', () => {
const { component } = setup();
expect(component.state('aggregateInInput')).toBeNull();
component.instance().checkIfAggregateInInput('colu');
expect(component.state('aggregateInInput')).toBeNull();
});
});
describe('option filter', () => {
it('includes user defined metrics', () => {
const { component } = setup({ datasourceType: 'druid' });
expect(
!!component.instance().selectFilterOption(
{
data: {
metric_name: 'a_metric',
optionName: 'a_metric',
expression: 'SUM(FANCY(metric))',
},
},
'a',
),
).toBe(true);
});
it('includes auto generated avg metrics for druid', () => {
const { component } = setup({ datasourceType: 'druid' });
expect(
!!component.instance().selectFilterOption(
{
data: {
metric_name: 'avg__metric',
optionName: 'avg__metric',
expression: 'AVG(metric)',
},
},
'a',
),
).toBe(true);
});
it('includes columns and aggregates', () => {
const { component } = setup();
expect(
!!component.instance().selectFilterOption(
{
data: {
type: 'VARCHAR(255)',
column_name: 'source',
optionName: '_col_source',
},
},
'sou',
),
).toBe(true);
expect(
!!component
.instance()
.selectFilterOption(
{ data: { aggregate_name: 'AVG', optionName: '_aggregate_AVG' } },
'av',
),
).toBe(true);
});
it('includes columns based on verbose_name', () => {
const { component } = setup();
expect(
!!component.instance().selectFilterOption(
{
data: {
metric_name: 'sum__num',
verbose_name: 'babies',
optionName: '_col_sum_num',
},
},
'bab',
),
).toBe(true);
});
it('excludes auto generated avg metrics for sqla', () => {
const { component } = setup();
expect(
!!component.instance().selectFilterOption(
{
data: {
metric_name: 'avg__metric',
optionName: 'avg__metric',
expression: 'AVG(metric)',
},
},
'a',
),
).toBe(false);
});
it('includes custom made simple saved metrics', () => {
const { component } = setup();
expect(
!!component.instance().selectFilterOption(
{
data: {
metric_name: 'my_fancy_sum_metric',
optionName: 'my_fancy_sum_metric',
expression: 'SUM(value)',
},
},
'sum',
),
).toBe(true);
});
it('excludes auto generated metrics', () => {
const { component } = setup();
expect(
!!component.instance().selectFilterOption(
{
data: {
metric_name: 'sum__value',
optionName: 'sum__value',
expression: 'SUM(value)',
},
},
'sum',
),
).toBe(false);
expect(
!!component.instance().selectFilterOption(
{
data: {
metric_name: 'sum__value',
optionName: 'sum__value',
expression: 'SUM("table"."value")',
},
},
'sum',
),
).toBe(false);
});
it('filters out metrics if the input begins with an aggregate', () => {
const { component } = setup();
component.setState({ aggregateInInput: true });
expect(
!!component.instance().selectFilterOption(
{
data: { metric_name: 'metric', expression: 'SUM(FANCY(metric))' },
},
'SUM(',
),
).toBe(false);
});
it('includes columns if the input begins with an aggregate', () => {
const { component } = setup();
component.setState({ aggregateInInput: true });
expect(
!!component
.instance()
.selectFilterOption(
{ data: { type: 'DOUBLE', column_name: 'value' } },
'SUM(',
),
).toBe(true);
});
it('Removes metrics if savedMetrics changes', () => {
const { props, component, onChange } = setup({
value: [

View File

@@ -50,7 +50,7 @@ describe('VizTypeControl', () => {
new ChartMetadata({
name: 'vis1',
thumbnail: '',
tags: ['Highly-used'],
tags: ['Popular'],
}),
)
.registerValue(

View File

@@ -133,7 +133,8 @@ const TableElement = ({ table, actions, ...props }: TableElementProps) => {
));
}
if (!partitions && !metadata) {
if (!partitions && (!metadata || !metadata.length)) {
// hide partition and metadata card view
return null;
}

View File

@@ -27,12 +27,21 @@ interface CardCollectionProps {
prepareRow: TableInstance['prepareRow'];
renderCard?: (row: any) => React.ReactNode;
rows: TableInstance['rows'];
showThumbnails?: boolean;
}
const CardContainer = styled.div`
display: grid;
grid-template-columns: repeat(auto-fit, minmax(459px, 1fr));
grid-gap: ${({ theme }) => theme.gridUnit * 8}px;
const CardContainer = styled.div<{ showThumbnails?: boolean }>`
${({ theme, showThumbnails }) => `
display: grid;
grid-gap: ${theme.gridUnit * 12}px ${theme.gridUnit * 4}px;
grid-template-columns: repeat(auto-fit, 300px);
margin-top: ${theme.gridUnit * -6}px;
padding: ${
showThumbnails
? `${theme.gridUnit * 8 + 3}px ${theme.gridUnit * 9}px`
: `${theme.gridUnit * 8 + 1}px ${theme.gridUnit * 9}px`
};
`}
`;
const CardWrapper = styled.div`
@@ -51,6 +60,7 @@ export default function CardCollection({
prepareRow,
renderCard,
rows,
showThumbnails,
}: CardCollectionProps) {
function handleClick(
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
@@ -65,7 +75,7 @@ export default function CardCollection({
if (!renderCard) return null;
return (
<CardContainer>
<CardContainer showThumbnails={showThumbnails}>
{loading &&
rows.length === 0 &&
[...new Array(25)].map((e, i) => (

View File

@@ -221,6 +221,7 @@ export interface ListViewProps<T extends object = any> {
cardSortSelectOptions?: Array<CardSortSelectOption>;
defaultViewMode?: ViewModeType;
highlightRowId?: number;
showThumbnails?: boolean;
emptyState?: {
message?: string;
slot?: React.ReactNode;
@@ -242,6 +243,7 @@ function ListView<T extends object = any>({
disableBulkSelect = () => {},
renderBulkSelectCopy = selected => t('%s Selected', selected.length),
renderCard,
showThumbnails,
cardSortSelectOptions,
defaultViewMode = 'card',
highlightRowId,
@@ -376,6 +378,7 @@ function ListView<T extends object = any>({
renderCard={renderCard}
rows={rows}
loading={loading}
showThumbnails={showThumbnails}
/>
)}
{viewMode === 'table' && (

View File

@@ -69,15 +69,6 @@ const StyledAnchor = styled.a`
padding-left: ${({ theme }) => theme.gridUnit}px;
`;
const WaterMark = styled.span`
font-size: 13px;
color: #b0b4c3;
margin: 0 ${({ theme }) => theme.gridUnit * 4}px;
@media (max-width: 1070px) {
display: none;
}
`;
const { SubMenu } = Menu;
interface RightMenuProps {
@@ -95,9 +86,6 @@ const RightMenu = ({
}: RightMenuProps) => (
<StyledDiv align={align}>
<Menu mode="horizontal">
{navbarRight.show_watermark && (
<WaterMark>{t('Powered by Apache Superset')}</WaterMark>
)}
{!navbarRight.user_is_anonymous && (
<SubMenu
data-test="new-dropdown"
@@ -148,9 +136,11 @@ const RightMenu = ({
<a href={navbarRight.user_profile_url}>{t('Profile')}</a>
</Menu.Item>
)}
<Menu.Item key="info">
<a href={navbarRight.user_info_url}>{t('Info')}</a>
</Menu.Item>
{navbarRight.user_info_url && (
<Menu.Item key="info">
<a href={navbarRight.user_info_url}>{t('Info')}</a>
</Menu.Item>
)}
<Menu.Item key="logout">
<a href={navbarRight.user_logout_url}>{t('Logout')}</a>
</Menu.Item>
@@ -160,6 +150,11 @@ const RightMenu = ({
<Menu.Divider key="version-info-divider" />,
<Menu.ItemGroup key="about-section" title={t('About')}>
<div className="about-section">
{navbarRight.show_watermark && (
<div css={versionInfoStyles}>
{t('Powered by Apache Superset')}
</div>
)}
{navbarRight.version_string && (
<div css={versionInfoStyles}>
Version: {navbarRight.version_string}

View File

@@ -18,7 +18,7 @@
*/
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { t, SupersetTheme, css } from '@superset-ui/core';
import { t, SupersetTheme, css, useTheme } from '@superset-ui/core';
import Icons from 'src/components/Icons';
import { Switch } from 'src/components/Switch';
import { AlertObject } from 'src/views/CRUD/alert/types';
@@ -47,6 +47,7 @@ export default function HeaderReportActionsDropDown({
currentReportDeleting,
setCurrentReportDeleting,
] = useState<AlertObject | null>(null);
const theme = useTheme();
const toggleActiveKey = async (data: AlertObject, checked: boolean) => {
if (data?.id) {
@@ -60,7 +61,7 @@ export default function HeaderReportActionsDropDown({
};
const menu = () => (
<Menu selectable={false}>
<Menu selectable={false} css={{ width: '200px' }}>
<Menu.Item>
{t('Email reports active')}
<Switch
@@ -68,6 +69,7 @@ export default function HeaderReportActionsDropDown({
checked={report?.active}
onClick={(checked: boolean) => toggleActiveKey(report, checked)}
size="small"
css={{ marginLeft: theme.gridUnit * 2 }}
/>
</Menu.Item>
<Menu.Item onClick={showReportModal}>{t('Edit email report')}</Menu.Item>

View File

@@ -38,6 +38,13 @@ const defaultProps = {
userEmail: 'test@test.com',
dashboardId: 1,
creationMethod: 'charts_dashboards',
props: {
chart: {
sliceFormData: {
viz_type: 'table',
},
},
},
};
describe('Email Report Modal', () => {

View File

@@ -29,22 +29,28 @@ import { bindActionCreators } from 'redux';
import { connect, useDispatch, useSelector } from 'react-redux';
import { addReport, editReport } from 'src/reports/actions/reports';
import { AlertObject } from 'src/views/CRUD/alert/types';
import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput';
import TimezoneSelector from 'src/components/TimezoneSelector';
import LabeledErrorBoundInput from 'src/components/Form/LabeledErrorBoundInput';
import Icons from 'src/components/Icons';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import { CronPicker, CronError } from 'src/components/CronPicker';
import { CronError } from 'src/components/CronPicker';
import { RadioChangeEvent } from 'src/common/components';
import {
StyledModal,
StyledTopSection,
StyledBottomSection,
StyledIconWrapper,
StyledScheduleTitle,
StyledCronPicker,
StyledCronError,
noBottomMargin,
StyledFooterButton,
TimezoneHeaderStyle,
SectionHeaderStyle,
StyledMessageContentTitle,
StyledRadio,
StyledRadioGroup,
} from './styles';
interface ReportObject {
@@ -67,6 +73,19 @@ interface ReportObject {
creation_method: string;
}
interface ChartObject {
id: number;
chartAlert: string;
chartStatus: string;
chartUpdateEndTime: number;
chartUpdateStartTime: number;
latestQueryFormData: object;
queryController: { abort: () => {} };
queriesResponse: object;
triggerQuery: boolean;
lastRendered: number;
}
interface ReportProps {
addDangerToast: (msg: string) => void;
addSuccessToast: (msg: string) => void;
@@ -77,26 +96,25 @@ interface ReportProps {
userId: number;
userEmail: string;
dashboardId?: number;
chartId?: number;
chart?: ChartObject;
creationMethod: string;
props: any;
}
enum ActionType {
textChange,
inputChange,
fetched,
reset,
}
interface ReportPayloadType {
name: string;
value: string;
}
enum ActionType {
inputChange,
fetched,
reset,
}
type ReportActionType =
| {
type: ActionType.textChange | ActionType.inputChange;
type: ActionType.inputChange;
payload: ReportPayloadType;
}
| {
@@ -107,17 +125,26 @@ type ReportActionType =
type: ActionType.reset;
};
const DEFAULT_NOTIFICATION_FORMAT = 'TEXT';
const TEXT_BASED_VISUALIZATION_TYPES = [
'pivot_table',
'pivot_table_v2',
'table',
'paired_ttest',
];
const reportReducer = (
state: Partial<ReportObject> | null,
action: ReportActionType,
): Partial<ReportObject> | null => {
const initialState = {
name: state?.name || 'Weekly Report',
report_format: state?.report_format || DEFAULT_NOTIFICATION_FORMAT,
...(state || {}),
};
switch (action.type) {
case ActionType.textChange:
case ActionType.inputChange:
return {
...initialState,
[action.payload.name]: action.payload.value,
@@ -139,6 +166,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
show = false,
...props
}) => {
const vizType = props.props.chart?.sliceFormData?.viz_type;
const [currentReport, setCurrentReport] = useReducer<
Reducer<Partial<ReportObject> | null, ReportActionType>
>(reportReducer, null);
@@ -166,7 +194,6 @@ const ReportModal: FunctionComponent<ReportProps> = ({
}
}, [reports]);
const onClose = () => {
// setLoading(false);
onHide();
};
const onSave = async () => {
@@ -174,7 +201,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
const newReportValues: Partial<ReportObject> = {
crontab: currentReport?.crontab,
dashboard: props.props.dashboardId,
chart: props.props.chartId,
chart: props.props.chart?.id,
description: currentReport?.description,
name: currentReport?.name,
owners: [props.props.userId],
@@ -187,9 +214,9 @@ const ReportModal: FunctionComponent<ReportProps> = ({
type: 'Report',
creation_method: props.props.creationMethod,
active: true,
report_format: currentReport?.report_format,
};
// setLoading(true);
if (isEditMode) {
await dispatch(
editReport(currentReport?.id, newReportValues as ReportObject),
@@ -217,7 +244,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
const renderModalFooter = (
<>
<StyledFooterButton key="back" onClick={onClose}>
Cancel
{t('Cancel')}
</StyledFooterButton>
<StyledFooterButton
key="submit"
@@ -225,11 +252,42 @@ const ReportModal: FunctionComponent<ReportProps> = ({
onClick={onSave}
disabled={!currentReport?.name}
>
Add
{isEditMode ? t('Save') : t('Add')}
</StyledFooterButton>
</>
);
const renderMessageContentSection = (
<>
<StyledMessageContentTitle>
<h4>{t('Message Content')}</h4>
</StyledMessageContentTitle>
<div className="inline-container">
<StyledRadioGroup
onChange={(event: RadioChangeEvent) => {
onChange(ActionType.inputChange, {
name: 'report_format',
value: event.target.value,
});
}}
value={currentReport?.report_format || DEFAULT_NOTIFICATION_FORMAT}
>
{TEXT_BASED_VISUALIZATION_TYPES.includes(vizType) && (
<StyledRadio value="TEXT">
{t('Text embedded in email')}
</StyledRadio>
)}
<StyledRadio value="PNG">
{t('Image (PNG) embedded in email')}
</StyledRadio>
<StyledRadio value="CSV">
{t('Formatted CSV attached in email')}
</StyledRadio>
</StyledRadioGroup>
</div>
</>
);
return (
<StyledModal
show={show}
@@ -248,7 +306,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
required
validationMethods={{
onChange: ({ target }: { target: HTMLInputElement }) =>
onChange(ActionType.textChange, {
onChange(ActionType.inputChange, {
name: target.name,
value: target.value,
}),
@@ -266,7 +324,7 @@ const ReportModal: FunctionComponent<ReportProps> = ({
value={currentReport?.description || ''}
validationMethods={{
onChange: ({ target }: { target: HTMLInputElement }) =>
onChange(ActionType.textChange, {
onChange(ActionType.inputChange, {
name: target.name,
value: target.value,
}),
@@ -284,16 +342,16 @@ const ReportModal: FunctionComponent<ReportProps> = ({
<StyledBottomSection>
<StyledScheduleTitle>
<h4 css={(theme: SupersetTheme) => SectionHeaderStyle(theme)}>
Schedule
{t('Schedule')}
</h4>
<p>Scheduled reports will be sent to your email as a PNG</p>
<p>{t('Scheduled reports will be sent to your email as a PNG')}</p>
</StyledScheduleTitle>
<CronPicker
<StyledCronPicker
clearButton={false}
value={currentReport?.crontab || '0 12 * * 1'}
setValue={(newValue: string) => {
onChange(ActionType.textChange, {
onChange(ActionType.inputChange, {
name: 'crontab',
value: newValue,
});
@@ -310,12 +368,13 @@ const ReportModal: FunctionComponent<ReportProps> = ({
<TimezoneSelector
onTimezoneChange={value => {
setCurrentReport({
type: ActionType.textChange,
type: ActionType.inputChange,
payload: { name: 'timezone', value },
});
}}
timezone={currentReport?.timezone}
/>
{props.props.chart && renderMessageContentSection}
</StyledBottomSection>
</StyledModal>
);

View File

@@ -20,11 +20,17 @@
import { styled, css, SupersetTheme } from '@superset-ui/core';
import Modal from 'src/components/Modal';
import Button from 'src/components/Button';
import { Radio } from 'src/components/Radio';
import { CronPicker } from 'src/components/CronPicker';
export const StyledModal = styled(Modal)`
.ant-modal-body {
padding: 0;
}
h4 {
font-weight: 600;
}
`;
export const StyledTopSection = styled.div`
@@ -61,6 +67,14 @@ export const StyledIconWrapper = styled.span`
export const StyledScheduleTitle = styled.div`
margin-bottom: ${({ theme }) => theme.gridUnit * 7}px;
h4 {
margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
}
`;
export const StyledCronPicker = styled(CronPicker)`
margin-bottom: ${({ theme }) => theme.gridUnit * 3}px;
`;
export const StyledCronError = styled.p`
@@ -83,3 +97,17 @@ export const SectionHeaderStyle = (theme: SupersetTheme) => css`
margin: ${theme.gridUnit * 3}px 0;
font-weight: ${theme.typography.weights.bold};
`;
export const StyledMessageContentTitle = styled.div`
margin: ${({ theme }) => theme.gridUnit * 8}px 0
${({ theme }) => theme.gridUnit * 4}px;
`;
export const StyledRadio = styled(Radio)`
display: block;
line-height: ${({ theme }) => theme.gridUnit * 8}px;
`;
export const StyledRadioGroup = styled(Radio.Group)`
margin-left: ${({ theme }) => theme.gridUnit * 0.5}px;
`;

View File

@@ -310,10 +310,13 @@ const Select = ({
const handleOnDeselect = (value: string | number | AntdLabeledValue) => {
if (Array.isArray(selectValue)) {
const selectedValues = [
...(selectValue as []).filter(opt => opt !== value),
];
setSelectValue(selectedValues);
if (typeof value === 'number' || typeof value === 'string') {
const array = selectValue as (string | number)[];
setSelectValue(array.filter(element => element !== value));
} else {
const array = selectValue as AntdLabeledValue[];
setSelectValue(array.filter(element => element.value !== value.value));
}
}
setSearchedValue('');
};

View File

@@ -23,7 +23,7 @@ import moment from 'moment-timezone';
import { NativeGraySelect as Select } from 'src/components/Select';
const DEFAULT_TIMEZONE = 'GMT Standard Time';
const MIN_SELECT_WIDTH = '375px';
const MIN_SELECT_WIDTH = '400px';
const offsetsToName = {
'-300-240': ['Eastern Standard Time', 'Eastern Daylight Time'],

View File

@@ -187,7 +187,7 @@ export const Table = styled.table`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 300px;
max-width: 320px;
line-height: 1;
vertical-align: middle;
&:first-of-type {

View File

@@ -335,7 +335,7 @@ export const hydrateDashboard = (dashboardData, chartData) => (
dashboardInfo: {
...dashboardData,
metadata,
userId: String(user.userId), // legacy, please use state.user instead
userId: user.userId ? String(user.userId) : null, // legacy, please use state.user instead
dash_edit_perm: canEdit,
dash_save_perm: findPermission('can_save_dash', 'Superset', roles),
dash_share_perm: findPermission(

View File

@@ -175,11 +175,13 @@ class Header extends React.PureComponent {
'dashboard_id',
'dashboards',
dashboardInfo.id,
user.email,
);
}
}
UNSAFE_componentWillReceiveProps(nextProps) {
const { user } = this.props;
if (
UNDO_LIMIT - nextProps.undoLength <= 0 &&
!this.state.didNotifyMaxUndoHistoryToast
@@ -193,6 +195,16 @@ class Header extends React.PureComponent {
) {
this.props.setMaxUndoHistoryExceeded();
}
if (user && nextProps.dashboardInfo.id !== this.props.dashboardInfo.id) {
// this is in case there is an anonymous user.
this.props.fetchUISpecificReport(
user.userId,
'dashboard_id',
'dashboards',
nextProps.dashboardInfo.id,
user.email,
);
}
}
componentWillUnmount() {
@@ -417,7 +429,7 @@ class Header extends React.PureComponent {
const roles = Object.keys(user.roles || []);
const permissions = roles.map(key =>
user.roles[key].filter(
perms => perms[0] === 'can_add' && perms[1] === 'AlertModelView',
perms => perms[0] === 'menu_access' && perms[1] === 'Manage',
),
);
return permissions[0].length > 0;

View File

@@ -30,6 +30,7 @@ export const FILTER_SUPPORTED_TYPES = {
filter_timegrain: [GenericDataType.TEMPORAL],
filter_timecolumn: [GenericDataType.TEMPORAL],
filter_select: [
GenericDataType.BOOLEAN,
GenericDataType.STRING,
GenericDataType.NUMERIC,
GenericDataType.TEMPORAL,

View File

@@ -112,7 +112,7 @@ const ColumnButtonWrapper = styled.div`
const checkboxGenerator = (d, onChange) => (
<CheckboxControl value={d} onChange={onChange} />
);
const DATA_TYPES = ['STRING', 'NUMERIC', 'DATETIME'];
const DATA_TYPES = ['STRING', 'NUMERIC', 'DATETIME', 'BOOLEAN'];
const DATASOURCE_TYPES_ARR = [
{ key: 'physical', label: t('Physical (table or view)') },
@@ -390,7 +390,7 @@ class DatasourceEditor extends React.PureComponent {
this.setState(prevState => ({ isEditMode: !prevState.isEditMode }));
}
onDatasourceChange(datasource, callback) {
onDatasourceChange(datasource, callback = this.validateAndChange) {
this.setState({ datasource }, callback);
}
@@ -616,7 +616,13 @@ class DatasourceEditor extends React.PureComponent {
'values from the table. Typically the intent would be to limit the scan ' +
'by applying a relative time filter on a partitioned or indexed time-related field.',
)}
control={<TextControl controlId="fetch_values_predicate" />}
control={
<TextAreaControl
language="sql"
controlId="fetch_values_predicate"
minLines={5}
/>
}
/>
)}
{this.state.isSqla && (

View File

@@ -48,7 +48,7 @@ const createProps = () => ({
cache_timeout: null,
changed_on: '2021-03-19T16:30:56.750230',
changed_on_humanized: '3 days ago',
datasource: 'FCC 2018 Survey',
datasource: 'FCC Survey Results',
description: null,
description_markeddown: '',
edit_url: '/chart/edit/318',

View File

@@ -91,6 +91,7 @@ const StyledHeader = styled.div`
}
.action-button {
color: ${({ theme }) => theme.colors.grayscale.base};
margin: 0 ${({ theme }) => theme.gridUnit * 1.5}px 0
${({ theme }) => theme.gridUnit}px;
}
@@ -199,7 +200,7 @@ export class ExploreChartHeader extends React.PureComponent {
const roles = Object.keys(user.roles || []);
const permissions = roles.map(key =>
user.roles[key].filter(
perms => perms[0] === 'can_add' && perms[1] === 'AlertModelView',
perms => perms[0] === 'menu_access' && perms[1] === 'Manage',
),
);
return permissions[0].length > 0;
@@ -294,7 +295,7 @@ export class ExploreChartHeader extends React.PureComponent {
props={{
userId: this.props.user.userId,
userEmail: this.props.user.email,
chartId: this.props.chart.id,
chart: this.props.chart,
creationMethod: 'charts',
}}
/>

View File

@@ -132,7 +132,7 @@ const ExploreChartPanel = props => {
const { slice } = props;
const updateQueryContext = useCallback(
async function fetchChartData() {
if (slice && slice.query_context === null) {
if (props.can_overwrite && slice && slice.query_context === null) {
const queryContext = buildV1ChartDataPayload({
formData: slice.form_data,
force: false,
@@ -147,6 +147,7 @@ const ExploreChartPanel = props => {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query_context: JSON.stringify(queryContext),
query_context_generation: true,
}),
});
}

View File

@@ -81,7 +81,7 @@ const Styles = styled.div`
text-align: left;
position: relative;
width: 100%;
height: 100%;
max-height: 100%;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
@@ -448,6 +448,7 @@ function ExploreViewContainer(props) {
margin-bottom: 0;
}
body {
height: 100vh;
max-height: 100vh;
overflow: hidden;
}
@@ -458,7 +459,7 @@ function ExploreViewContainer(props) {
#app {
flex-basis: 100%;
overflow: hidden;
height: 100vh;
height: 100%;
}
#app-menu {
flex-shrink: 0;

View File

@@ -29,7 +29,7 @@ const createProps = () => ({
cache_timeout: null,
changed_on: '2021-03-19T16:30:56.750230',
changed_on_humanized: '7 days ago',
datasource: 'FCC 2018 Survey',
datasource: 'FCC Survey Results',
description: null,
description_markeddown: '',
edit_url: '/chart/edit/318',

View File

@@ -125,6 +125,8 @@ const ConditionalFormattingControl = ({
}: ConditionalFormattingConfig) => {
const columnName = (column && verboseMap?.[column]) ?? column;
switch (operator) {
case COMPARATOR.NONE:
return `${columnName}`;
case COMPARATOR.BETWEEN:
return `${targetValueLeft} ${COMPARATOR.LESS_THAN} ${columnName} ${COMPARATOR.LESS_THAN} ${targetValueRight}`;
case COMPARATOR.BETWEEN_OR_EQUAL:

View File

@@ -44,6 +44,7 @@ const colorSchemeOptions = [
];
const operatorOptions = [
{ value: COMPARATOR.NONE, label: 'None' },
{ value: COMPARATOR.GREATER_THAN, label: '>' },
{ value: COMPARATOR.LESS_THAN, label: '<' },
{ value: COMPARATOR.GREATER_OR_EQUAL, label: '≥' },
@@ -68,6 +69,9 @@ export const FormattingPopoverContent = ({
const isOperatorMultiValue = (operator?: COMPARATOR) =>
operator && MULTIPLE_VALUE_COMPARATORS.includes(operator);
const isOperatorNone = (operator?: COMPARATOR) =>
!operator || operator === COMPARATOR.NONE;
const operatorField = useMemo(
() => (
<FormItem
@@ -146,12 +150,18 @@ export const FormattingPopoverContent = ({
prevValues: ConditionalFormattingConfig,
currentValues: ConditionalFormattingConfig,
) =>
isOperatorNone(prevValues.operator) !==
isOperatorNone(currentValues.operator) ||
isOperatorMultiValue(prevValues.operator) !==
isOperatorMultiValue(currentValues.operator)
isOperatorMultiValue(currentValues.operator)
}
>
{({ getFieldValue }) =>
isOperatorMultiValue(getFieldValue('operator')) ? (
isOperatorNone(getFieldValue('operator')) ? (
<Row gutter={12}>
<Col span={6}>{operatorField}</Col>
</Row>
) : isOperatorMultiValue(getFieldValue('operator')) ? (
<Row gutter={12}>
<Col span={9}>
<FormItem

View File

@@ -22,6 +22,7 @@ import { PopoverProps } from 'antd/lib/popover';
import { ControlComponentProps } from '@superset-ui/chart-controls/lib/shared-controls/components/types';
export enum COMPARATOR {
NONE = 'None',
GREATER_THAN = '>',
LESS_THAN = '<',
GREATER_OR_EQUAL = '≥',

View File

@@ -29,7 +29,7 @@ const defaultProps: LabelProps = {
test('renders with default props', () => {
render(<DndColumnSelect {...defaultProps} />, { useDnd: true });
expect(screen.getByText('Drop columns')).toBeInTheDocument();
expect(screen.getByText('Drop columns here')).toBeInTheDocument();
});
test('renders with value', () => {

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, { useCallback, useEffect, useMemo } from 'react';
import React, { useCallback, useMemo } from 'react';
import { tn } from '@superset-ui/core';
import { ColumnMeta } from '@superset-ui/chart-controls';
import { isEmpty } from 'lodash';
@@ -27,6 +27,7 @@ import { OptionSelector } from 'src/explore/components/controls/DndColumnSelectC
import { DatasourcePanelDndItem } from 'src/explore/components/DatasourcePanel/types';
import { DndItemType } from 'src/explore/components/DndItemType';
import { StyledColumnOption } from 'src/explore/components/optionRenderers';
import { useComponentDidUpdate } from 'src/common/hooks/useComponentDidUpdate';
export const DndColumnSelect = (props: LabelProps) => {
const {
@@ -45,7 +46,7 @@ export const DndColumnSelect = (props: LabelProps) => {
);
// synchronize values in case of dataset changes
useEffect(() => {
const handleOptionsChange = useCallback(() => {
const optionSelectorValues = optionSelector.getValues();
if (typeof value !== typeof optionSelectorValues) {
onChange(optionSelectorValues);
@@ -65,9 +66,12 @@ export const DndColumnSelect = (props: LabelProps) => {
) {
onChange(optionSelectorValues);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [JSON.stringify(value), JSON.stringify(optionSelector.getValues())]);
// useComponentDidUpdate to avoid running this for the first render, to avoid
// calling onChange when the initial value is not valid for the dataset
useComponentDidUpdate(handleOptionsChange);
const onDrop = useCallback(
(item: DatasourcePanelDndItem) => {
const column = item.value as ColumnMeta;
@@ -139,7 +143,8 @@ export const DndColumnSelect = (props: LabelProps) => {
accept={DndItemType.Column}
displayGhostButton={multi || optionSelector.values.length === 0}
ghostButtonText={
ghostButtonText || tn('Drop column', 'Drop columns', multi ? 2 : 1)
ghostButtonText ||
tn('Drop column here', 'Drop columns here', multi ? 2 : 1)
}
{...props}
/>

View File

@@ -38,7 +38,7 @@ const defaultProps = {
test('renders with default props', () => {
render(<DndFilterSelect {...defaultProps} />, { useDnd: true });
expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument();
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
});
test('renders with value', () => {
@@ -56,7 +56,7 @@ test('renders options with saved metric', () => {
render(<DndFilterSelect {...defaultProps} formData={['saved_metric']} />, {
useDnd: true,
});
expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument();
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
});
test('renders options with column', () => {
@@ -76,7 +76,7 @@ test('renders options with column', () => {
useDnd: true,
},
);
expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument();
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
});
test('renders options with adhoc metric', () => {
@@ -87,5 +87,5 @@ test('renders options with adhoc metric', () => {
render(<DndFilterSelect {...defaultProps} formData={[adhocMetric]} />, {
useDnd: true,
});
expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument();
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
});

View File

@@ -374,7 +374,7 @@ export const DndFilterSelect = (props: DndFilterSelectProps) => {
canDrop={canDrop}
valuesRenderer={valuesRenderer}
accept={DND_ACCEPTED_TYPES}
ghostButtonText={t('Drop columns or metrics')}
ghostButtonText={t('Drop columns or metrics here')}
{...props}
/>
<AdhocFilterPopoverTrigger

View File

@@ -31,10 +31,10 @@ const defaultProps = {
test('renders with default props', () => {
render(<DndMetricSelect {...defaultProps} />, { useDnd: true });
expect(screen.getByText('Drop column or metric')).toBeInTheDocument();
expect(screen.getByText('Drop column or metric here')).toBeInTheDocument();
});
test('renders with default props and multi = true', () => {
render(<DndMetricSelect {...defaultProps} multi />, { useDnd: true });
expect(screen.getByText('Drop columns or metrics')).toBeInTheDocument();
expect(screen.getByText('Drop columns or metrics here')).toBeInTheDocument();
});

View File

@@ -245,7 +245,10 @@ export const DndMetricSelect = (props: any) => {
[props.savedMetrics, props.value],
);
const handleDropLabel = useCallback(() => onChange(value), [onChange, value]);
const handleDropLabel = useCallback(
() => onChange(multi ? value : value[0]),
[multi, onChange, value],
);
const valueRenderer = useCallback(
(option: Metric | AdhocMetric | string, index: number) => (
@@ -262,12 +265,14 @@ export const DndMetricSelect = (props: any) => {
onMoveLabel={moveLabel}
onDropLabel={handleDropLabel}
type={`${DndItemType.AdhocMetricOption}_${props.name}_${props.label}`}
multi={multi}
/>
),
[
getSavedMetricOptionsForMetric,
handleDropLabel,
moveLabel,
multi,
onMetricEdit,
onRemoveMetric,
props.columns,
@@ -334,8 +339,8 @@ export const DndMetricSelect = (props: any) => {
valuesRenderer={valuesRenderer}
accept={DND_ACCEPTED_TYPES}
ghostButtonText={tn(
'Drop column or metric',
'Drop columns or metrics',
'Drop column or metric here',
'Drop columns or metrics here',
multi ? 2 : 1,
)}
displayGhostButton={multi || value.length === 0}

View File

@@ -33,7 +33,7 @@ const defaultProps = {
test('renders with default props', async () => {
render(<DndSelectLabel {...defaultProps} />, { useDnd: true });
expect(await screen.findByText('Drop columns')).toBeInTheDocument();
expect(await screen.findByText('Drop columns here')).toBeInTheDocument();
});
test('renders ghost button when empty', async () => {

View File

@@ -55,7 +55,7 @@ export default function DndSelectLabel<T, O>({
return (
<AddControlLabel cancelHover>
<Icons.PlusSmall iconColor={theme.colors.grayscale.light1} />
{t(props.ghostButtonText || 'Drop columns')}
{t(props.ghostButtonText || 'Drop columns here')}
</AddControlLabel>
);
}

View File

@@ -37,6 +37,7 @@ const propTypes = {
onDropLabel: PropTypes.func,
index: PropTypes.number,
type: PropTypes.string,
multi: PropTypes.bool,
};
class AdhocMetricOption extends React.PureComponent {
@@ -62,6 +63,7 @@ class AdhocMetricOption extends React.PureComponent {
onDropLabel,
index,
type,
multi,
} = this.props;
return (
@@ -84,6 +86,7 @@ class AdhocMetricOption extends React.PureComponent {
type={type ?? DndItemType.AdhocMetricOption}
withCaret
isFunction
multi={multi}
/>
</AdhocMetricPopoverTrigger>
);

View File

@@ -49,6 +49,7 @@ export default function MetricDefinitionValue({
onDropLabel,
index,
type,
multi,
}) {
const getSavedMetricByName = metricName =>
savedMetrics.find(metric => metric.metric_name === metricName);
@@ -76,6 +77,7 @@ export default function MetricDefinitionValue({
index,
savedMetric: savedMetric ?? {},
type,
multi,
};
return <AdhocMetricOption {...metricOptionProps} />;

View File

@@ -16,16 +16,10 @@
* specific language governing permissions and limitations
* under the License.
*/
import React from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import PropTypes from 'prop-types';
import { t, withTheme } from '@superset-ui/core';
import { isEqual } from 'lodash';
import { ensureIsArray, t, useTheme } from '@superset-ui/core';
import ControlHeader from 'src/explore/components/ControlHeader';
import {
AGGREGATES_OPTIONS,
sqlaAutoGeneratedMetricNameRegex,
druidAutoGeneratedMetricRegex,
} from 'src/explore/constants';
import Icons from 'src/components/Icons';
import {
AddIconButton,
@@ -34,7 +28,6 @@ import {
LabelsContainer,
} from 'src/explore/components/controls/OptionControls';
import columnType from './columnType';
import MetricDefinitionOption from './MetricDefinitionOption';
import MetricDefinitionValue from './MetricDefinitionValue';
import AdhocMetric from './AdhocMetric';
import savedMetricType from './savedMetricType';
@@ -82,9 +75,9 @@ function isDictionaryForAdhocMetric(value) {
return value && !(value instanceof AdhocMetric) && value.expressionType;
}
function columnsContainAllMetrics(value, nextProps) {
function columnsContainAllMetrics(value, columns, savedMetrics) {
const columnNames = new Set(
[...(nextProps.columns || []), ...(nextProps.savedMetrics || [])]
[...(columns || []), ...(savedMetrics || [])]
// eslint-disable-next-line camelcase
.map(({ column_name, metric_name }) => column_name || metric_name),
);
@@ -123,294 +116,228 @@ function coerceAdhocMetrics(value) {
});
}
class MetricsControl extends React.PureComponent {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onMetricEdit = this.onMetricEdit.bind(this);
this.onNewMetric = this.onNewMetric.bind(this);
this.onRemoveMetric = this.onRemoveMetric.bind(this);
this.moveLabel = this.moveLabel.bind(this);
this.checkIfAggregateInInput = this.checkIfAggregateInInput.bind(this);
this.optionsForSelect = this.optionsForSelect.bind(this);
this.selectFilterOption = this.selectFilterOption.bind(this);
this.isAutoGeneratedMetric = this.isAutoGeneratedMetric.bind(this);
this.optionRenderer = option => <MetricDefinitionOption option={option} />;
this.valueRenderer = (option, index) => (
const emptySavedMetric = { metric_name: '', expression: '' };
const MetricsControl = ({
onChange,
multi,
value: propsValue,
columns,
savedMetrics,
datasource,
datasourceType,
...props
}) => {
const [value, setValue] = useState(coerceAdhocMetrics(propsValue));
const theme = useTheme();
const handleChange = useCallback(
opts => {
// if clear out options
if (opts === null) {
onChange(null);
return;
}
const transformedOpts = ensureIsArray(opts);
const optionValues = transformedOpts
.map(option => {
// pre-defined metric
if (option.metric_name) {
return option.metric_name;
}
return option;
})
.filter(option => option);
onChange(multi ? optionValues : optionValues[0]);
},
[multi, onChange],
);
const onNewMetric = useCallback(
newMetric => {
const newValue = [...value, newMetric];
setValue(newValue);
handleChange(newValue);
},
[handleChange, value],
);
const onMetricEdit = useCallback(
(changedMetric, oldMetric) => {
const newValue = value.map(val => {
if (
// compare saved metrics
val === oldMetric.metric_name ||
// compare adhoc metrics
typeof val.optionName !== 'undefined'
? val.optionName === oldMetric.optionName
: false
) {
return changedMetric;
}
return val;
});
setValue(newValue);
handleChange(newValue);
},
[handleChange, value],
);
const onRemoveMetric = useCallback(
index => {
if (!Array.isArray(value)) {
return;
}
const valuesCopy = [...value];
valuesCopy.splice(index, 1);
setValue(valuesCopy);
handleChange(valuesCopy);
},
[handleChange, value],
);
const moveLabel = useCallback(
(dragIndex, hoverIndex) => {
const newValues = [...value];
[newValues[hoverIndex], newValues[dragIndex]] = [
newValues[dragIndex],
newValues[hoverIndex],
];
setValue(newValues);
},
[value],
);
const isAddNewMetricDisabled = useCallback(() => !multi && value.length > 0, [
multi,
value.length,
]);
const savedMetricOptions = useMemo(
() => getOptionsForSavedMetrics(savedMetrics, propsValue, null),
[propsValue, savedMetrics],
);
const newAdhocMetric = useMemo(() => new AdhocMetric({ isNew: true }), [
value,
]);
const addNewMetricPopoverTrigger = useCallback(
trigger => {
if (isAddNewMetricDisabled()) {
return trigger;
}
return (
<AdhocMetricPopoverTrigger
adhocMetric={newAdhocMetric}
onMetricEdit={onNewMetric}
columns={columns}
savedMetricsOptions={savedMetricOptions}
datasource={datasource}
savedMetric={emptySavedMetric}
datasourceType={datasourceType}
createNew
>
{trigger}
</AdhocMetricPopoverTrigger>
);
},
[
columns,
datasource,
datasourceType,
isAddNewMetricDisabled,
newAdhocMetric,
onNewMetric,
savedMetricOptions,
],
);
useEffect(() => {
// Remove all metrics if selected value no longer a valid column
// in the dataset. Must use `nextProps` here because Redux reducers may
// have already updated the value for this control.
if (!columnsContainAllMetrics(propsValue, columns, savedMetrics)) {
handleChange([]);
}
}, [columns, savedMetrics]);
useEffect(() => {
setValue(coerceAdhocMetrics(propsValue));
}, [propsValue]);
const onDropLabel = useCallback(() => handleChange(value), [
handleChange,
value,
]);
const valueRenderer = useCallback(
(option, index) => (
<MetricDefinitionValue
key={index}
index={index}
option={option}
onMetricEdit={this.onMetricEdit}
onRemoveMetric={this.onRemoveMetric}
columns={this.props.columns}
datasource={this.props.datasource}
savedMetrics={this.props.savedMetrics}
onMetricEdit={onMetricEdit}
onRemoveMetric={onRemoveMetric}
columns={columns}
datasource={datasource}
savedMetrics={savedMetrics}
savedMetricsOptions={getOptionsForSavedMetrics(
this.props.savedMetrics,
this.props.value,
this.props.value?.[index],
savedMetrics,
value,
value?.[index],
)}
datasourceType={this.props.datasourceType}
onMoveLabel={this.moveLabel}
onDropLabel={() => this.props.onChange(this.state.value)}
datasourceType={datasourceType}
onMoveLabel={moveLabel}
onDropLabel={onDropLabel}
multi={multi}
/>
);
this.select = null;
this.selectRef = ref => {
if (ref) {
this.select = ref.select;
} else {
this.select = null;
}
};
this.state = {
aggregateInInput: null,
options: this.optionsForSelect(this.props),
value: coerceAdhocMetrics(this.props.value),
};
}
),
[
columns,
datasource,
datasourceType,
moveLabel,
multi,
onDropLabel,
onMetricEdit,
onRemoveMetric,
savedMetrics,
value,
],
);
UNSAFE_componentWillReceiveProps(nextProps) {
const { value } = this.props;
if (
!isEqual(this.props.columns, nextProps.columns) ||
!isEqual(this.props.savedMetrics, nextProps.savedMetrics)
) {
this.setState({ options: this.optionsForSelect(nextProps) });
// Remove all metrics if selected value no longer a valid column
// in the dataset. Must use `nextProps` here because Redux reducers may
// have already updated the value for this control.
if (!columnsContainAllMetrics(nextProps.value, nextProps)) {
this.props.onChange([]);
}
}
if (value !== nextProps.value) {
this.setState({ value: coerceAdhocMetrics(nextProps.value) });
}
}
onNewMetric(newMetric) {
this.setState(
prevState => ({
...prevState,
value: [...prevState.value, newMetric],
}),
() => {
this.onChange(this.state.value);
},
);
}
onMetricEdit(changedMetric, oldMetric) {
this.setState(
prevState => ({
value: prevState.value.map(value => {
if (
// compare saved metrics
value === oldMetric.metric_name ||
// compare adhoc metrics
typeof value.optionName !== 'undefined'
? value.optionName === oldMetric.optionName
: false
) {
return changedMetric;
}
return value;
}),
}),
() => {
this.onChange(this.state.value);
},
);
}
onRemoveMetric(index) {
if (!Array.isArray(this.state.value)) {
return;
}
const valuesCopy = [...this.state.value];
valuesCopy.splice(index, 1);
this.setState(prevState => ({
...prevState,
value: valuesCopy,
}));
this.props.onChange(valuesCopy);
}
onChange(opts) {
// if clear out options
if (opts === null) {
this.props.onChange(null);
return;
}
let transformedOpts;
if (Array.isArray(opts)) {
transformedOpts = opts;
} else {
transformedOpts = opts ? [opts] : [];
}
const optionValues = transformedOpts
.map(option => {
// pre-defined metric
if (option.metric_name) {
return option.metric_name;
}
return option;
})
.filter(option => option);
this.props.onChange(this.props.multi ? optionValues : optionValues[0]);
}
moveLabel(dragIndex, hoverIndex) {
const { value } = this.state;
const newValues = [...value];
[newValues[hoverIndex], newValues[dragIndex]] = [
newValues[dragIndex],
newValues[hoverIndex],
];
this.setState({ value: newValues });
}
isAddNewMetricDisabled() {
return !this.props.multi && this.state.value.length > 0;
}
addNewMetricPopoverTrigger(trigger) {
if (this.isAddNewMetricDisabled()) {
return trigger;
}
return (
<AdhocMetricPopoverTrigger
adhocMetric={new AdhocMetric({ isNew: true })}
onMetricEdit={this.onNewMetric}
columns={this.props.columns}
savedMetricsOptions={getOptionsForSavedMetrics(
this.props.savedMetrics,
this.props.value,
null,
return (
<div className="metrics-select">
<HeaderContainer>
<ControlHeader {...props} />
{addNewMetricPopoverTrigger(
<AddIconButton
disabled={isAddNewMetricDisabled()}
data-test="add-metric-button"
>
<Icons.PlusLarge
iconSize="s"
iconColor={theme.colors.grayscale.light5}
/>
</AddIconButton>,
)}
datasource={this.props.datasource}
savedMetric={{ metric_name: '', expression: '' }}
datasourceType={this.props.datasourceType}
createNew
>
{trigger}
</AdhocMetricPopoverTrigger>
);
}
checkIfAggregateInInput(input) {
const lowercaseInput = input.toLowerCase();
const aggregateInInput =
AGGREGATES_OPTIONS.find(x =>
lowercaseInput.startsWith(`${x.toLowerCase()}(`),
) || null;
this.clearedAggregateInInput = this.state.aggregateInInput;
this.setState({ aggregateInInput });
}
optionsForSelect(props) {
const { columns, savedMetrics } = props;
const aggregates =
columns && columns.length
? AGGREGATES_OPTIONS.map(aggregate => ({
aggregate_name: aggregate,
}))
: [];
const options = [
...(columns || []),
...aggregates,
...(savedMetrics || []),
];
return options.reduce((results, option) => {
if (option.metric_name) {
results.push({ ...option, optionName: option.metric_name });
} else if (option.column_name) {
results.push({ ...option, optionName: `_col_${option.column_name}` });
} else if (option.aggregate_name) {
results.push({
...option,
optionName: `_aggregate_${option.aggregate_name}`,
});
}
return results;
}, []);
}
isAutoGeneratedMetric(savedMetric) {
if (this.props.datasourceType === 'druid') {
return druidAutoGeneratedMetricRegex.test(savedMetric.verbose_name);
}
return sqlaAutoGeneratedMetricNameRegex.test(savedMetric.metric_name);
}
selectFilterOption({ data: option }, filterValue) {
if (this.state.aggregateInInput) {
let endIndex = filterValue.length;
if (filterValue.endsWith(')')) {
endIndex = filterValue.length - 1;
}
const valueAfterAggregate = filterValue.substring(
filterValue.indexOf('(') + 1,
endIndex,
);
return (
option.column_name &&
option.column_name.toLowerCase().indexOf(valueAfterAggregate) >= 0
);
}
return (
option.optionName &&
(!option.metric_name ||
!this.isAutoGeneratedMetric(option) ||
option.verbose_name) &&
(option.optionName.toLowerCase().indexOf(filterValue) >= 0 ||
(option.verbose_name &&
option.verbose_name.toLowerCase().indexOf(filterValue) >= 0))
);
}
render() {
const { theme } = this.props;
return (
<div className="metrics-select">
<HeaderContainer>
<ControlHeader {...this.props} />
{this.addNewMetricPopoverTrigger(
<AddIconButton
disabled={this.isAddNewMetricDisabled()}
data-test="add-metric-button"
>
<Icons.PlusLarge
iconSize="s"
iconColor={theme.colors.grayscale.light5}
/>
</AddIconButton>,
)}
</HeaderContainer>
<LabelsContainer>
{this.state.value.length > 0
? this.state.value.map((value, index) =>
this.valueRenderer(value, index),
)
: this.addNewMetricPopoverTrigger(
<AddControlLabel>
<Icons.PlusSmall iconColor={theme.colors.grayscale.light1} />
{t('Add metric')}
</AddControlLabel>,
)}
</LabelsContainer>
</div>
);
}
}
</HeaderContainer>
<LabelsContainer>
{value.length > 0
? value.map((value, index) => valueRenderer(value, index))
: addNewMetricPopoverTrigger(
<AddControlLabel>
<Icons.PlusSmall iconColor={theme.colors.grayscale.light1} />
{t('Add metric')}
</AddControlLabel>,
)}
</LabelsContainer>
</div>
);
};
MetricsControl.propTypes = propTypes;
MetricsControl.defaultProps = defaultProps;
export default withTheme(MetricsControl);
export default MetricsControl;

View File

@@ -177,6 +177,7 @@ export const OptionControlLabel = ({
index,
isExtra,
tooltipTitle,
multi = true,
...props
}: {
label: string | React.ReactNode;
@@ -192,15 +193,22 @@ export const OptionControlLabel = ({
index: number;
isExtra?: boolean;
tooltipTitle: string;
multi?: boolean;
}) => {
const theme = useTheme();
const ref = useRef<HTMLDivElement>(null);
const [, drop] = useDrop({
accept: type,
drop() {
if (!multi) {
return;
}
onDropLabel?.();
},
hover(item: DragItem, monitor: DropTargetMonitor) {
if (!multi) {
return;
}
if (!ref.current) {
return;
}

View File

@@ -120,7 +120,7 @@ describe('VizTypeControl', () => {
expect(visualizations).toHaveTextContent(/Time-series Table/);
expect(visualizations).toHaveTextContent(/Time-series Chart/);
expect(visualizations).toHaveTextContent(/Mixed timeseries chart/);
expect(visualizations).toHaveTextContent(/Mixed Time-Series/);
expect(visualizations).not.toHaveTextContent(/Line Chart/);
});
});

View File

@@ -62,17 +62,22 @@ enum SECTIONS {
const DEFAULT_ORDER = [
'line',
'big_number',
'big_number_total',
'table',
'pivot_table_v2',
'echarts_timeseries_line',
'echarts_area',
'echarts_timeseries_bar',
'echarts_timeseries_scatter',
'pie',
'mixed_timeseries',
'filter_box',
'dist_bar',
'area',
'bar',
'deck_polygon',
'pie',
'time_table',
'pivot_table_v2',
'histogram',
'big_number_total',
'deck_scatter',
'deck_hex',
'time_pivot',
@@ -116,11 +121,7 @@ const OTHER_CATEGORY = t('Other');
const ALL_CHARTS = t('All charts');
const RECOMMENDED_TAGS = [
t('Highly-used'),
t('ECharts'),
t('Advanced-Analytics'),
];
const RECOMMENDED_TAGS = [t('Popular'), t('ECharts'), t('Advanced-Analytics')];
export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control';

View File

@@ -19,6 +19,7 @@
/* eslint camelcase: 0 */
import { t, SupersetClient } from '@superset-ui/core';
import rison from 'rison';
import { getClientErrorObject } from 'src/utils/getClientErrorObject';
import { addDangerToast, addSuccessToast } from '../../messageToasts/actions';
export const SET_REPORT = 'SET_REPORT';
@@ -102,15 +103,21 @@ export const addReport = report => dispatch => {
endpoint: `/api/v1/report/`,
jsonPayload: report,
})
.then(() => {
dispatch({ type: ADD_REPORT, report });
.then(({ json }) => {
dispatch({ type: ADD_REPORT, json });
dispatch(addSuccessToast(t('The report has been created')));
})
.catch(() =>
.catch(async e => {
const parsedError = await getClientErrorObject(e);
const errorMessage = parsedError.message;
const errorArr = Object.keys(errorMessage);
const error = errorMessage[errorArr[0]][0];
dispatch(
addDangerToast(t('An error occurred while creating this report.')),
),
);
addDangerToast(
t('An error occurred while editing this report: %s', error),
),
);
});
};
export const EDIT_REPORT = 'EDIT_REPORT';

View File

@@ -25,12 +25,12 @@ import {
import React, { useMemo, useState } from 'react';
import rison from 'rison';
import { uniqBy } from 'lodash';
import moment from 'moment';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import {
createErrorHandler,
createFetchRelated,
handleChartDelete,
CardStylesOverrides,
} from 'src/views/CRUD/utils';
import {
useChartEditModal,
@@ -160,6 +160,9 @@ function ChartList(props: ChartListProps) {
const [passwordFields, setPasswordFields] = useState<string[]>([]);
const [preparingExport, setPreparingExport] = useState<boolean>(false);
const { userId } = props.user;
const userKey = getFromLocalStorage(userId.toString(), null);
const openChartImportModal = () => {
showImportModal(true);
};
@@ -270,23 +273,33 @@ function ChartList(props: ChartListProps) {
Cell: ({
row: {
original: {
changed_by_name: changedByName,
last_saved_by: lastSavedBy,
changed_by_url: changedByUrl,
},
},
}: any) => <a href={changedByUrl}>{changedByName}</a>,
}: any) => (
<a href={changedByUrl}>
{lastSavedBy?.first_name
? `${lastSavedBy?.first_name} ${lastSavedBy?.last_name}`
: null}
</a>
),
Header: t('Modified by'),
accessor: 'changed_by.first_name',
accessor: 'last_saved_by',
size: 'xl',
},
{
Cell: ({
row: {
original: { changed_on_delta_humanized: changedOn },
original: { last_saved_at: lastSavedAt },
},
}: any) => <span className="no-wrap">{changedOn}</span>,
}: any) => (
<span className="no-wrap">
{lastSavedAt ? moment.utc(lastSavedAt).fromNow() : null}
</span>
),
Header: t('Last modified'),
accessor: 'changed_on_delta_humanized',
accessor: 'last_saved_at',
size: 'xl',
},
{
@@ -532,29 +545,25 @@ function ChartList(props: ChartListProps) {
];
function renderCard(chart: Chart) {
const { userId } = props.user;
const userKey = getFromLocalStorage(userId.toString(), null);
return (
<CardStylesOverrides>
<ChartCard
chart={chart}
showThumbnails={
userKey
? userKey.thumbnails
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
}
hasPerm={hasPerm}
openChartEditModal={openChartEditModal}
bulkSelectEnabled={bulkSelectEnabled}
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
refreshData={refreshData}
loading={loading}
favoriteStatus={favoriteStatus[chart.id]}
saveFavoriteStatus={saveFavoriteStatus}
handleBulkChartExport={handleBulkChartExport}
/>
</CardStylesOverrides>
<ChartCard
chart={chart}
showThumbnails={
userKey
? userKey.thumbnails
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
}
hasPerm={hasPerm}
openChartEditModal={openChartEditModal}
bulkSelectEnabled={bulkSelectEnabled}
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
refreshData={refreshData}
loading={loading}
favoriteStatus={favoriteStatus[chart.id]}
saveFavoriteStatus={saveFavoriteStatus}
handleBulkChartExport={handleBulkChartExport}
/>
);
}
const subMenuButtons: SubMenuProps['buttons'] = [];
@@ -644,6 +653,11 @@ function ChartList(props: ChartListProps) {
loading={loading}
pageSize={PAGE_SIZE}
renderCard={renderCard}
showThumbnails={
userKey
? userKey.thumbnails
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
}
defaultViewMode={
isFeatureEnabled(FeatureFlag.LISTVIEWS_DEFAULT_CARD_VIEW)
? 'card'

View File

@@ -25,7 +25,6 @@ import {
createFetchRelated,
createErrorHandler,
handleDashboardDelete,
CardStylesOverrides,
} from 'src/views/CRUD/utils';
import { useListViewResource, useFavoriteStatus } from 'src/views/CRUD/hooks';
import ConfirmStatusChange from 'src/components/ConfirmStatusChange';
@@ -140,6 +139,9 @@ function DashboardList(props: DashboardListProps) {
refreshData();
};
const { userId } = props.user;
const userKey = getFromLocalStorage(userId.toString(), null);
const canCreate = hasPerm('can_write');
const canEdit = hasPerm('can_write');
const canDelete = hasPerm('can_write');
@@ -499,29 +501,25 @@ function DashboardList(props: DashboardListProps) {
];
function renderCard(dashboard: Dashboard) {
const { userId } = props.user;
const userKey = getFromLocalStorage(userId.toString(), null);
return (
<CardStylesOverrides>
<DashboardCard
dashboard={dashboard}
hasPerm={hasPerm}
bulkSelectEnabled={bulkSelectEnabled}
refreshData={refreshData}
showThumbnails={
userKey
? userKey.thumbnails
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
}
loading={loading}
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
openDashboardEditModal={openDashboardEditModal}
saveFavoriteStatus={saveFavoriteStatus}
favoriteStatus={favoriteStatus[dashboard.id]}
handleBulkDashboardExport={handleBulkDashboardExport}
/>
</CardStylesOverrides>
<DashboardCard
dashboard={dashboard}
hasPerm={hasPerm}
bulkSelectEnabled={bulkSelectEnabled}
refreshData={refreshData}
showThumbnails={
userKey
? userKey.thumbnails
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
}
loading={loading}
addDangerToast={addDangerToast}
addSuccessToast={addSuccessToast}
openDashboardEditModal={openDashboardEditModal}
saveFavoriteStatus={saveFavoriteStatus}
favoriteStatus={favoriteStatus[dashboard.id]}
handleBulkDashboardExport={handleBulkDashboardExport}
/>
);
}
@@ -614,6 +612,11 @@ function DashboardList(props: DashboardListProps) {
initialSort={initialSort}
loading={loading}
pageSize={PAGE_SIZE}
showThumbnails={
userKey
? userKey.thumbnails
: isFeatureEnabled(FeatureFlag.THUMBNAILS)
}
renderCard={renderCard}
defaultViewMode={
isFeatureEnabled(FeatureFlag.LISTVIEWS_DEFAULT_CARD_VIEW)

View File

@@ -76,6 +76,18 @@ import {
} from './styles';
import ModalHeader, { DOCUMENTATION_LINK } from './ModalHeader';
const engineSpecificAlertMapping = {
gsheets: {
message: 'Why do I need to create a database?',
description:
'To begin using your Google Sheets, you need to create a database first. ' +
'Databases are used as a way to identify ' +
'your data so that it can be queried and visualized. This ' +
'database will hold all of your individual Google Sheets ' +
'you choose to connect here.',
},
};
const errorAlertMapping = {
CONNECTION_MISSING_PARAMETERS_ERROR: {
message: 'Missing Required Fields',
@@ -332,16 +344,21 @@ function dbReducer(
action.payload.configuration_method ===
CONFIGURATION_METHOD.DYNAMIC_FORM
) {
// convert query into URI params string
query = new URLSearchParams(
action?.payload?.parameters?.query as string,
).toString();
return {
...action.payload,
engine: action.payload.backend,
configuration_method: action.payload.configuration_method,
extra_json: deserializeExtraJSON,
parameters: {
query,
credentials_info: JSON.stringify(
action.payload?.parameters?.credentials_info || '',
),
query,
},
};
}
@@ -454,10 +471,11 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
const sslForced = isFeatureEnabled(
FeatureFlag.FORCE_DATABASE_CONNECTIONS_SSL,
);
const hasAlert =
connectionAlert || !!(db?.engine && engineSpecificAlertMapping[db.engine]);
const useSqlAlchemyForm =
db?.configuration_method === CONFIGURATION_METHOD.SQLALCHEMY_URI;
const useTabLayout = isEditMode || useSqlAlchemyForm;
// Database fetch logic
const {
state: { loading: dbLoading, resource: dbFetched, error: dbErrors },
@@ -471,9 +489,9 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
addDangerToast,
);
const isDynamic = (engine: string | undefined) =>
availableDbs?.databases.filter(
availableDbs?.databases?.find(
(DB: DatabaseObject) => DB.backend === engine || DB.engine === engine,
)[0].parameters !== undefined;
)?.parameters !== undefined;
const showDBError = validationErrors || dbErrors;
const isEmpty = (data?: Object | null) =>
data && Object.keys(data).length === 0;
@@ -521,20 +539,10 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
const dbToUpdate = JSON.parse(JSON.stringify(update));
if (dbToUpdate.configuration_method === CONFIGURATION_METHOD.DYNAMIC_FORM) {
// Validate DB before saving
await getValidation(dbToUpdate, true);
if (validationErrors && !isEmpty(validationErrors)) {
return;
}
if (dbToUpdate?.parameters?.query) {
// convert query params into dictionary
dbToUpdate.parameters.query = JSON.parse(
`{"${decodeURI((dbToUpdate?.parameters?.query as string) || '')
.replace(/"/g, '\\"')
.replace(/&/g, '","')
.replace(/=/g, '":"')}"}`,
);
const urlParams = new URLSearchParams(dbToUpdate?.parameters?.query);
dbToUpdate.parameters.query = Object.fromEntries(urlParams);
} else if (
dbToUpdate?.parameters?.query === '' &&
'query' in dbModel.parameters.properties
@@ -542,6 +550,12 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
dbToUpdate.parameters.query = {};
}
// Validate DB before saving
await getValidation(dbToUpdate, true);
if (validationErrors && !isEmpty(validationErrors)) {
return;
}
const engine = dbToUpdate.backend || dbToUpdate.engine;
if (engine === 'bigquery' && dbToUpdate.parameters?.credentials_info) {
// wrap encrypted_extra in credentials_info only for BigQuery
@@ -834,6 +848,26 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
setTabKey(key);
};
const renderStepTwoAlert = () =>
db?.engine && (
<StyledAlertMargin>
<Alert
closable={false}
css={(theme: SupersetTheme) => antDAlertStyles(theme)}
type="info"
showIcon
message={
engineSpecificAlertMapping[db.engine]?.message ||
connectionAlert?.DEFAULT?.message
}
description={
engineSpecificAlertMapping[db.engine]?.description ||
connectionAlert?.DEFAULT?.description
}
/>
</StyledAlertMargin>
);
const errorAlert = () => {
if (
isEmpty(dbErrors) ||
@@ -1188,18 +1222,7 @@ const DatabaseModal: FunctionComponent<DatabaseModalProps> = ({
dbName={dbName}
dbModel={dbModel}
/>
{connectionAlert && (
<StyledAlertMargin>
<Alert
closable={false}
css={(theme: SupersetTheme) => antDAlertStyles(theme)}
type="info"
showIcon
message={t('IP Allowlist')}
description={connectionAlert.ALLOWED_IPS}
/>
</StyledAlertMargin>
)}
{hasAlert && renderStepTwoAlert()}
<DatabaseConnectionForm
db={db}
sslForced={sslForced}

View File

@@ -17,7 +17,8 @@
* under the License.
*/
// storage keys for welcome page sticky tabs..
// storage keys for welcome page sticky tabs and tables
export const HOMEPAGE_CHART_FILTER = 'homepage_chart_filter';
export const HOMEPAGE_ACTIVITY_FILTER = 'homepage_activity_filter';
export const HOMEPAGE_DASHBOARD_FILTER = 'homepage_dashboard_filter';
export const HOMEPAGE_COLLAPSE_STATE = 'homepage_collapse_state';

View File

@@ -32,7 +32,7 @@ export enum TableTabTypes {
export type Filters = {
col: string;
opr: string;
value: string;
value: string | number;
};
export interface DashboardTableProps {

View File

@@ -132,10 +132,19 @@ export const getRecentAcitivtyObjs = (
) =>
SupersetClient.get({ endpoint: recent }).then(recentsRes => {
const res: any = {};
const filters = [
{
col: 'created_by',
opr: 'rel_o_m',
value: 0,
},
];
const newBatch = [
SupersetClient.get({ endpoint: `/api/v1/chart/?q=${getParams()}` }),
SupersetClient.get({
endpoint: `/api/v1/dashboard/?q=${getParams()}`,
endpoint: `/api/v1/chart/?q=${getParams(filters)}`,
}),
SupersetClient.get({
endpoint: `/api/v1/dashboard/?q=${getParams(filters)}`,
}),
];
return Promise.all(newBatch)
@@ -269,15 +278,12 @@ export function shortenSQL(sql: string, maxLines: number) {
return lines.join('\n');
}
// loading card count for homepage
export const loadingCardCount = 5;
const breakpoints = [576, 768, 992, 1200];
export const mq = breakpoints.map(bp => `@media (max-width: ${bp}px)`);
export const CardStylesOverrides = styled.div`
.ant-card-cover > div {
height: 264px;
}
`;
export const CardContainer = styled.div<{
showThumbnails?: boolean | undefined;
}>`
@@ -286,7 +292,7 @@ export const CardContainer = styled.div<{
display: grid;
grid-gap: ${theme.gridUnit * 12}px ${theme.gridUnit * 4}px;
grid-template-columns: repeat(auto-fit, 300px);
max-height: ${showThumbnails ? '314' : '140'}px;
max-height: ${showThumbnails ? '314' : '148'}px;
margin-top: ${theme.gridUnit * -6}px;
padding: ${
showThumbnails

View File

@@ -21,9 +21,9 @@ import moment from 'moment';
import { styled, t } from '@superset-ui/core';
import { setInLocalStorage } from 'src/utils/localStorageHelpers';
import Loading from 'src/components/Loading';
import ListViewCard from 'src/components/ListViewCard';
import SubMenu from 'src/components/Menu/SubMenu';
import { LoadingCards, ActivityData } from 'src/views/CRUD/welcome/Welcome';
import {
CardStyles,
getEditedObjects,
@@ -34,7 +34,7 @@ import { Chart } from 'src/types/Chart';
import { Dashboard, SavedQueryObject } from 'src/views/CRUD/types';
import Icons from 'src/components/Icons';
import { ActivityData } from './Welcome';
import EmptyState from './EmptyState';
/**
@@ -230,7 +230,7 @@ export default function ActivityTable({
const doneFetching = loadedCount < 3;
if ((loadingState && !editedObjs) || doneFetching) {
return <Loading position="inline" />;
return <LoadingCards />;
}
return (
<Styles>

View File

@@ -35,6 +35,7 @@ import PropertiesModal from 'src/explore/components/PropertiesModal';
import { User } from 'src/types/bootstrapTypes';
import { CardContainer, PAGE_SIZE } from 'src/views/CRUD/utils';
import { HOMEPAGE_CHART_FILTER } from 'src/views/CRUD/storageKeys';
import { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import ChartCard from 'src/views/CRUD/chart/ChartCard';
import Chart from 'src/types/Chart';
import handleResourceExport from 'src/utils/export';
@@ -177,7 +178,7 @@ function ChartTable({
});
}
if (loading) return <Loading position="inline" />;
if (loading) return <LoadingCards cover={showThumbnails} />;
return (
<ErrorBoundary>
{sliceCurrentlyEditing && (

View File

@@ -31,6 +31,7 @@ import {
setInLocalStorage,
getFromLocalStorage,
} from 'src/utils/localStorageHelpers';
import { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import {
createErrorHandler,
CardContainer,
@@ -189,7 +190,7 @@ function DashboardTable({
filters: getFilters(filter),
});
if (loading) return <Loading position="inline" />;
if (loading) return <LoadingCards cover={showThumbnails} />;
return (
<>
<SubMenu

View File

@@ -22,7 +22,7 @@ import SyntaxHighlighter from 'react-syntax-highlighter/dist/cjs/light';
import sql from 'react-syntax-highlighter/dist/cjs/languages/hljs/sql';
import github from 'react-syntax-highlighter/dist/cjs/styles/hljs/github';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import Loading from 'src/components/Loading';
import { LoadingCards } from 'src/views/CRUD/welcome/Welcome';
import { Dropdown, Menu } from 'src/common/components';
import { useListViewResource, copyQueryLink } from 'src/views/CRUD/hooks';
import ListViewCard from 'src/components/ListViewCard';
@@ -240,7 +240,7 @@ const SavedQueries = ({
</Menu>
);
if (loading) return <Loading position="inline" />;
if (loading) return <LoadingCards cover={showThumbnails} />;
return (
<>
{queryDeleteModal && (

View File

@@ -25,15 +25,20 @@ import {
getFromLocalStorage,
setInLocalStorage,
} from 'src/utils/localStorageHelpers';
import ListViewCard from 'src/components/ListViewCard';
import withToasts from 'src/messageToasts/enhancers/withToasts';
import Loading from 'src/components/Loading';
import {
createErrorHandler,
getRecentAcitivtyObjs,
mq,
CardContainer,
getUserOwnedObjects,
loadingCardCount,
} from 'src/views/CRUD/utils';
import { HOMEPAGE_ACTIVITY_FILTER } from 'src/views/CRUD/storageKeys';
import {
HOMEPAGE_ACTIVITY_FILTER,
HOMEPAGE_COLLAPSE_STATE,
} from 'src/views/CRUD/storageKeys';
import { FeatureFlag, isFeatureEnabled } from 'src/featureFlags';
import { Switch } from 'src/common/components';
@@ -54,6 +59,10 @@ export interface ActivityData {
Examples?: Array<object>;
}
interface LoadingProps {
cover?: boolean;
}
const DEFAULT_TAB_ARR = ['2', '3'];
const WelcomeContainer = styled.div`
@@ -96,6 +105,12 @@ const WelcomeContainer = styled.div`
div.ant-collapse-item:last-child .ant-collapse-header {
padding-bottom: ${({ theme }) => theme.gridUnit * 9}px;
}
.loading-cards {
margin-top: ${({ theme }) => theme.gridUnit * 8}px;
.ant-card-cover > div {
height: 168px;
}
}
`;
const WelcomeNav = styled.div`
@@ -118,6 +133,14 @@ const WelcomeNav = styled.div`
}
`;
export const LoadingCards = ({ cover }: LoadingProps) => (
<CardContainer showThumbnails={cover} className="loading-cards">
{[...new Array(loadingCardCount)].map(() => (
<ListViewCard cover={cover ? false : <></>} description="" loading />
))}
</CardContainer>
);
function Welcome({ user, addDangerToast }: WelcomeProps) {
const userid = user.userId;
const id = userid.toString();
@@ -137,16 +160,18 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
null,
);
const [loadedCount, setLoadedCount] = useState(0);
const [activeState, setActiveState] = useState<Array<string>>(
DEFAULT_TAB_ARR,
);
const collapseState = getFromLocalStorage(HOMEPAGE_COLLAPSE_STATE, null);
const [activeState, setActiveState] = useState<Array<string>>(collapseState);
const handleCollapse = (state: Array<string>) => {
setActiveState(state);
setInLocalStorage(HOMEPAGE_COLLAPSE_STATE, state);
};
useEffect(() => {
const activeTab = getFromLocalStorage(HOMEPAGE_ACTIVITY_FILTER, null);
setActiveState(collapseState || DEFAULT_TAB_ARR);
getRecentAcitivtyObjs(user.userId, recent, addDangerToast)
.then(res => {
const data: ActivityData | null = {};
@@ -216,7 +241,7 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
};
useEffect(() => {
if (queryData?.length) {
if (!collapseState && queryData?.length) {
setActiveState(activeState => [...activeState, '4']);
}
setActivityData(activityData => ({
@@ -230,7 +255,7 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
}, [chartData, queryData, dashboardData]);
useEffect(() => {
if (activityData?.Viewed?.length) {
if (!collapseState && activityData?.Viewed?.length) {
setActiveState(activeState => ['1', ...activeState]);
}
}, [activityData]);
@@ -263,12 +288,12 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
loadedCount={loadedCount}
/>
) : (
<Loading position="inline" />
<LoadingCards />
)}
</Collapse.Panel>
<Collapse.Panel header={t('Dashboards')} key="2">
{!dashboardData || isRecentActivityLoading ? (
<Loading position="inline" />
<LoadingCards cover={checked} />
) : (
<DashboardTable
user={user}
@@ -280,7 +305,7 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
</Collapse.Panel>
<Collapse.Panel header={t('Charts')} key="3">
{!chartData || isRecentActivityLoading ? (
<Loading position="inline" />
<LoadingCards cover={checked} />
) : (
<ChartTable
showThumbnails={checked}
@@ -292,7 +317,7 @@ function Welcome({ user, addDangerToast }: WelcomeProps) {
</Collapse.Panel>
<Collapse.Panel header={t('Saved queries')} key="4">
{!queryData ? (
<Loading position="inline" />
<LoadingCards cover={checked} />
) : (
<SavedQueries
showThumbnails={checked}

View File

@@ -16,7 +16,7 @@
* specific language governing permissions and limitations
* under the License.
*/
import { isFeatureEnabled, Preset } from '@superset-ui/core';
import { isFeatureEnabled, Preset, FeatureFlag } from '@superset-ui/core';
import {
BigNumberChartPlugin,
BigNumberTotalChartPlugin,
@@ -56,7 +56,13 @@ import { DeckGLChartPreset } from '@superset-ui/legacy-preset-chart-deckgl';
import {
EchartsPieChartPlugin,
EchartsBoxPlotChartPlugin,
EchartsAreaChartPlugin,
EchartsTimeseriesChartPlugin,
EchartsTimeseriesBarChartPlugin,
EchartsTimeseriesLineChartPlugin,
EchartsTimeseriesScatterChartPlugin,
EchartsTimeseriesSmoothLineChartPlugin,
EchartsTimeseriesStepChartPlugin,
EchartsGraphChartPlugin,
EchartsGaugeChartPlugin,
EchartsRadarChartPlugin,
@@ -76,7 +82,6 @@ import {
import { PivotTableChartPlugin as PivotTableChartPluginV2 } from '@superset-ui/plugin-chart-pivot-table';
import FilterBoxChartPlugin from '../FilterBox/FilterBoxChartPlugin';
import TimeTableChartPlugin from '../TimeTable/TimeTableChartPlugin';
import { FeatureFlag } from '../../featureFlags';
export default class MainPreset extends Preset {
constructor() {
@@ -134,9 +139,27 @@ export default class MainPreset extends Preset {
new TreemapChartPlugin().configure({ key: 'treemap' }),
new WordCloudChartPlugin().configure({ key: 'word_cloud' }),
new WorldMapChartPlugin().configure({ key: 'world_map' }),
new EchartsAreaChartPlugin().configure({
key: 'echarts_area',
}),
new EchartsTimeseriesChartPlugin().configure({
key: 'echarts_timeseries',
}),
new EchartsTimeseriesBarChartPlugin().configure({
key: 'echarts_timeseries_bar',
}),
new EchartsTimeseriesLineChartPlugin().configure({
key: 'echarts_timeseries_line',
}),
new EchartsTimeseriesSmoothLineChartPlugin().configure({
key: 'echarts_timeseries_smooth',
}),
new EchartsTimeseriesScatterChartPlugin().configure({
key: 'echarts_timeseries_scatter',
}),
new EchartsTimeseriesStepChartPlugin().configure({
key: 'echarts_timeseries_step',
}),
new SelectFilterPlugin().configure({ key: 'filter_select' }),
new RangeFilterPlugin().configure({ key: 'filter_range' }),
new TimeFilterPlugin().configure({ key: 'filter_time' }),

View File

@@ -7,7 +7,7 @@
"forceConsistentCasingInFileNames": true,
"importHelpers": false,
"jsx": "preserve",
"lib": ["dom", "esnext"],
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "node",
"noImplicitAny": true,

View File

@@ -107,7 +107,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
RouteMethod.IMPORT,
RouteMethod.RELATED,
"bulk_delete", # not using RouteMethod since locally defined
"post_data",
"data",
"get_data",
"data_from_cache",
"viz_types",
@@ -152,6 +152,10 @@ class ChartRestApi(BaseSupersetModelRestApi):
"description_markeddown",
"edit_url",
"id",
"last_saved_at",
"last_saved_by.id",
"last_saved_by.first_name",
"last_saved_by.last_name",
"owners.first_name",
"owners.id",
"owners.last_name",
@@ -170,12 +174,20 @@ class ChartRestApi(BaseSupersetModelRestApi):
"changed_on_delta_humanized",
"datasource_id",
"datasource_name",
"last_saved_at",
"last_saved_by.id",
"last_saved_by.first_name",
"last_saved_by.last_name",
"slice_name",
"viz_type",
]
search_columns = [
"created_by",
"changed_by",
"last_saved_at",
"last_saved_by.id",
"last_saved_by.first_name",
"last_saved_by.last_name",
"datasource_id",
"datasource_name",
"datasource_type",
@@ -641,7 +653,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
action=lambda self, *args, **kwargs: f"{self.__class__.__name__}.data",
log_to_statsd=False,
)
def post_data(self) -> Response:
def data(self) -> Response:
"""
Takes a query context constructed in the client and returns payload
data response for the given query.
@@ -989,6 +1001,7 @@ class ChartRestApi(BaseSupersetModelRestApi):
)
# If not screenshot then send request to compute thumb to celery
if not screenshot:
self.incr_stats("async", self.thumbnail.__name__)
logger.info(
"Triggering thumbnail compute (chart id: %s) ASYNC", str(chart.id)
)
@@ -996,11 +1009,13 @@ class ChartRestApi(BaseSupersetModelRestApi):
return self.response(202, message="OK Async")
# If digests
if chart.digest != digest:
self.incr_stats("redirect", self.thumbnail.__name__)
return redirect(
url_for(
f"{self.__class__.__name__}.thumbnail", pk=pk, digest=chart.digest
)
)
self.incr_stats("from_cache", self.thumbnail.__name__)
return Response(
FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True
)

View File

@@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
@@ -43,6 +44,8 @@ class CreateChartCommand(BaseCommand):
def run(self) -> Model:
self.validate()
try:
self._properties["last_saved_at"] = datetime.now()
self._properties["last_saved_by"] = self._actor
chart = ChartDAO.create(self._properties)
except DAOCreateFailedError as ex:
logger.exception(ex.exception)

View File

@@ -15,6 +15,7 @@
# specific language governing permissions and limitations
# under the License.
import logging
from datetime import datetime
from typing import Any, Dict, List, Optional
from flask_appbuilder.models.sqla import Model
@@ -51,6 +52,9 @@ class UpdateChartCommand(BaseCommand):
def run(self) -> Model:
self.validate()
try:
if self._properties.get("query_context_generation") is None:
self._properties["last_saved_at"] = datetime.now()
self._properties["last_saved_by"] = self._actor
chart = ChartDAO.update(self._model, self._properties)
except DAOUpdateFailedError as ex:
logger.exception(ex.exception)

View File

@@ -82,6 +82,11 @@ query_context_description = (
"in order to generate the data the visualization, and in what "
"format the data should be returned."
)
query_context_generation_description = (
"The query context generation represents whether the query_context"
"is user generated or not so that it does not update user modfied"
"state."
)
cache_timeout_description = (
"Duration (in seconds) of the caching timeout "
"for this chart. Note this defaults to the datasource/table"
@@ -177,6 +182,9 @@ class ChartPostSchema(Schema):
allow_none=True,
validate=utils.validate_json,
)
query_context_generation = fields.Boolean(
description=query_context_generation_description, allow_none=True
)
cache_timeout = fields.Integer(
description=cache_timeout_description, allow_none=True
)
@@ -212,6 +220,9 @@ class ChartPutSchema(Schema):
query_context = fields.String(
description=query_context_description, allow_none=True
)
query_context_generation = fields.Boolean(
description=query_context_generation_description, allow_none=True
)
cache_timeout = fields.Integer(
description=cache_timeout_description, allow_none=True
)
@@ -294,6 +305,13 @@ class ChartDataAdhocMetricSchema(Schema):
"will be generated.",
example="metric_aec60732-fac0-4b17-b736-93f1a5c93e30",
)
timeGrain = fields.String(
description="Optional time grain for temporal filters", example="PT1M",
)
isExtra = fields.Boolean(
description="Indicates if the filter has been added by a filter component as "
"opposed to being a part of the original query."
)
class ChartDataAggregateConfigField(fields.Dict):
@@ -772,6 +790,13 @@ class ChartDataFilterSchema(Schema):
"integer, decimal or list, depending on the operator.",
example=["China", "France", "Japan"],
)
grain = fields.String(
description="Optional time grain for temporal filters", example="PT1M",
)
isExtra = fields.Boolean(
description="Indicates if the filter has been added by a filter component as "
"opposed to being a part of the original query."
)
class ChartDataExtrasSchema(Schema):

View File

@@ -124,8 +124,9 @@ def load_examples_run(
examples.load_css_templates()
print("Loading energy related dataset")
examples.load_energy(only_metadata, force)
if load_test_data:
print("Loading energy related dataset")
examples.load_energy(only_metadata, force)
print("Loading [World Bank's Health Nutrition and Population Stats]")
examples.load_world_bank_health_n_pop(only_metadata, force)
@@ -133,25 +134,17 @@ def load_examples_run(
print("Loading [Birth names]")
examples.load_birth_names(only_metadata, force)
print("Loading [Tabbed dashboard]")
examples.load_tabbed_dashboard(only_metadata)
if load_test_data:
print("Loading [Tabbed dashboard]")
examples.load_tabbed_dashboard(only_metadata)
if not load_test_data:
print("Loading [Random time series data]")
examples.load_random_time_series_data(only_metadata, force)
print("Loading [Random long/lat data]")
examples.load_long_lat_data(only_metadata, force)
print("Loading [Country Map data]")
examples.load_country_map_data(only_metadata, force)
print("Loading [Multiformat time series]")
examples.load_multiformat_time_series(only_metadata, force)
print("Loading [Paris GeoJson]")
examples.load_paris_iris_geojson(only_metadata, force)
print("Loading [San Francisco population polygons]")
examples.load_sf_population_polygons(only_metadata, force)

View File

@@ -36,6 +36,7 @@ from superset.utils.core import (
get_metric_names,
is_adhoc_metric,
json_int_dttm_ser,
QueryObjectFilterClause,
)
from superset.utils.date_parser import get_since_until, parse_human_timedelta
from superset.utils.hashing import md5_sha_from_dict
@@ -85,7 +86,7 @@ class QueryObject:
metrics: Optional[List[Metric]]
row_limit: int
row_offset: int
filter: List[Dict[str, Any]]
filter: List[QueryObjectFilterClause]
timeseries_limit: int
timeseries_limit_metric: Optional[Metric]
order_desc: bool
@@ -108,7 +109,7 @@ class QueryObject:
granularity: Optional[str] = None,
metrics: Optional[List[Metric]] = None,
groupby: Optional[List[str]] = None,
filters: Optional[List[Dict[str, Any]]] = None,
filters: Optional[List[QueryObjectFilterClause]] = None,
time_range: Optional[str] = None,
time_shift: Optional[str] = None,
is_timeseries: Optional[bool] = None,

View File

@@ -1229,6 +1229,9 @@ GLOBAL_ASYNC_QUERIES_WEBSOCKET_URL = "ws://127.0.0.1:8080/"
#
DATASET_HEALTH_CHECK: Optional[Callable[["SqlaTable"], str]] = None
# Do not show user info or profile in the menu
MENU_HIDE_USER_INFO = False
# SQLalchemy link doc reference
SQLALCHEMY_DOCS_URL = "https://docs.sqlalchemy.org/en/13/core/engines.html"
SQLALCHEMY_DISPLAY_TEXT = "SQLAlchemy docs"

View File

@@ -375,6 +375,8 @@ class BaseDatasource(
return None
if value == "<empty string>":
return ""
if target_column_type == utils.GenericDataType.BOOLEAN:
return utils.cast_to_boolean(value)
return value
if isinstance(values, (list, tuple)):

View File

@@ -86,7 +86,11 @@ from superset.models.helpers import AuditMixinNullable, QueryResult
from superset.sql_parse import ParsedQuery
from superset.typing import AdhocMetric, Metric, OrderBy, QueryObjectDict
from superset.utils import core as utils
from superset.utils.core import GenericDataType, remove_duplicates
from superset.utils.core import (
GenericDataType,
QueryObjectFilterClause,
remove_duplicates,
)
config = app.config
metadata = Model.metadata # pylint: disable=no-member
@@ -303,13 +307,15 @@ class TableColumn(Model, BaseColumn):
pdf = self.python_date_format
is_epoch = pdf in ("epoch_s", "epoch_ms")
column_spec = self.db_engine_spec.get_column_spec(self.type)
type_ = column_spec.sqla_type if column_spec else DateTime
if not self.expression and not time_grain and not is_epoch:
sqla_col = column(self.column_name, type_=DateTime)
sqla_col = column(self.column_name, type_=type_)
return self.table.make_sqla_column_compatible(sqla_col, label)
if self.expression:
col = literal_column(self.expression)
col = literal_column(self.expression, type_=type_)
else:
col = column(self.column_name)
col = column(self.column_name, type_=type_)
time_expr = self.db_engine_spec.get_timestamp_expr(
col, pdf, time_grain, self.type
)
@@ -496,7 +502,7 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
table_name = Column(String(250), nullable=False)
main_dttm_col = Column(String(250))
database_id = Column(Integer, ForeignKey("dbs.id"), nullable=False)
fetch_values_predicate = Column(String(1000))
fetch_values_predicate = Column(Text)
owners = relationship(owner_class, secondary=sqlatable_user, backref="tables")
database: Database = relationship(
"Database",
@@ -935,7 +941,7 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
columns: Optional[List[str]] = None,
groupby: Optional[List[str]] = None,
filter: Optional[ # pylint: disable=redefined-builtin
List[Dict[str, Any]]
List[QueryObjectFilterClause]
] = None,
is_timeseries: bool = True,
timeseries_limit: int = 15,
@@ -1056,6 +1062,8 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
# filter out the pseudo column __timestamp from columns
columns = columns or []
columns = [col for col in columns if col != utils.DTTM_ALIAS]
time_grain = extras.get("time_grain_sqla")
dttm_col = columns_by_name.get(granularity) if granularity else None
if need_groupby:
# dedup columns while preserving order
@@ -1063,7 +1071,6 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
for selected in columns:
# if groupby field/expr equals granularity field/expr
if selected == granularity:
time_grain = extras.get("time_grain_sqla")
sqla_col = columns_by_name[selected]
outer = sqla_col.get_timestamp_expression(time_grain, selected)
# if groupby field equals a selected column
@@ -1087,15 +1094,13 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
groupby_exprs_with_timestamp = OrderedDict(groupby_exprs_sans_timestamp.items())
if granularity:
if granularity not in columns_by_name:
if granularity not in columns_by_name or not dttm_col:
raise QueryObjectValidationError(
_(
'Time column "%(col)s" does not exist in dataset',
col=granularity,
)
)
dttm_col = columns_by_name[granularity]
time_grain = extras.get("time_grain_sqla")
time_filters = []
if is_timeseries:
@@ -1150,7 +1155,12 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
col = flt["col"]
val = flt.get("val")
op = flt["op"].upper()
col_obj = columns_by_name.get(col)
col_obj = (
dttm_col
if col == utils.DTTM_ALIAS and is_timeseries and dttm_col
else columns_by_name.get(col)
)
filter_grain = flt.get("grain")
if is_feature_enabled("ENABLE_TEMPLATE_REMOVE_FILTERS"):
if col in removed_filters:
@@ -1158,6 +1168,10 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
continue
if col_obj:
if filter_grain:
sqla_col = col_obj.get_timestamp_expression(filter_grain)
else:
sqla_col = col_obj.get_sqla_col()
col_spec = db_engine_spec.get_column_spec(col_obj.type)
is_list_target = op in (
utils.FilterOperator.IN.value,
@@ -1180,24 +1194,24 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
)
if None in eq:
eq = [x for x in eq if x is not None]
is_null_cond = col_obj.get_sqla_col().is_(None)
is_null_cond = sqla_col.is_(None)
if eq:
cond = or_(is_null_cond, col_obj.get_sqla_col().in_(eq))
cond = or_(is_null_cond, sqla_col.in_(eq))
else:
cond = is_null_cond
else:
cond = col_obj.get_sqla_col().in_(eq)
cond = sqla_col.in_(eq)
if op == utils.FilterOperator.NOT_IN.value:
cond = ~cond
where_clause_and.append(cond)
elif op == utils.FilterOperator.IS_NULL.value:
where_clause_and.append(col_obj.get_sqla_col().is_(None))
where_clause_and.append(sqla_col.is_(None))
elif op == utils.FilterOperator.IS_NOT_NULL.value:
where_clause_and.append(col_obj.get_sqla_col().isnot(None))
where_clause_and.append(sqla_col.isnot(None))
elif op == utils.FilterOperator.IS_TRUE.value:
where_clause_and.append(col_obj.get_sqla_col().is_(True))
where_clause_and.append(sqla_col.is_(True))
elif op == utils.FilterOperator.IS_FALSE.value:
where_clause_and.append(col_obj.get_sqla_col().is_(False))
where_clause_and.append(sqla_col.is_(False))
else:
if eq is None:
raise QueryObjectValidationError(
@@ -1207,21 +1221,21 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
)
)
if op == utils.FilterOperator.EQUALS.value:
where_clause_and.append(col_obj.get_sqla_col() == eq)
where_clause_and.append(sqla_col == eq)
elif op == utils.FilterOperator.NOT_EQUALS.value:
where_clause_and.append(col_obj.get_sqla_col() != eq)
where_clause_and.append(sqla_col != eq)
elif op == utils.FilterOperator.GREATER_THAN.value:
where_clause_and.append(col_obj.get_sqla_col() > eq)
where_clause_and.append(sqla_col > eq)
elif op == utils.FilterOperator.LESS_THAN.value:
where_clause_and.append(col_obj.get_sqla_col() < eq)
where_clause_and.append(sqla_col < eq)
elif op == utils.FilterOperator.GREATER_THAN_OR_EQUALS.value:
where_clause_and.append(col_obj.get_sqla_col() >= eq)
where_clause_and.append(sqla_col >= eq)
elif op == utils.FilterOperator.LESS_THAN_OR_EQUALS.value:
where_clause_and.append(col_obj.get_sqla_col() <= eq)
where_clause_and.append(sqla_col <= eq)
elif op == utils.FilterOperator.LIKE.value:
where_clause_and.append(col_obj.get_sqla_col().like(eq))
where_clause_and.append(sqla_col.like(eq))
elif op == utils.FilterOperator.ILIKE.value:
where_clause_and.append(col_obj.get_sqla_col().ilike(eq))
where_clause_and.append(sqla_col.ilike(eq))
else:
raise QueryObjectValidationError(
_("Invalid filter operation type: %(op)s", op=op)
@@ -1281,6 +1295,7 @@ class SqlaTable( # pylint: disable=too-many-public-methods,too-many-instance-at
and timeseries_limit
and not time_groupby_inline
and groupby_exprs_sans_timestamp
and dttm_col
):
if db_engine_spec.allows_joins:
# some sql dialects require for order by expressions

View File

@@ -125,6 +125,7 @@ MODEL_API_RW_METHOD_PERMISSION_MAP = {
"get_datasets": "read",
"function_names": "read",
"available": "read",
"get_data": "read",
}
EXTRA_FORM_DATA_APPEND_KEYS = {

View File

@@ -820,10 +820,12 @@ class DashboardRestApi(BaseSupersetModelRestApi):
).get_from_cache(cache=thumbnail_cache)
# If the screenshot does not exist, request one from the workers
if not screenshot:
self.incr_stats("async", self.thumbnail.__name__)
cache_dashboard_thumbnail.delay(dashboard_url, dashboard.digest, force=True)
return self.response(202, message="OK Async")
# If digests
if dashboard.digest != digest:
self.incr_stats("redirect", self.thumbnail.__name__)
return redirect(
url_for(
f"{self.__class__.__name__}.thumbnail",
@@ -831,6 +833,7 @@ class DashboardRestApi(BaseSupersetModelRestApi):
digest=dashboard.digest,
)
)
self.incr_stats("from_cache", self.thumbnail.__name__)
return Response(
FileWrapper(screenshot), mimetype="image/png", direct_passthrough=True
)

View File

@@ -44,7 +44,7 @@ from flask import current_app, g
from flask_babel import gettext as __, lazy_gettext as _
from marshmallow import fields, Schema
from marshmallow.validate import Range
from sqlalchemy import column, DateTime, select, types
from sqlalchemy import column, select, types
from sqlalchemy.engine.base import Engine
from sqlalchemy.engine.interfaces import Compiled, Dialect
from sqlalchemy.engine.reflection import Inspector
@@ -381,7 +381,7 @@ class BaseEngineSpec: # pylint: disable=too-many-public-methods
elif pdf == "epoch_ms":
time_expr = time_expr.replace("{col}", cls.epoch_ms_to_dttm())
return TimestampExpression(time_expr, col, type_=DateTime)
return TimestampExpression(time_expr, col, type_=col.type)
@classmethod
def get_time_grains(cls) -> Tuple[TimeGrain, ...]:

View File

@@ -27,7 +27,7 @@ from .helpers import get_example_data, get_table_connector_registry
def load_bart_lines(only_metadata: bool = False, force: bool = False) -> None:
tbl_name = "bart_lines"
tbl_name = "San Franciso BART Lines"
database = get_example_database()
table_exists = database.has_table_by_name(tbl_name)

View File

@@ -16,7 +16,7 @@
# under the License.
dashboard_title: COVID Vaccine Dashboard
description: null
css: ''
css: ""
slug: null
uuid: f4065089-110a-41fa-8dd7-9ce98a65e250
position:
@@ -25,111 +25,63 @@ position:
id: CHART-63bEuxjDMJ
meta:
chartId: 3961
height: 76
height: 72
sliceName: Vaccine Candidates per Country
sliceNameOverride: Map of Vaccine Candidates
uuid: ddc91df6-fb40-4826-bdca-16b85af1c024
width: 7
width: 12
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zvw7luvEL
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zvw7luvEL
type: CHART
CHART-F-fkth0Dnv:
children: []
id: CHART-F-fkth0Dnv
meta:
chartId: 3960
height: 76
height: 60
sliceName: Vaccine Candidates per Country
sliceNameOverride: Treemap of Vaccine Candidates per Country
uuid: e2f5a8a7-feb0-4f79-bc6b-01fe55b98b3c
width: 5
width: 8
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zvw7luvEL
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-xSeNAspgw
type: CHART
CHART-RjD_ygqtwH:
children: []
id: CHART-RjD_ygqtwH
meta:
chartId: 3957
height: 59
height: 72
sliceName: Vaccine Candidates per Phase
sliceNameOverride: Vaccine Candidates per Phase
uuid: 30b73c65-85e7-455f-bb24-801bb0cdc670
width: 2
width: 3
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-xSeNAspgw
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zvw7luvEL
type: CHART
CHART-aGfmWtliqA:
children: []
id: CHART-aGfmWtliqA
meta:
chartId: 3956
height: 59
height: 72
sliceName: Vaccine Candidates per Phase
uuid: 392f293e-0892-4316-bd41-c927b65606a4
width: 4
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-xSeNAspgw
type: CHART
CHART-dCUpAcPsji:
children: []
id: CHART-dCUpAcPsji
meta:
chartId: 3963
height: 82
sliceName: Vaccine Candidates per Country & Stage
sliceNameOverride: Heatmap of Countries & Clinical Stages
uuid: cd111331-d286-4258-9020-c7949a109ed2
width: 4
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zhOlQLQnB
type: CHART
CHART-eirDduqb1A:
children: []
id: CHART-eirDduqb1A
meta:
chartId: 3965
height: 59
sliceName: Filtering Vaccines
sliceNameOverride: Filter Box of Vaccines
uuid: c29381ce-0e99-4cf3-bf0f-5f55d6b94176
width: 3
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-xSeNAspgw
type: CHART
CHART-fYo7IyvKZQ:
children: []
id: CHART-fYo7IyvKZQ
meta:
chartId: 3964
height: 82
sliceName: Vaccine Candidates per Country & Stage
sliceNameOverride: Sunburst of Country & Clinical Stages
uuid: f69c556f-15fe-4a82-a8bb-69d5b6954123
width: 5
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zhOlQLQnB
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zvw7luvEL
type: CHART
CHART-j4hUvP5dDD:
children: []
@@ -140,19 +92,51 @@ position:
sliceName: Vaccine Candidates per Approach & Stage
sliceNameOverride: Heatmap of Aproaches & Clinical Stages
uuid: 0c953c84-0c9a-418d-be9f-2894d2a2cee0
width: 3
width: 4
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zhOlQLQnB
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-dieUdkeUw
type: CHART
CHART-dCUpAcPsji:
children: []
id: CHART-dCUpAcPsji
meta:
chartId: 3963
height: 72
sliceName: Vaccine Candidates per Country & Stage
sliceNameOverride: Heatmap of Countries & Clinical Stages
uuid: cd111331-d286-4258-9020-c7949a109ed2
width: 4
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-dieUdkeUw
type: CHART
CHART-eirDduqb1A:
children: []
id: CHART-eirDduqb1A
meta:
chartId: 3965
height: 60
sliceName: Filtering Vaccines
sliceNameOverride: Filter Box of Vaccines
uuid: c29381ce-0e99-4cf3-bf0f-5f55d6b94176
width: 4
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-xSeNAspgw
type: CHART
DASHBOARD_VERSION_KEY: v2
GRID_ID:
children: []
id: GRID_ID
parents:
- ROOT_ID
- ROOT_ID
type: GRID
HEADER_ID:
id: HEADER_ID
@@ -163,7 +147,7 @@ position:
children: []
id: MARKDOWN-VjQQ5SFj5v
meta:
code: '# COVID-19 Vaccine Dashboard
code: "# COVID-19 Vaccine Dashboard
Everywhere you look, you see negative news about COVID-19. This is to be expected;
@@ -193,129 +177,157 @@ position:
country responsible for each vaccine effort.
_Note that this dataset was last updated on 12/23/2020_.
_Note that this dataset was last updated on 07/2021_.
'
height: 59
width: 3
"
height: 72
width: 4
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-xSeNAspgw
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-zhOlQLQnB
type: MARKDOWN
CHART-fYo7IyvKZQ:
children: []
id: CHART-fYo7IyvKZQ
meta:
chartId: 3964
height: 72
sliceName: Vaccine Candidates per Country & Stage
sliceNameOverride: Sunburst of Country & Clinical Stages
uuid: f69c556f-15fe-4a82-a8bb-69d5b6954123
width: 4
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROW-dieUdkeUw
type: CHART
ROOT_ID:
children:
- TABS-wUKya7eQ0Z
- TABS-wUKya7eQ0Z
id: ROOT_ID
type: ROOT
ROW-xSeNAspgw:
children:
- MARKDOWN-VjQQ5SFj5v
- CHART-aGfmWtliqA
- CHART-RjD_ygqtwH
- CHART-eirDduqb1A
id: ROW-xSeNAspgw
meta:
'0': ROOT_ID
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
type: ROW
ROW-zhOlQLQnB:
children:
- CHART-j4hUvP5dDD
- CHART-dCUpAcPsji
- CHART-fYo7IyvKZQ
- MARKDOWN-VjQQ5SFj5v
- CHART-RjD_ygqtwH
- CHART-aGfmWtliqA
id: ROW-zhOlQLQnB
meta:
'0': ROOT_ID
"0": ROOT_ID
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
type: ROW
ROW-xSeNAspgw:
children:
- CHART-eirDduqb1A
- CHART-F-fkth0Dnv
id: ROW-xSeNAspgw
meta:
"0": ROOT_ID
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
type: ROW
ROW-dieUdkeUw:
children:
- CHART-dCUpAcPsji
- CHART-fYo7IyvKZQ
- CHART-j4hUvP5dDD
id: ROW-xSeNAspgw
meta:
"0": ROOT_ID
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
type: ROW
ROW-zvw7luvEL:
children:
- CHART-63bEuxjDMJ
- CHART-F-fkth0Dnv
- CHART-63bEuxjDMJ
id: ROW-zvw7luvEL
meta:
'0': ROOT_ID
"0": ROOT_ID
background: BACKGROUND_TRANSPARENT
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
- ROOT_ID
- TABS-wUKya7eQ0Z
- TAB-BCIJF4NvgQ
type: ROW
TAB-BCIJF4NvgQ:
children:
- ROW-xSeNAspgw
- ROW-zvw7luvEL
- ROW-zhOlQLQnB
- ROW-zhOlQLQnB
- ROW-xSeNAspgw
- ROW-zvw7luvEL
- ROW-dieUdkeUw
id: TAB-BCIJF4NvgQ
meta:
text: Overview
parents:
- ROOT_ID
- TABS-wUKya7eQ0Z
- ROOT_ID
- TABS-wUKya7eQ0Z
type: TAB
TABS-wUKya7eQ0Z:
children:
- TAB-BCIJF4NvgQ
- TAB-BCIJF4NvgQ
id: TABS-wUKya7eQ0Z
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:
'0': '#D3B3DA'
'1': '#9EE5E5'
0. Pre-clinical: '#1FA8C9'
2. Phase II or Combined I/II: '#454E7C'
1. Phase I: '#5AC189'
3. Phase III: '#FF7F44'
4. Authorized: '#666666'
root: '#1FA8C9'
Protein subunit: '#454E7C'
Phase II: '#5AC189'
Pre-clinical: '#FF7F44'
Phase III: '#666666'
Phase I: '#E04355'
Phase I/II: '#FCC700'
Inactivated virus: '#A868B7'
Virus-like particle: '#3CCCCB'
Replicating bacterial vector: '#A38F79'
DNA-based: '#8FD3E4'
RNA-based vaccine: '#A1A6BD'
Authorized: '#ACE1C4'
Non-replicating viral vector: '#FEC0A1'
Replicating viral vector: '#B2B2B2'
Unknown: '#EFA1AA'
Live attenuated virus: '#FDE380'
COUNT(*): '#D1C6BC'
"0": "#D3B3DA"
"1": "#9EE5E5"
0. Pre-clinical: "#1FA8C9"
2. Phase II or Combined I/II: "#454E7C"
1. Phase I: "#5AC189"
3. Phase III: "#FF7F44"
4. Authorized: "#666666"
root: "#1FA8C9"
Protein subunit: "#454E7C"
Phase II: "#5AC189"
Pre-clinical: "#FF7F44"
Phase III: "#666666"
Phase I: "#E04355"
Phase I/II: "#FCC700"
Inactivated virus: "#A868B7"
Virus-like particle: "#3CCCCB"
Replicating bacterial vector: "#A38F79"
DNA-based: "#8FD3E4"
RNA-based vaccine: "#A1A6BD"
Authorized: "#ACE1C4"
Non-replicating viral vector: "#FEC0A1"
Replicating viral vector: "#B2B2B2"
Unknown: "#EFA1AA"
Live attenuated virus: "#FDE380"
COUNT(*): "#D1C6BC"
filter_scopes:
'3965':
"3965":
Country_Name:
scope:
- ROOT_ID
- ROOT_ID
immune: []
Product_Category:
scope:
- ROOT_ID
- ROOT_ID
immune: []
Clinical Stage:
scope:
- ROOT_ID
- ROOT_ID
immune: []
version: 1.0.0

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.
table_name: FCC 2018 Survey
table_name: FCC Survey Results
main_dttm_col: null
description: null
default_endpoint: null

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.
table_name: channel_members
table_name: Slack Channels and Members
main_dttm_col: null
description: null
default_endpoint: null

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.
table_name: channels
table_name: Slack Channels
main_dttm_col: created
description: null
default_endpoint: null

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.
table_name: cleaned_sales_data
table_name: Vehicle Sales
main_dttm_col: OrderDate
description: null
default_endpoint: null

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.
table_name: covid_vaccines
table_name: COVID Vaccines
main_dttm_col: null
description: null
default_endpoint: null

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.
table_name: exported_stats
table_name: Slack Exported Metrics
main_dttm_col: Date
description: null
default_endpoint: null

View File

@@ -14,14 +14,14 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: members_channels_2
table_name: Slack Members and Channels
main_dttm_col: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
schema: null
sql: SELECT c.name AS channel_name, u.name AS member_name FROM channel_members cm
sql: SELECT c.name AS channel_name, u.name AS member_name FROM "Slack Channels and Members" cm
JOIN channels c ON cm.channel_id = c.id JOIN users u ON cm.user_id = u.id
params: null
template_params: null

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.
table_name: messages
table_name: Slack Messages
main_dttm_col: bot_profile__updated
description: null
default_endpoint: null

View File

@@ -14,14 +14,14 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: messages_channels
table_name: Slack Messages and Channels
main_dttm_col: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
schema: null
sql: SELECT m.ts, c.name, m.text FROM messages m JOIN channels c ON m.channel_id =
sql: SELECT m.ts, c.name, m.text FROM messages m JOIN "Slack Messages" c ON m.channel_id =
c.id
params: null
template_params: null

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.
table_name: new_members_daily
table_name: Slack Daily Member Count
main_dttm_col: null
description: null
default_endpoint: null
@@ -22,7 +22,7 @@ offset: 0
cache_timeout: null
schema: null
sql: SELECT date, total_membership - lag(total_membership) OVER (ORDER BY date) AS
new_members FROM exported_stats
new_members FROM "Slack Exported Metrics"
params: null
template_params: null
filter_select_enabled: true

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.
table_name: threads
table_name: Slack Threads
main_dttm_col: ts
description: null
default_endpoint: null

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.
table_name: users
table_name: Slack Users
main_dttm_col: updated
description: null
default_endpoint: null
@@ -29,195 +29,195 @@ fetch_values_predicate: null
extra: null
uuid: 7195db6b-2d17-7619-b7c7-26b15378df8c
metrics:
- metric_name: count
verbose_name: COUNT(*)
metric_type: count
expression: COUNT(*)
description: null
d3format: null
extra: null
warning_text: null
- metric_name: count
verbose_name: COUNT(*)
metric_type: count
expression: COUNT(*)
description: null
d3format: null
extra: null
warning_text: null
columns:
- column_name: updated
verbose_name: null
is_dttm: true
is_active: true
type: TIMESTAMP WITHOUT TIME ZONE
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: has_2fa
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: real_name
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: tz_label
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: team_id
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: name
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: color
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: id
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: tz
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_ultra_restricted
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_primary_owner
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_app_user
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_admin
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_bot
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_restricted
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_owner
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: deleted
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: tz_offset
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: updated
verbose_name: null
is_dttm: true
is_active: true
type: TIMESTAMP WITHOUT TIME ZONE
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: has_2fa
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: real_name
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: tz_label
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: team_id
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: name
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: color
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: id
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: tz
verbose_name: null
is_dttm: false
is_active: true
type: VARCHAR
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_ultra_restricted
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_primary_owner
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_app_user
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_admin
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_bot
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_restricted
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: is_owner
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: deleted
verbose_name: null
is_dttm: false
is_active: true
type: BOOLEAN
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: tz_offset
verbose_name: null
is_dttm: false
is_active: true
type: BIGINT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
version: 1.0.0
database_uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee
data: https://raw.githubusercontent.com/apache-superset/examples-data/master/datasets/examples/slack/users.csv

View File

@@ -14,18 +14,19 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: users_channels-uzooNNtSRO
table_name: Slack Channel Combinations and Users
main_dttm_col: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
schema: null
sql: 'SELECT uc1.name as channel_1, uc2.name as channel_2, count(*) AS cnt
FROM users_channels uc1
JOIN users_channels uc2 ON uc1.user_id = uc2.user_id
sql: >
SELECT uc1.name as channel_1, uc2.name as channel_2, count(*) AS cnt
FROM "Slack Users and Channels" uc1
JOIN "Slack Users and Channels" uc2 ON uc1.user_id = uc2.user_id
GROUP BY uc1.name, uc2.name
HAVING uc1.name <> uc2.name'
HAVING uc1.name <> uc2.name
params: null
template_params: null
filter_select_enabled: true
@@ -33,44 +34,44 @@ fetch_values_predicate: null
extra: null
uuid: 473d6113-b44a-48d8-a6ae-e0ef7e2aebb0
metrics:
- metric_name: count
verbose_name: null
metric_type: null
expression: count(*)
description: null
d3format: null
extra: null
warning_text: null
- metric_name: count
verbose_name: null
metric_type: null
expression: count(*)
description: null
d3format: null
extra: null
warning_text: null
columns:
- column_name: channel_1
verbose_name: null
is_dttm: false
is_active: true
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: channel_2
verbose_name: null
is_dttm: false
is_active: true
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: cnt
verbose_name: null
is_dttm: false
is_active: true
type: INT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: channel_1
verbose_name: null
is_dttm: false
is_active: true
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: channel_2
verbose_name: null
is_dttm: false
is_active: true
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: cnt
verbose_name: null
is_dttm: false
is_active: true
type: INT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
version: 1.0.0
database_uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee

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.
table_name: users_channels
table_name: Slack Users and Channels
main_dttm_col: null
description: null
default_endpoint: null
@@ -29,35 +29,35 @@ fetch_values_predicate: null
extra: null
uuid: 29b18573-c9d6-40bc-b8cb-f70c9a1b6244
metrics:
- metric_name: count
verbose_name: null
metric_type: null
expression: count(*)
description: null
d3format: null
extra: null
warning_text: null
- metric_name: count
verbose_name: null
metric_type: null
expression: count(*)
description: null
d3format: null
extra: null
warning_text: null
columns:
- column_name: user_id
verbose_name: null
is_dttm: false
is_active: true
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: name
verbose_name: null
is_dttm: false
is_active: true
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: user_id
verbose_name: null
is_dttm: false
is_active: true
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: name
verbose_name: null
is_dttm: false
is_active: true
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
version: 1.0.0
database_uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee
data: https://raw.githubusercontent.com/apache-superset/examples-data/master/datasets/examples/slack/users_channels.csv

View File

@@ -14,14 +14,14 @@
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
table_name: video_game_sales
table_name: Video Game Sales
main_dttm_col: null
description: null
default_endpoint: null
offset: 0
cache_timeout: null
schema: null
sql: ''
sql: ""
params:
remote_id: 64
database_name: examples
@@ -32,125 +32,125 @@ fetch_values_predicate: null
extra: null
uuid: 53d47c0c-c03d-47f0-b9ac-81225f808283
metrics:
- metric_name: count
verbose_name: COUNT(*)
metric_type: null
expression: COUNT(*)
description: null
d3format: null
extra: null
warning_text: null
- metric_name: count
verbose_name: COUNT(*)
metric_type: null
expression: COUNT(*)
description: null
d3format: null
extra: null
warning_text: null
columns:
- column_name: year
verbose_name: null
is_dttm: true
is_active: null
type: BIGINT
groupby: true
filterable: true
expression: null
description: null
python_date_format: '%Y'
- column_name: na_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: eu_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: global_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: jp_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: other_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: rank
verbose_name: null
is_dttm: false
is_active: null
type: BIGINT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: genre
verbose_name: null
is_dttm: false
is_active: null
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: name
verbose_name: null
is_dttm: false
is_active: null
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: platform
verbose_name: null
is_dttm: false
is_active: null
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: publisher
verbose_name: null
is_dttm: false
is_active: null
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: year
verbose_name: null
is_dttm: true
is_active: null
type: BIGINT
groupby: true
filterable: true
expression: null
description: null
python_date_format: "%Y"
- column_name: na_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: eu_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: global_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: jp_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: other_sales
verbose_name: null
is_dttm: false
is_active: null
type: FLOAT64
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: rank
verbose_name: null
is_dttm: false
is_active: null
type: BIGINT
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: genre
verbose_name: null
is_dttm: false
is_active: null
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: name
verbose_name: null
is_dttm: false
is_active: null
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: platform
verbose_name: null
is_dttm: false
is_active: null
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
- column_name: publisher
verbose_name: null
is_dttm: false
is_active: null
type: STRING
groupby: true
filterable: true
expression: null
description: null
python_date_format: null
version: 1.0.0
database_uuid: a2dc77af-e654-49bb-b321-40f6b559a1ee
data: https://github.com/apache-superset/examples-data/raw/lowercase_columns_examples/datasets/examples/video_game_sales.csv

View File

@@ -176,7 +176,7 @@ def load_deck_dash() -> None:
print("Loading deck.gl dashboard")
slices = []
table = get_table_connector_registry()
tbl = db.session.query(table).filter_by(table_name="long_lat").first()
tbl = db.session.query(table).filter_by(table_name="Sample Geodata").first()
slice_data = {
"spatial": {"type": "latlong", "lonCol": "LON", "latCol": "LAT"},
"color_picker": COLOR_RED,
@@ -323,7 +323,9 @@ def load_deck_dash() -> None:
slices.append(slc)
polygon_tbl = (
db.session.query(table).filter_by(table_name="sf_population_polygons").first()
db.session.query(table)
.filter_by(table_name="San Francisco Population Polygons")
.first()
)
slice_data = {
"datasource": "11__table",
@@ -456,7 +458,7 @@ def load_deck_dash() -> None:
viz_type="deck_arc",
datasource_type="table",
datasource_id=db.session.query(table)
.filter_by(table_name="flights")
.filter_by(table_name="Flights")
.first()
.id,
params=get_slice_json(slice_data),
@@ -508,7 +510,7 @@ def load_deck_dash() -> None:
viz_type="deck_path",
datasource_type="table",
datasource_id=db.session.query(table)
.filter_by(table_name="bart_lines")
.filter_by(table_name="San Franciso BART Lines")
.first()
.id,
params=get_slice_json(slice_data),

View File

@@ -56,7 +56,7 @@ def load_energy(
method="multi",
)
print("Creating table [wb_health_population] reference")
print("Creating table [World Bank Health Data] reference")
table = get_table_connector_registry()
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:

View File

@@ -25,7 +25,7 @@ from .helpers import get_example_data, get_table_connector_registry
def load_flights(only_metadata: bool = False, force: bool = False) -> None:
"""Loading random time series data from a zip file in the repo"""
tbl_name = "flights"
tbl_name = "Flights"
database = utils.get_example_database()
table_exists = database.has_table_by_name(tbl_name)

View File

@@ -36,7 +36,7 @@ from .helpers import (
def load_long_lat_data(only_metadata: bool = False, force: bool = False) -> None:
"""Loading lat/long data from a csv file in the repo"""
tbl_name = "long_lat"
tbl_name = "Sample Geodata"
database = utils.get_example_database()
table_exists = database.has_table_by_name(tbl_name)

View File

@@ -72,7 +72,7 @@ def load_random_time_series_data(
tbl = obj
slice_data = {
"granularity_sqla": "day",
"granularity_sqla": "ds",
"row_limit": app.config["ROW_LIMIT"],
"since": "2019-01-01",
"until": "2019-02-01",

View File

@@ -28,7 +28,7 @@ from .helpers import get_example_data, get_table_connector_registry
def load_sf_population_polygons(
only_metadata: bool = False, force: bool = False
) -> None:
tbl_name = "sf_population_polygons"
tbl_name = "San Francisco Population Polygons"
database = utils.get_example_database()
table_exists = database.has_table_by_name(tbl_name)

View File

@@ -45,7 +45,7 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals, too-many-s
only_metadata: bool = False, force: bool = False, sample: bool = False,
) -> None:
"""Loads the world bank health dataset, slices and a dashboard"""
tbl_name = "wb_health_population"
tbl_name = "World Bank Health Data"
database = utils.get_example_database()
table_exists = database.has_table_by_name(tbl_name)
@@ -76,7 +76,7 @@ def load_world_bank_health_n_pop( # pylint: disable=too-many-locals, too-many-s
index=False,
)
print("Creating table [wb_health_population] reference")
print("Creating table [World Bank Health Data] reference")
table = get_table_connector_registry()
tbl = db.session.query(table).filter_by(table_name=tbl_name).first()
if not tbl:

View File

@@ -0,0 +1,84 @@
# 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.
"""change_fetch_values_predicate_to_text
Revision ID: 07071313dd52
Revises: 6d20ba9ecb33
Create Date: 2021-08-09 17:32:56.204184
"""
# revision identifiers, used by Alembic.
revision = "07071313dd52"
down_revision = "6d20ba9ecb33"
import logging
import sqlalchemy as sa
from alembic import op
from sqlalchemy import and_, func, or_
from sqlalchemy.dialects import postgresql
from sqlalchemy.sql.schema import Table
from superset import db
from superset.connectors.sqla.models import SqlaTable
def upgrade():
with op.batch_alter_table("tables") as batch_op:
batch_op.alter_column(
"fetch_values_predicate",
existing_type=sa.String(length=1000),
type_=sa.Text(),
existing_nullable=True,
)
def remove_value_if_too_long():
bind = op.get_bind()
session = db.Session(bind=bind)
# it will be easier for users to notice that their field has been deleted rather than truncated
# so just remove it if it won't fit back into the 1000 string length column
try:
rows = (
session.query(SqlaTable)
.filter(func.length(SqlaTable.fetch_values_predicate) > 1000)
.all()
)
for row in rows:
row.fetch_values_predicate = None
logging.info("%d values deleted", len(rows))
session.commit()
session.close()
except Exception as ex:
logging.warning(ex)
def downgrade():
remove_value_if_too_long()
with op.batch_alter_table("tables") as batch_op:
batch_op.alter_column(
"fetch_values_predicate",
existing_type=sa.Text(),
type_=sa.String(length=1000),
existing_nullable=True,
)

View File

@@ -0,0 +1,108 @@
# 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.
"""migrate pivot table v2 heatmaps to new format
Revision ID: 143b6f2815da
Revises: e323605f370a
Create Date: 2021-08-03 15:36:35.925420
"""
# revision identifiers, used by Alembic.
revision = "143b6f2815da"
down_revision = "e323605f370a"
import json
from typing import Any, Dict, List, Tuple
from alembic import op
from sqlalchemy import and_, Column, Integer, String, Text
from sqlalchemy.ext.declarative import declarative_base
from superset import db
Base = declarative_base()
class Slice(Base):
__tablename__ = "slices"
id = Column(Integer, primary_key=True)
viz_type = Column(String(250))
params = Column(Text)
VALID_RENDERERS = (
"Table With Subtotal",
"Table With Subtotal Heatmap",
"Table With Subtotal Col Heatmap",
"Table With Subtotal Row Heatmap",
"Table With Subtotal Barchart",
"Table With Subtotal Col Barchart",
"Table With Subtotal Row Barchart",
)
def upgrade():
bind = op.get_bind()
session = db.Session(bind=bind)
slices = (
session.query(Slice)
.filter(
and_(
Slice.viz_type == "pivot_table_v2",
Slice.params.like('%"tableRenderer%'),
)
)
.all()
)
changed_slices = 0
for slice in slices:
try:
params = json.loads(slice.params)
table_renderer = params.pop("tableRenderer", None)
conditional_formatting = params.get("conditional_formatting")
# don't update unless table_renderer is valid and
# conditional_formatting is undefined
if table_renderer in VALID_RENDERERS and conditional_formatting is None:
metric_labels = [
metric if isinstance(metric, str) else metric["label"]
for metric in params.get("metrics")
]
params["conditional_formatting"] = [
{
"colorScheme": "rgb(255,0,0)",
"column": metric_label,
"operator": "None",
}
for metric_label in metric_labels
]
changed_slices += 1
slice.params = json.dumps(params, sort_keys=True)
except Exception as e:
print(f"Parsing json_metadata for slice {slice.id} failed.")
raise e
session.commit()
session.close()
print(f"Upgraded {changed_slices} slices.")
def downgrade():
# slices can't be downgraded
pass

View File

@@ -0,0 +1,66 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""add_last_saved_at_to_slice_model
Revision ID: 6d20ba9ecb33
Revises: ('ae1ed299413b', 'f6196627326f')
Create Date: 2021-08-02 21:14:58.200438
"""
# revision identifiers, used by Alembic.
revision = "6d20ba9ecb33"
down_revision = ("ae1ed299413b", "f6196627326f")
import sqlalchemy as sa
from alembic import op
from sqlalchemy.dialects import postgresql
def upgrade():
with op.batch_alter_table("slices") as batch_op:
batch_op.add_column(sa.Column("last_saved_at", sa.DateTime(), nullable=True))
batch_op.add_column(sa.Column("last_saved_by_fk", sa.Integer(), nullable=True))
batch_op.create_foreign_key(
"slices_last_saved_by_fk", "ab_user", ["last_saved_by_fk"], ["id"]
)
# now do data migration, copy values from changed_on and changed_by
slices_table = sa.Table(
"slices",
sa.MetaData(),
sa.Column("changed_on", sa.DateTime(), nullable=True),
sa.Column("changed_by_fk", sa.Integer(), nullable=True),
sa.Column("last_saved_at", sa.DateTime(), nullable=True),
sa.Column("last_saved_by_fk", sa.Integer(), nullable=True),
)
conn = op.get_bind()
conn.execute(
slices_table.update().values(
last_saved_at=slices_table.c.changed_on,
last_saved_by_fk=slices_table.c.changed_by_fk,
)
)
# ### end Alembic commands ###
def downgrade():
with op.batch_alter_table("slices") as batch_op:
batch_op.drop_constraint("slices_last_saved_by_fk", type_="foreignkey")
batch_op.drop_column("last_saved_by_fk")
batch_op.drop_column("last_saved_at")
# ### end Alembic commands ###

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.
"""update chart permissions
Revision ID: f6196627326f
Revises: 143b6f2815da
Create Date: 2021-08-04 17:16:47.714866
"""
from alembic import op
from sqlalchemy.exc import SQLAlchemyError
from sqlalchemy.orm import Session
from superset.migrations.shared.security_converge import (
add_pvms,
get_reversed_new_pvms,
get_reversed_pvm_map,
migrate_roles,
Pvm,
)
# revision identifiers, used by Alembic.
revision = "f6196627326f"
down_revision = "143b6f2815da"
NEW_PVMS = {"Chart": ("can_read",)}
PVM_MAP = {
Pvm("Chart", "can_get_data"): (Pvm("Chart", "can_read"),),
Pvm("Chart", "can_post_data"): (Pvm("Chart", "can_read"),),
}
def upgrade():
bind = op.get_bind()
session = Session(bind=bind)
# Add the new permissions on the migration itself
add_pvms(session, NEW_PVMS)
migrate_roles(session, PVM_MAP)
try:
session.commit()
except SQLAlchemyError as ex:
print(f"An error occurred while upgrading permissions: {ex}")
session.rollback()
def downgrade():
bind = op.get_bind()
session = Session(bind=bind)
# Add the old permissions on the migration itself
add_pvms(session, get_reversed_new_pvms(PVM_MAP))
migrate_roles(session, get_reversed_pvm_map(PVM_MAP))
try:
session.commit()
except SQLAlchemyError as ex:
print(f"An error occurred while downgrading permissions: {ex}")
session.rollback()

View File

@@ -23,7 +23,7 @@ import sqlalchemy as sqla
from flask_appbuilder import Model
from flask_appbuilder.models.decorators import renders
from markupsafe import escape, Markup
from sqlalchemy import Column, ForeignKey, Integer, String, Table, Text
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Table, Text
from sqlalchemy.engine.base import Connection
from sqlalchemy.orm import relationship
from sqlalchemy.orm.mapper import Mapper
@@ -71,6 +71,13 @@ class Slice( # pylint: disable=too-many-instance-attributes,too-many-public-met
cache_timeout = Column(Integer)
perm = Column(String(1000))
schema_perm = Column(String(1000))
# the last time a user has saved the chart, changed_on is referencing
# when the database row was last written
last_saved_at = Column(DateTime, nullable=True)
last_saved_by_fk = Column(Integer, ForeignKey("ab_user.id"), nullable=True,)
last_saved_by = relationship(
security_manager.user_model, foreign_keys=[last_saved_by_fk]
)
owners = relationship(security_manager.user_model, secondary=slice_user)
table = relationship(
"SqlaTable",

View File

@@ -42,7 +42,7 @@ DbapiDescriptionRow = Tuple[
]
DbapiDescription = Union[List[DbapiDescriptionRow], Tuple[DbapiDescriptionRow, ...]]
DbapiResult = Sequence[Union[List[Any], Tuple[Any, ...]]]
FilterValue = Union[datetime, float, int, str]
FilterValue = Union[bool, datetime, float, int, str]
FilterValues = Union[FilterValue, List[FilterValue], Tuple[FilterValue]]
FormData = Dict[str, Any]
Granularity = Union[str, Dict[str, Union[str, float]]]

View File

@@ -96,7 +96,7 @@ from superset.exceptions import (
SupersetException,
SupersetTimeoutException,
)
from superset.typing import AdhocMetric, FlaskResponse, FormData, Metric
from superset.typing import AdhocMetric, FilterValues, FlaskResponse, FormData, Metric
from superset.utils.dates import datetime_to_epoch, EPOCH
from superset.utils.hashing import md5_sha_from_dict, md5_sha_from_str
@@ -189,6 +189,25 @@ class DatasourceDict(TypedDict):
id: int
class AdhocFilterClause(TypedDict, total=False):
clause: str
expressionType: str
filterOptionName: Optional[str]
comparator: Optional[FilterValues]
operator: str
subject: str
isExtra: Optional[bool]
sqlExpression: Optional[str]
class QueryObjectFilterClause(TypedDict, total=False):
col: str
op: str # pylint: disable=invalid-name
val: Optional[FilterValues]
grain: Optional[str]
isExtra: Optional[bool]
class ExtraFiltersTimeColumnType(str, Enum):
GRANULARITY = "__granularity"
TIME_COL = "__time_col"
@@ -423,6 +442,35 @@ def cast_to_num(value: Optional[Union[float, int, str]]) -> Optional[Union[float
return None
def cast_to_boolean(value: Any) -> bool:
"""Casts a value to an int/float
>>> cast_to_boolean(1)
True
>>> cast_to_boolean(0)
False
>>> cast_to_boolean(0.5)
True
>>> cast_to_boolean('true')
True
>>> cast_to_boolean('false')
False
>>> cast_to_boolean('False')
False
>>> cast_to_boolean(None)
False
:param value: value to be converted to boolean representation
:returns: value cast to `bool`. when value is 'true' or value that are not 0
converte into True
"""
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):
return value.strip().lower() == "true"
return False
def list_minus(l: List[Any], minus: List[Any]) -> List[Any]:
"""Returns l without what is in minus
@@ -988,28 +1036,32 @@ def zlib_decompress(blob: bytes, decode: Optional[bool] = True) -> Union[bytes,
return decompressed.decode("utf-8") if decode else decompressed
def to_adhoc(
filt: Dict[str, Any], expression_type: str = "SIMPLE", clause: str = "where"
) -> Dict[str, Any]:
result = {
def simple_filter_to_adhoc(
filter_clause: QueryObjectFilterClause, clause: str = "where",
) -> AdhocFilterClause:
result: AdhocFilterClause = {
"clause": clause.upper(),
"expressionType": expression_type,
"isExtra": bool(filt.get("isExtra")),
"expressionType": "SIMPLE",
"comparator": filter_clause.get("val"),
"operator": filter_clause["op"],
"subject": filter_clause["col"],
}
if filter_clause.get("isExtra"):
result["isExtra"] = True
result["filterOptionName"] = md5_sha_from_dict(cast(Dict[Any, Any], result))
if expression_type == "SIMPLE":
result.update(
{
"comparator": filt.get("val"),
"operator": filt.get("op"),
"subject": filt.get("col"),
}
)
elif expression_type == "SQL":
result.update({"sqlExpression": filt.get(clause)})
return result
deterministic_name = md5_sha_from_dict(result)
result["filterOptionName"] = deterministic_name
def form_data_to_adhoc(form_data: Dict[str, Any], clause: str) -> AdhocFilterClause:
if clause not in ("where", "having"):
raise ValueError(__("Unsupported clause type: %(clause)s", clause=clause))
result: AdhocFilterClause = {
"clause": clause.upper(),
"expressionType": "SQL",
"sqlExpression": form_data.get(clause),
}
result["filterOptionName"] = md5_sha_from_dict(cast(Dict[Any, Any], result))
return result
@@ -1021,7 +1073,7 @@ def merge_extra_form_data(form_data: Dict[str, Any]) -> None:
"""
filter_keys = ["filters", "adhoc_filters"]
extra_form_data = form_data.pop("extra_form_data", {})
append_filters = extra_form_data.get("filters", None)
append_filters: List[QueryObjectFilterClause] = extra_form_data.get("filters", None)
# merge append extras
for key in [key for key in EXTRA_FORM_DATA_APPEND_KEYS if key not in filter_keys]:
@@ -1046,13 +1098,21 @@ def merge_extra_form_data(form_data: Dict[str, Any]) -> None:
if extras:
form_data["extras"] = extras
adhoc_filters = form_data.get("adhoc_filters", [])
adhoc_filters: List[AdhocFilterClause] = form_data.get("adhoc_filters", [])
form_data["adhoc_filters"] = adhoc_filters
append_adhoc_filters = extra_form_data.get("adhoc_filters", [])
adhoc_filters.extend({"isExtra": True, **fltr} for fltr in append_adhoc_filters)
append_adhoc_filters: List[AdhocFilterClause] = extra_form_data.get(
"adhoc_filters", []
)
adhoc_filters.extend(
{"isExtra": True, **fltr} for fltr in append_adhoc_filters # type: ignore
)
if append_filters:
adhoc_filters.extend(
to_adhoc({"isExtra": True, **fltr}) for fltr in append_filters if fltr
simple_filter_to_adhoc(
{"isExtra": True, **fltr} # type: ignore
)
for fltr in append_filters
if fltr
)
@@ -1119,16 +1179,16 @@ def merge_extra_filters( # pylint: disable=too-many-branches
# Add filters for unequal lists
# order doesn't matter
if set(existing_filters[filter_key]) != set(filtr["val"]):
adhoc_filters.append(to_adhoc(filtr))
adhoc_filters.append(simple_filter_to_adhoc(filtr))
else:
adhoc_filters.append(to_adhoc(filtr))
adhoc_filters.append(simple_filter_to_adhoc(filtr))
else:
# Do not add filter if same value already exists
if filtr["val"] != existing_filters[filter_key]:
adhoc_filters.append(to_adhoc(filtr))
adhoc_filters.append(simple_filter_to_adhoc(filtr))
else:
# Filter not found, add it
adhoc_filters.append(to_adhoc(filtr))
adhoc_filters.append(simple_filter_to_adhoc(filtr))
# Remove extra filters from the form data since no longer needed
del form_data["extra_filters"]
@@ -1239,15 +1299,16 @@ def convert_legacy_filters_into_adhoc( # pylint: disable=invalid-name
mapping = {"having": "having_filters", "where": "filters"}
if not form_data.get("adhoc_filters"):
form_data["adhoc_filters"] = []
adhoc_filters: List[AdhocFilterClause] = []
form_data["adhoc_filters"] = adhoc_filters
for clause, filters in mapping.items():
if clause in form_data and form_data[clause] != "":
form_data["adhoc_filters"].append(to_adhoc(form_data, "SQL", clause))
adhoc_filters.append(form_data_to_adhoc(form_data, clause))
if filters in form_data:
for filt in filter(lambda x: x is not None, form_data[filters]):
form_data["adhoc_filters"].append(to_adhoc(filt, "SIMPLE", clause))
adhoc_filters.append(simple_filter_to_adhoc(filt, clause))
for key in ("filters", "having", "having_filters", "where"):
if key in form_data:

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