mirror of
https://github.com/apache/superset.git
synced 2026-05-04 23:44:23 +00:00
Compare commits
43 Commits
upgrade-sq
...
v2021.31.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de615eb79e | ||
|
|
9fb638da6f | ||
|
|
60ceb9213f | ||
|
|
7458292ca9 | ||
|
|
adb3ebbba3 | ||
|
|
c5f07bc6c1 | ||
|
|
81d2d32dbf | ||
|
|
cce369ee00 | ||
|
|
d95721cb14 | ||
|
|
0a91bc8c3f | ||
|
|
13f01ac2ab | ||
|
|
705bad9792 | ||
|
|
86d079b31c | ||
|
|
b4d4d1cc88 | ||
|
|
7c5546586c | ||
|
|
5895af50f5 | ||
|
|
baeac9dc97 | ||
|
|
9e21009db3 | ||
|
|
79e4253230 | ||
|
|
520284a4a4 | ||
|
|
bb78d492e9 | ||
|
|
1e9f1de563 | ||
|
|
d00a2c2899 | ||
|
|
0b07566346 | ||
|
|
c0572c5302 | ||
|
|
4b4f6b9c1d | ||
|
|
ffa4226f10 | ||
|
|
64d54d6fdd | ||
|
|
fa7a5249ea | ||
|
|
f8f3b7abbb | ||
|
|
83661aee99 | ||
|
|
9976250954 | ||
|
|
08e64048fa | ||
|
|
18f551580e | ||
|
|
d85875d962 | ||
|
|
31f994b090 | ||
|
|
a16605cf01 | ||
|
|
e70bcc7283 | ||
|
|
980c38d1b7 | ||
|
|
87fad13f89 | ||
|
|
a1a71ee6e1 | ||
|
|
feab690b8d | ||
|
|
b7d9be449d |
@@ -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
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
663
superset-frontend/package-lock.json
generated
663
superset-frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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",
|
||||
|
||||
@@ -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: [
|
||||
|
||||
@@ -50,7 +50,7 @@ describe('VizTypeControl', () => {
|
||||
new ChartMetadata({
|
||||
name: 'vis1',
|
||||
thumbnail: '',
|
||||
tags: ['Highly-used'],
|
||||
tags: ['Popular'],
|
||||
}),
|
||||
)
|
||||
.registerValue(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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' && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
`;
|
||||
|
||||
@@ -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('');
|
||||
};
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = '≥',
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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} />;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -32,7 +32,7 @@ export enum TableTabTypes {
|
||||
export type Filters = {
|
||||
col: string;
|
||||
opr: string;
|
||||
value: string;
|
||||
value: string | number;
|
||||
};
|
||||
|
||||
export interface DashboardTableProps {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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' }),
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"importHelpers": false,
|
||||
"jsx": "preserve",
|
||||
"lib": ["dom", "esnext"],
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"noImplicitAny": true,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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, ...]:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
@@ -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
|
||||
@@ -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 ###
|
||||
@@ -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()
|
||||
@@ -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",
|
||||
|
||||
@@ -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]]]
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user