diff --git a/.github/workflows/superset-python-integrationtest.yml b/.github/workflows/superset-python-integrationtest.yml index f2caf2de389ad..655b5c85d5944 100644 --- a/.github/workflows/superset-python-integrationtest.yml +++ b/.github/workflows/superset-python-integrationtest.yml @@ -150,7 +150,9 @@ jobs: SUPERSET_CONFIG: tests.integration_tests.superset_test_config REDIS_PORT: 16379 SUPERSET__SQLALCHEMY_DATABASE_URI: | - sqlite:///${{ github.workspace }}/.temp/unittest.db + sqlite:///${{ github.workspace }}/.temp/superset.db?check_same_thread=true + SUPERSET__SQLALCHEMY_EXAMPLES_URI: | + sqlite:///${{ github.workspace }}/.temp/examples.db?check_same_thread=true services: redis: image: redis:7-alpine diff --git a/docs/docs/frequently-asked-questions.mdx b/docs/docs/frequently-asked-questions.mdx index 8c9fa034cfdd7..882573515e18a 100644 --- a/docs/docs/frequently-asked-questions.mdx +++ b/docs/docs/frequently-asked-questions.mdx @@ -175,10 +175,6 @@ non-OLTP databases are not designed for this type of workload. You can take a look at this Flask-AppBuilder [configuration example](https://github.com/dpgaspar/Flask-AppBuilder/blob/master/examples/oauth/config.py). -### How can I set a default filter on my dashboard? - -Simply apply the filter and save the dashboard while the filter is active. - ### Is there a way to force the dashboard to use specific colors? It is possible on a per-dashboard basis by providing a mapping of labels to colors in the JSON diff --git a/setup.py b/setup.py index 18fb4dd1e68e5..563cd32342408 100644 --- a/setup.py +++ b/setup.py @@ -155,7 +155,7 @@ def get_git_sha() -> str: "dremio": ["sqlalchemy-dremio>=1.1.5, <1.3"], "drill": ["sqlalchemy-drill==0.1.dev"], "druid": ["pydruid>=0.6.5,<0.7"], - "duckdb": ["duckdb-engine==0.9.2"], + "duckdb": ["duckdb-engine>=0.9.5, <0.10"], "dynamodb": ["pydynamodb>=0.4.2"], "solr": ["sqlalchemy-solr >= 0.2.0"], "elasticsearch": ["elasticsearch-dbapi>=0.2.9, <0.3.0"], diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts index ae6dd8f894d8f..1aefc25464bb5 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/constants.ts @@ -17,12 +17,11 @@ * under the License. */ import { - t, - QueryMode, DTTM_ALIAS, GenericDataType, QueryColumn, - DatasourceType, + QueryMode, + t, } from '@superset-ui/core'; import { ColumnMeta, SortSeriesData, SortSeriesType } from './types'; @@ -43,6 +42,7 @@ export const COLUMN_NAME_ALIASES: Record = { export const DATASET_TIME_COLUMN_OPTION: ColumnMeta = { verbose_name: COLUMN_NAME_ALIASES[DTTM_ALIAS], column_name: DTTM_ALIAS, + type: 'TIMESTAMP', type_generic: GenericDataType.TEMPORAL, description: t( 'A reference to the [Time] configuration, taking granularity into account', @@ -51,8 +51,9 @@ export const DATASET_TIME_COLUMN_OPTION: ColumnMeta = { export const QUERY_TIME_COLUMN_OPTION: QueryColumn = { column_name: DTTM_ALIAS, - type: DatasourceType.Query, - is_dttm: false, + is_dttm: true, + type: 'TIMESTAMP', + type_generic: GenericDataType.TEMPORAL, }; export const QueryModeLabel = { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts index cc0b678f6db6c..b12d5be048e8a 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/fixtures.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { DatasourceType } from '@superset-ui/core'; +import { DatasourceType, GenericDataType } from '@superset-ui/core'; import { Dataset } from './types'; export const TestDataset: Dataset = { @@ -37,7 +37,7 @@ export const TestDataset: Dataset = { is_dttm: false, python_date_format: null, type: 'BIGINT', - type_generic: 0, + type_generic: GenericDataType.NUMERIC, verbose_name: null, warning_markdown: null, }, @@ -55,7 +55,7 @@ export const TestDataset: Dataset = { is_dttm: false, python_date_format: null, type: 'VARCHAR(16)', - type_generic: 1, + type_generic: GenericDataType.STRING, verbose_name: '', warning_markdown: null, }, @@ -73,7 +73,7 @@ export const TestDataset: Dataset = { is_dttm: false, python_date_format: null, type: 'VARCHAR(10)', - type_generic: 1, + type_generic: GenericDataType.STRING, verbose_name: null, warning_markdown: null, }, @@ -91,7 +91,7 @@ export const TestDataset: Dataset = { is_dttm: true, python_date_format: null, type: 'TIMESTAMP WITHOUT TIME ZONE', - type_generic: 2, + type_generic: GenericDataType.TEMPORAL, verbose_name: null, warning_markdown: null, }, @@ -109,7 +109,7 @@ export const TestDataset: Dataset = { is_dttm: false, python_date_format: null, type: 'VARCHAR(255)', - type_generic: 1, + type_generic: GenericDataType.STRING, verbose_name: null, warning_markdown: null, }, diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx index 53c9aa7447f31..596c6f0e55cab 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/sections/echartsTimeSeriesQuery.tsx @@ -20,6 +20,7 @@ import { hasGenericChartAxes, t } from '@superset-ui/core'; import { ControlPanelSectionConfig, ControlSetRow } from '../types'; import { contributionModeControl, + xAxisForceCategoricalControl, xAxisSortAscControl, xAxisSortControl, xAxisSortSeriesAscendingControl, @@ -55,6 +56,7 @@ export const echartsTimeSeriesQueryWithXAxisSort: ControlPanelSectionConfig = { controlSetRows: [ [hasGenericChartAxes ? 'x_axis' : null], [hasGenericChartAxes ? 'time_grain_sqla' : null], + [hasGenericChartAxes ? xAxisForceCategoricalControl : null], [hasGenericChartAxes ? xAxisSortControl : null], [hasGenericChartAxes ? xAxisSortAscControl : null], [hasGenericChartAxes ? xAxisSortSeriesControl : null], diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx index 82ba6dfeebe45..b0f3006353d64 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/src/shared-controls/customControls.tsx @@ -20,9 +20,9 @@ import { ContributionType, ensureIsArray, + GenericDataType, getColumnLabel, getMetricLabel, - isDefined, QueryFormColumn, QueryFormMetric, t, @@ -38,6 +38,7 @@ import { DEFAULT_XAXIS_SORT_SERIES_DATA, SORT_SERIES_CHOICES, } from '../constants'; +import { checkColumnType } from '../utils/checkColumnType'; export const contributionModeControl = { name: 'contributionMode', @@ -54,18 +55,29 @@ export const contributionModeControl = { }, }; -function isTemporal(controls: ControlStateMapping): boolean { - return !( - isDefined(controls?.x_axis?.value) && - !isTemporalColumn( +function isForcedCategorical(controls: ControlStateMapping): boolean { + return ( + checkColumnType( getColumnLabel(controls?.x_axis?.value as QueryFormColumn), controls?.datasource?.datasource, + [GenericDataType.NUMERIC], + ) && !!controls?.xAxisForceCategorical?.value + ); +} + +function isSortable(controls: ControlStateMapping): boolean { + return ( + isForcedCategorical(controls) || + checkColumnType( + getColumnLabel(controls?.x_axis?.value as QueryFormColumn), + controls?.datasource?.datasource, + [GenericDataType.STRING, GenericDataType.BOOLEAN], ) ); } const xAxisSortVisibility = ({ controls }: { controls: ControlStateMapping }) => - !isTemporal(controls) && + isSortable(controls) && ensureIsArray(controls?.groupby?.value).length === 0 && ensureIsArray(controls?.metrics?.value).length === 1; @@ -74,7 +86,7 @@ const xAxisMultiSortVisibility = ({ }: { controls: ControlStateMapping; }) => - !isTemporal(controls) && + isSortable(controls) && (!!ensureIsArray(controls?.groupby?.value).length || ensureIsArray(controls?.metrics?.value).length > 1); @@ -141,7 +153,29 @@ export const xAxisSortAscControl = { : t('X-Axis Sort Ascending'), default: true, description: t('Whether to sort ascending or descending on the base Axis.'), - visibility: xAxisSortVisibility, + visibility: ({ controls }: { controls: ControlStateMapping }) => + controls?.x_axis_sort?.value !== undefined && + xAxisSortVisibility({ controls }), + }, +}; + +export const xAxisForceCategoricalControl = { + name: 'xAxisForceCategorical', + config: { + type: 'CheckboxControl', + label: () => t('Force categorical'), + default: false, + description: t('Treat values as categorical.'), + initialValue: (control: ControlState, state: ControlPanelState | null) => + state?.form_data?.x_axis_sort !== undefined || control.value, + renderTrigger: true, + visibility: ({ controls }: { controls: ControlStateMapping }) => + checkColumnType( + getColumnLabel(controls?.x_axis?.value as QueryFormColumn), + controls?.datasource?.datasource, + [GenericDataType.NUMERIC], + ), + shouldMapStateToProps: () => true, }, }; @@ -173,6 +207,8 @@ export const xAxisSortSeriesAscendingControl = { default: DEFAULT_XAXIS_SORT_SERIES_DATA.sort_series_ascending, description: t('Whether to sort ascending or descending on the base Axis.'), renderTrigger: true, - visibility: xAxisMultiSortVisibility, + visibility: ({ controls }: { controls: ControlStateMapping }) => + controls?.x_axis_sort_series?.value !== undefined && + xAxisMultiSortVisibility({ controls }), }, }; diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts index 09e4f63ee3d37..9314f8d33f3b9 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/types.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/types.ts @@ -479,13 +479,15 @@ export function isControlPanelSectionConfig( export function isDataset( datasource: Dataset | QueryResponse | null | undefined, ): datasource is Dataset { - return !!datasource && 'columns' in datasource; + return ( + !!datasource && 'columns' in datasource && !('sqlEditorId' in datasource) + ); } export function isQueryResponse( datasource: Dataset | QueryResponse | null | undefined, ): datasource is QueryResponse { - return !!datasource && 'results' in datasource && 'sql' in datasource; + return !!datasource && 'results' in datasource && 'sqlEditorId' in datasource; } export enum SortSeriesType { diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/checkColumnType.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/checkColumnType.ts new file mode 100644 index 0000000000000..202b9605d545d --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/checkColumnType.ts @@ -0,0 +1,49 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { ensureIsArray, GenericDataType, ValueOf } from '@superset-ui/core'; +import { + ControlPanelState, + isDataset, + isQueryResponse, +} from '@superset-ui/chart-controls'; + +export function checkColumnType( + columnName: string, + datasource: ValueOf>, + columnTypes: GenericDataType[], +): boolean { + if (isDataset(datasource)) { + return ensureIsArray(datasource.columns).some( + c => + c.type_generic !== undefined && + columnTypes.includes(c.type_generic) && + columnName === c.column_name, + ); + } + if (isQueryResponse(datasource)) { + return ensureIsArray(datasource.columns) + .filter( + c => + c.type_generic !== undefined && columnTypes.includes(c.type_generic), + ) + .map(c => c.column_name) + .some(c => columnName === c); + } + return false; +} diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts index c76cd79031c23..f0561517502c5 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/columnChoices.ts @@ -17,7 +17,7 @@ * under the License. */ import { QueryResponse } from '@superset-ui/core'; -import { Dataset, isColumnMeta, isDataset } from '../types'; +import { Dataset, isDataset, isQueryResponse } from '../types'; /** * Convert Datasource columns to column choices @@ -25,11 +25,13 @@ import { Dataset, isColumnMeta, isDataset } from '../types'; export default function columnChoices( datasource?: Dataset | QueryResponse | null, ): [string, string][] { - if (isDataset(datasource) && isColumnMeta(datasource.columns[0])) { + if (isDataset(datasource) || isQueryResponse(datasource)) { return datasource.columns .map((col): [string, string] => [ col.column_name, - col.verbose_name || col.column_name, + 'verbose_name' in col + ? col.verbose_name || col.column_name + : col.column_name, ]) .sort((opt1, opt2) => opt1[1].toLowerCase() > opt2[1].toLowerCase() ? 1 : -1, diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts index 4fa4243c1e850..208d708a96854 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/index.ts @@ -16,6 +16,7 @@ * specific language governing permissions and limitations * under the License. */ +export * from './checkColumnType'; export * from './selectOptions'; export * from './D3Formatting'; export * from './expandControlConfig'; diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/checkColumnType.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/checkColumnType.test.ts new file mode 100644 index 0000000000000..44ebbf605cfaa --- /dev/null +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/checkColumnType.test.ts @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { GenericDataType, testQueryResponse } from '@superset-ui/core'; +import { checkColumnType, TestDataset } from '../../src'; + +test('checkColumnType columns from a Dataset', () => { + expect( + checkColumnType('num', TestDataset, [GenericDataType.NUMERIC]), + ).toEqual(true); + expect(checkColumnType('num', TestDataset, [GenericDataType.STRING])).toEqual( + false, + ); + expect( + checkColumnType('gender', TestDataset, [GenericDataType.STRING]), + ).toEqual(true); + expect( + checkColumnType('gender', TestDataset, [GenericDataType.NUMERIC]), + ).toEqual(false); +}); + +test('checkColumnType from a QueryResponse', () => { + expect( + checkColumnType('Column 1', testQueryResponse, [GenericDataType.STRING]), + ).toEqual(true); + expect( + checkColumnType('Column 1', testQueryResponse, [GenericDataType.NUMERIC]), + ).toEqual(false); +}); + +test('checkColumnType from null', () => { + expect(checkColumnType('col', null, [])).toEqual(false); +}); diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx index 70018ddc67fa1..de5bb1ab6980d 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/columnChoices.test.tsx @@ -16,7 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { DatasourceType, testQueryResponse } from '@superset-ui/core'; +import { + DatasourceType, + GenericDataType, + testQueryResponse, +} from '@superset-ui/core'; import { columnChoices } from '../../src'; describe('columnChoices()', () => { @@ -31,14 +35,20 @@ describe('columnChoices()', () => { columns: [ { column_name: 'fiz', + type: 'INT', + type_generic: GenericDataType.NUMERIC, }, { column_name: 'about', verbose_name: 'right', + type: 'VARCHAR', + type_generic: GenericDataType.STRING, }, { column_name: 'foo', - verbose_name: 'bar', + verbose_name: undefined, + type: 'TIMESTAMP', + type_generic: GenericDataType.TEMPORAL, }, ], verbose_map: {}, @@ -48,8 +58,8 @@ describe('columnChoices()', () => { description: 'this is my datasource', }), ).toEqual([ - ['foo', 'bar'], ['fiz', 'fiz'], + ['foo', 'foo'], ['about', 'right'], ]); }); diff --git a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getTemporalColumns.test.ts b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getTemporalColumns.test.ts index 7227173045b52..f0a6529de7122 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/test/utils/getTemporalColumns.test.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/test/utils/getTemporalColumns.test.ts @@ -16,7 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -import { testQueryResponse, testQueryResults } from '@superset-ui/core'; +import { + GenericDataType, + testQueryResponse, + testQueryResults, +} from '@superset-ui/core'; import { Dataset, getTemporalColumns, @@ -55,8 +59,9 @@ test('get temporal columns from a QueryResponse', () => { temporalColumns: [ { column_name: 'Column 2', - type: 'TIMESTAMP', is_dttm: true, + type: 'TIMESTAMP', + type_generic: GenericDataType.TEMPORAL, }, ], defaultTemporalColumn: 'Column 2', diff --git a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts index f42b01abdcb7f..1271be2fff89f 100644 --- a/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts +++ b/superset-frontend/packages/superset-ui-core/src/query/types/Query.ts @@ -31,6 +31,7 @@ import { Maybe } from '../../types'; import { PostProcessingRule } from './PostProcessing'; import { JsonObject } from '../../connection'; import { TimeGranularity } from '../../time-format'; +import { GenericDataType } from './QueryResponse'; export type BaseQueryObjectFilterClause = { col: QueryFormColumn; @@ -250,6 +251,7 @@ export type QueryColumn = { name?: string; column_name: string; type: string | null; + type_generic: GenericDataType; is_dttm: boolean; }; @@ -383,16 +385,19 @@ export const testQuery: Query = { column_name: 'Column 1', type: 'STRING', is_dttm: false, + type_generic: GenericDataType.STRING, }, { column_name: 'Column 3', type: 'STRING', is_dttm: false, + type_generic: GenericDataType.STRING, }, { column_name: 'Column 2', type: 'TIMESTAMP', is_dttm: true, + type_generic: GenericDataType.TEMPORAL, }, ], }; @@ -404,16 +409,19 @@ export const testQueryResults = { { column_name: 'Column 1', type: 'STRING', + type_generic: GenericDataType.STRING, is_dttm: false, }, { column_name: 'Column 3', type: 'STRING', + type_generic: GenericDataType.STRING, is_dttm: false, }, { column_name: 'Column 2', type: 'TIMESTAMP', + type_generic: GenericDataType.TEMPORAL, is_dttm: true, }, ], @@ -425,16 +433,19 @@ export const testQueryResults = { { column_name: 'Column 1', type: 'STRING', + type_generic: GenericDataType.STRING, is_dttm: false, }, { column_name: 'Column 3', type: 'STRING', + type_generic: GenericDataType.STRING, is_dttm: false, }, { column_name: 'Column 2', type: 'TIMESTAMP', + type_generic: GenericDataType.TEMPORAL, is_dttm: true, }, ], diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx index 76c1465357b73..3908ea4516d9b 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/controlPanel.tsx @@ -124,6 +124,10 @@ const config: ControlPanelConfig = { EchartsFunnelLabelTypeType.KeyValuePercent, t('Category, Value and Percentage'), ], + [ + EchartsFunnelLabelTypeType.ValuePercent, + t('Value and Percentage'), + ], ], description: t('What should be shown as the label'), }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts index a8d8c9e65cc3d..df43fe0a83d73 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/transformProps.ts @@ -94,6 +94,8 @@ export function formatFunnelLabel({ return `${name}: ${formattedValue} (${formattedPercent})`; case EchartsFunnelLabelTypeType.KeyPercent: return `${name}: ${formattedPercent}`; + case EchartsFunnelLabelTypeType.ValuePercent: + return `${formattedValue} (${formattedPercent})`; default: return name; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts index 928664e223c91..51595622345e7 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Funnel/types.ts @@ -52,6 +52,7 @@ export enum EchartsFunnelLabelTypeType { KeyValue, KeyPercent, KeyValuePercent, + ValuePercent, } export interface EchartsFunnelChartProps diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts index 6c96ab7104a6e..1d4eceb33f3b6 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/transformProps.ts @@ -189,6 +189,7 @@ export default function transformProps( groupby, groupbyB, xAxis: xAxisOrig, + xAxisForceCategorical, xAxisTitle, yAxisTitle, xAxisTitleMargin, @@ -227,7 +228,7 @@ export default function transformProps( const dataTypes = getColtypesMapping(queriesData[0]); const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; - const xAxisType = getAxisType(stack, xAxisDataType); + const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType); const series: SeriesOption[] = []; const formatter = contributionMode ? getNumberFormatter(',.0%') diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts index c027ed7ac4ffc..e79523d176d02 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/MixedTimeseries/types.ts @@ -106,6 +106,7 @@ export const DEFAULT_FORM_DATA: EchartsMixedTimeseriesFormData = { yAxisTitleSecondary: DEFAULT_TITLE_FORM_DATA.yAxisTitle, tooltipTimeFormat: TIMESERIES_DEFAULTS.tooltipTimeFormat, xAxisBounds: TIMESERIES_DEFAULTS.xAxisBounds, + xAxisForceCategorical: TIMESERIES_DEFAULTS.xAxisForceCategorical, xAxisTimeFormat: TIMESERIES_DEFAULTS.xAxisTimeFormat, area: TIMESERIES_DEFAULTS.area, areaB: TIMESERIES_DEFAULTS.area, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx index 53d406538d622..8a7612c20f973 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/controlPanel.tsx @@ -107,6 +107,7 @@ const config: ControlPanelConfig = { ['key_value', t('Category and Value')], ['key_percent', t('Category and Percentage')], ['key_value_percent', t('Category, Value and Percentage')], + ['value_percent', t('Value and Percentage')], ], description: t('What should be shown on the label?'), }, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts index 4ad92b0bb2e0d..031b072b449de 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/transformProps.ts @@ -80,6 +80,8 @@ export function formatPieLabel({ return `${name}: ${formattedValue} (${formattedPercent})`; case EchartsPieLabelType.KeyPercent: return `${name}: ${formattedPercent}`; + case EchartsPieLabelType.ValuePercent: + return `${formattedValue} (${formattedPercent})`; default: return name; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts index d4acbb9517107..631c1c7de3568 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Pie/types.ts @@ -54,6 +54,7 @@ export enum EchartsPieLabelType { KeyValue = 'key_value', KeyPercent = 'key_percent', KeyValuePercent = 'key_value_percent', + ValuePercent = 'value_percent', } export interface EchartsPieChartProps diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts index 215996ab122a5..839bc607f77a5 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/constants.ts @@ -63,6 +63,7 @@ export const DEFAULT_FORM_DATA: EchartsTimeseriesFormData = { yAxisBounds: [null, null], zoomable: false, richTooltip: true, + xAxisForceCategorical: false, xAxisLabelRotation: defaultXAxis.xAxisLabelRotation, groupby: [], showValue: false, diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts index bbb624c0e0a1f..3bbe285aeca54 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/transformProps.ts @@ -167,6 +167,7 @@ export default function transformProps( truncateYAxis, xAxis: xAxisOrig, xAxisBounds, + xAxisForceCategorical, xAxisLabelRotation, xAxisSortSeries, xAxisSortSeriesAscending, @@ -248,7 +249,7 @@ export default function transformProps( const isAreaExpand = stack === StackControlsValue.Expand; const xAxisDataType = dataTypes?.[xAxisLabel] ?? dataTypes?.[xAxisOrig]; - const xAxisType = getAxisType(stack, xAxisDataType); + const xAxisType = getAxisType(stack, xAxisForceCategorical, xAxisDataType); const series: SeriesOption[] = []; const forcePercentFormatter = Boolean(contributionMode || isAreaExpand); diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts index 751f262ec1757..692abb2c79577 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/Timeseries/types.ts @@ -79,6 +79,7 @@ export type EchartsTimeseriesFormData = QueryFormData & { truncateXAxis: boolean; truncateYAxis: boolean; yAxisFormat?: string; + xAxisForceCategorical?: boolean; xAxisTimeFormat?: string; timeGrainSqla?: TimeGranularity; xAxisBounds: [number | undefined | null, number | undefined | null]; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx index 21f8c7d97dbd8..c91d27acc6ab1 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx +++ b/superset-frontend/plugins/plugin-chart-echarts/src/controls.tsx @@ -309,3 +309,14 @@ export const minorTicks: ControlSetItem = { description: t('Show minor ticks on axes.'), }, }; + +export const forceCategorical: ControlSetItem = { + name: 'forceCategorical', + config: { + type: 'CheckboxControl', + label: t('Force categorical'), + default: false, + renderTrigger: true, + description: t('Make the x-axis categorical'), + }, +}; diff --git a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts index a294fa44c35c2..6a51e7cbf7c5c 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/src/utils/series.ts @@ -229,8 +229,10 @@ export function sortRows( } const value = - xAxisSortSeries === SortSeriesType.Name && typeof sortKey === 'string' - ? sortKey.toLowerCase() + xAxisSortSeries === SortSeriesType.Name + ? typeof sortKey === 'string' + ? sortKey.toLowerCase() + : sortKey : aggregate; return { @@ -515,8 +517,12 @@ export function sanitizeHtml(text: string): string { export function getAxisType( stack: StackType, + forceCategorical?: boolean, dataType?: GenericDataType, ): AxisType { + if (forceCategorical) { + return AxisType.category; + } if (dataType === GenericDataType.TEMPORAL) { return AxisType.time; } diff --git a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts index 8e55b125b8747..3d7d21c8d0b02 100644 --- a/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts +++ b/superset-frontend/plugins/plugin-chart-echarts/test/utils/series.test.ts @@ -878,14 +878,28 @@ test('calculateLowerLogTick', () => { expect(calculateLowerLogTick(0.005)).toEqual(0.001); }); -test('getAxisType', () => { - expect(getAxisType(false, GenericDataType.TEMPORAL)).toEqual(AxisType.time); - expect(getAxisType(false, GenericDataType.NUMERIC)).toEqual(AxisType.value); - expect(getAxisType(true, GenericDataType.NUMERIC)).toEqual(AxisType.category); - expect(getAxisType(false, GenericDataType.BOOLEAN)).toEqual( +test('getAxisType without forced categorical', () => { + expect(getAxisType(false, false, GenericDataType.TEMPORAL)).toEqual( + AxisType.time, + ); + expect(getAxisType(false, false, GenericDataType.NUMERIC)).toEqual( + AxisType.value, + ); + expect(getAxisType(true, false, GenericDataType.NUMERIC)).toEqual( + AxisType.category, + ); + expect(getAxisType(false, false, GenericDataType.BOOLEAN)).toEqual( + AxisType.category, + ); + expect(getAxisType(false, false, GenericDataType.STRING)).toEqual( + AxisType.category, + ); +}); + +test('getAxisType with forced categorical', () => { + expect(getAxisType(false, true, GenericDataType.NUMERIC)).toEqual( AxisType.category, ); - expect(getAxisType(false, GenericDataType.STRING)).toEqual(AxisType.category); }); test('getMinAndMaxFromBounds returns empty object when not truncating', () => { diff --git a/superset-frontend/src/SqlLab/fixtures.ts b/superset-frontend/src/SqlLab/fixtures.ts index c578aac3fc010..7a08c51876ff2 100644 --- a/superset-frontend/src/SqlLab/fixtures.ts +++ b/superset-frontend/src/SqlLab/fixtures.ts @@ -22,6 +22,7 @@ import { ColumnKeyTypeType } from 'src/SqlLab/components/ColumnElement'; import { DatasourceType, denormalizeTimestamp, + GenericDataType, QueryResponse, QueryState, } from '@superset-ui/core'; @@ -581,11 +582,13 @@ const baseQuery: QueryResponse = { is_dttm: true, column_name: 'ds', type: 'STRING', + type_generic: GenericDataType.STRING, }, { is_dttm: false, column_name: 'gender', type: 'STRING', + type_generic: GenericDataType.STRING, }, ], selected_columns: [ @@ -593,11 +596,13 @@ const baseQuery: QueryResponse = { is_dttm: true, column_name: 'ds', type: 'STRING', + type_generic: GenericDataType.TEMPORAL, }, { is_dttm: false, column_name: 'gender', type: 'STRING', + type_generic: GenericDataType.STRING, }, ], expanded_columns: [ @@ -605,6 +610,7 @@ const baseQuery: QueryResponse = { is_dttm: true, column_name: 'ds', type: 'STRING', + type_generic: GenericDataType.STRING, }, ], data: [ diff --git a/superset-frontend/src/components/Dropdown/index.tsx b/superset-frontend/src/components/Dropdown/index.tsx index c40f479579d2e..1e2e03ceb24e8 100644 --- a/superset-frontend/src/components/Dropdown/index.tsx +++ b/superset-frontend/src/components/Dropdown/index.tsx @@ -104,12 +104,6 @@ interface ExtendedDropDownProps extends DropDownProps { ref?: RefObject; } -// @z-index-below-dashboard-header (100) - 1 = 99 export const NoAnimationDropdown = ( props: ExtendedDropDownProps & { children?: React.ReactNode }, -) => ( - -); +) => ; diff --git a/superset-frontend/src/components/DvtDropdown/dvt-dropdown.module.tsx b/superset-frontend/src/components/DvtDropdown/dvt-dropdown.module.tsx new file mode 100644 index 0000000000000..134d8cf0f34dd --- /dev/null +++ b/superset-frontend/src/components/DvtDropdown/dvt-dropdown.module.tsx @@ -0,0 +1,63 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { keyframes, styled } from '@superset-ui/core'; + +const optionsKeyframes = keyframes` + from { + transform: scaleY(0); + } + to { + transform: scaleY(1); + } +`; + +const StyledDropdown = styled.div` + position: absolute; + top: calc(100% + 2px); + left: 9px; +`; + +const DropdownMenu = styled.div` + display: flex; + flex-direction: column; + box-shadow: 0 0 8px ${({ theme }) => theme.colors.dvt.boxShadow.base}; + transform-origin: top; + animation: ${optionsKeyframes} 0.3s ease-in-out; + border-radius: 4px; + padding: 4px 0; +`; + +const DropdownOption = styled.div` + display: flex; + gap: 10px; + align-items: end; + padding: 5px 12px; + cursor: pointer; + + &:hover { + background-color: ${({ theme }) => theme.colors.dvt.grayscale.light1}; + } +`; + +const StyledDropdownGroup = styled.div` + position: relative; + display: inline-flex; +`; + +export { StyledDropdown, DropdownMenu, DropdownOption, StyledDropdownGroup }; diff --git a/superset-frontend/src/components/DvtDropdown/dvt-dropdown.stories.tsx b/superset-frontend/src/components/DvtDropdown/dvt-dropdown.stories.tsx new file mode 100644 index 0000000000000..8476247072232 --- /dev/null +++ b/superset-frontend/src/components/DvtDropdown/dvt-dropdown.stories.tsx @@ -0,0 +1,36 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import DvtDropdown, { DvtDropdownProps } from '.'; + +export default { + title: 'Dvt-Components/DvtDropdown', + component: DvtDropdown, +}; + +export const Default = (args: DvtDropdownProps) => ; + +Default.args = { + data: [ + { label: 'Edit', icon: 'edit_alt', onClick: () => {} }, + { label: 'Export', icon: 'share', onClick: () => {} }, + { label: 'Delete', icon: 'trash', onClick: () => {} }, + ], + icon: 'more_vert', +}; diff --git a/superset-frontend/src/components/DvtDropdown/index.tsx b/superset-frontend/src/components/DvtDropdown/index.tsx new file mode 100644 index 0000000000000..7b4ee615d70bf --- /dev/null +++ b/superset-frontend/src/components/DvtDropdown/index.tsx @@ -0,0 +1,65 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useRef, useState } from 'react'; +import useOnClickOutside from 'src/hooks/useOnClickOutsite'; +import Icon from '../Icons/Icon'; +import { + StyledDropdown, + DropdownMenu, + DropdownOption, + StyledDropdownGroup, +} from './dvt-dropdown.module'; + +interface OptionProps { + label: string; + icon: string; + onClick: () => void; +} + +export interface DvtDropdownProps { + isOpen: boolean; + setIsOpen: () => void; + data: OptionProps[]; + icon: string; +} + +const DvtDropdown: React.FC = ({ data, icon }) => { + const [isOpen, setIsOpen] = useState(false); + const ref = useRef(null); + useOnClickOutside(ref, () => setIsOpen(false)); + + return ( + + setIsOpen(!isOpen)} /> + + {isOpen && ( + + {data.map((item, index) => ( + + + {item.label} + + ))} + + )} + + + ); +}; +export default DvtDropdown; diff --git a/superset-frontend/src/components/DvtNavbar/dvt-navbar-tabs-data.ts b/superset-frontend/src/components/DvtNavbar/dvt-navbar-tabs-data.ts index 5a8bfc02200b5..e92c30a70749e 100644 --- a/superset-frontend/src/components/DvtNavbar/dvt-navbar-tabs-data.ts +++ b/superset-frontend/src/components/DvtNavbar/dvt-navbar-tabs-data.ts @@ -60,4 +60,5 @@ export const WithNavbarBottom: string[] = [ '/alert/list/', '/superset/sqllab/history/', '/superset/sqllab/', + '/chart/add', ]; diff --git a/superset-frontend/src/components/DvtNavbar/index.tsx b/superset-frontend/src/components/DvtNavbar/index.tsx index 2c273fa60651c..e0c38879aa91d 100644 --- a/superset-frontend/src/components/DvtNavbar/index.tsx +++ b/superset-frontend/src/components/DvtNavbar/index.tsx @@ -78,7 +78,7 @@ const DvtNavbar: React.FC = ({ pathName, user }) => { case '/superset/profile/admin/': return 'Profile'; case '/chart/add': - return 'Create a New Graph/Chart'; + return 'Create New Chart'; case '/dataset/add/': return 'New Dataset'; default: @@ -152,6 +152,19 @@ const DvtNavbar: React.FC = ({ pathName, user }) => { )} + {pathName === '/chart/add' && ( + <> +
+ + {}} + bold + /> + + + )} )} diff --git a/superset-frontend/src/components/DvtSidebar/dvtSidebarData.ts b/superset-frontend/src/components/DvtSidebar/dvtSidebarData.ts index bec138178a1b9..a5a79df6e5677 100644 --- a/superset-frontend/src/components/DvtSidebar/dvtSidebarData.ts +++ b/superset-frontend/src/components/DvtSidebar/dvtSidebarData.ts @@ -171,6 +171,7 @@ const DvtSidebarData: SidebarDataProps[] = [ { label: 'Success', value: 'success' }, ], placeholder: 'Owner', + name: 'owner', }, { values: [ @@ -178,6 +179,7 @@ const DvtSidebarData: SidebarDataProps[] = [ { label: 'Success', value: 'success' }, ], placeholder: 'Database', + name: 'database', }, { values: [ @@ -187,6 +189,7 @@ const DvtSidebarData: SidebarDataProps[] = [ { label: 'Dolor Sit Amet', value: 'success1' }, ], placeholder: 'Schema', + name: 'schema', }, { values: [ @@ -194,6 +197,7 @@ const DvtSidebarData: SidebarDataProps[] = [ { label: 'Success', value: 'success' }, ], placeholder: 'Type', + name: 'type', }, { values: [ @@ -201,6 +205,7 @@ const DvtSidebarData: SidebarDataProps[] = [ { label: 'Success', value: 'success' }, ], placeholder: 'Certified', + name: 'certified', }, ], }, @@ -611,6 +616,32 @@ const DvtSidebarData: SidebarDataProps[] = [ }, ], }, + { + pathname: '/chart/add', + data: [ + { + placeholder: 'Dataset', + name: 'dataset', + }, + { + values: [ + { label: 'Popular', value: 'popular' }, + { label: 'ECharts', value: 'echarts' }, + { label: 'Advanced-Analytics', value: 'advanced_analytics' }, + ], + placeholder: 'Recommended Tags', + name: 'recommended_tags', + }, + { + placeholder: 'Category', + name: 'category', + }, + { + placeholder: 'Tags', + name: 'tags', + }, + ], + }, ]; export default DvtSidebarData; diff --git a/superset-frontend/src/components/DvtSidebar/index.tsx b/superset-frontend/src/components/DvtSidebar/index.tsx index d24080da29815..0e7c148ad1e88 100644 --- a/superset-frontend/src/components/DvtSidebar/index.tsx +++ b/superset-frontend/src/components/DvtSidebar/index.tsx @@ -1,11 +1,15 @@ -import React, { useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; import { dvtSidebarAlertsSetProperty, + dvtSidebarChartAddSetProperty, dvtSidebarConnectionSetProperty, + dvtSidebarDatasetsSetProperty, dvtSidebarReportsSetProperty, } from 'src/dvt-redux/dvt-sidebarReducer'; import { useAppSelector } from 'src/hooks/useAppSelector'; +import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils'; +import { ChartMetadata, t } from '@superset-ui/core'; import DvtLogo from '../DvtLogo'; import DvtDarkMode from '../DvtDarkMode'; import DvtTitlePlus from '../DvtTitlePlus'; @@ -25,20 +29,34 @@ import { } from './dvt-sidebar.module'; import DvtList from '../DvtList'; import DvtDatePicker from '../DvtDatepicker'; +import { usePluginContext } from '../DynamicPlugins'; interface DvtSidebarProps { pathName: string; } +interface EditedDataItem { + value: string; + label: string; +} + +type VizEntry = { + key: string; + value: ChartMetadata; +}; + const DvtSidebar: React.FC = ({ pathName }) => { const dispatch = useDispatch(); const reportsSelector = useAppSelector(state => state.dvtSidebar.reports); const alertsSelector = useAppSelector(state => state.dvtSidebar.alerts); + const datasetsSelector = useAppSelector(state => state.dvtSidebar.datasets); const connectionSelector = useAppSelector( state => state.dvtSidebar.connection, ); + const chartAddSelector = useAppSelector(state => state.dvtSidebar.chartAdd); const [darkMode, setDarkMode] = useState(false); const [active, setActive] = useState('test'); + const [editedData, setEditedData] = useState([]); const pathTitles = (pathname: string) => { switch (pathname) { @@ -60,6 +78,8 @@ const DvtSidebar: React.FC = ({ pathName }) => { return 'SQL History'; case '/superset/profile/admin/': return 'Profile'; + case '/chart/add': + return 'Chart Add'; case '/dataset/add/': return 'New Dataset'; default: @@ -104,6 +124,200 @@ const DvtSidebar: React.FC = ({ pathName }) => { ); }; + const updateDatasetsProperty = (value: string, propertyName: string) => { + dispatch( + dvtSidebarDatasetsSetProperty({ + datasets: { + ...datasetsSelector, + [propertyName]: value, + }, + }), + ); + }; + + const updateChartAddProperty = (value: string, propertyName: string) => { + const changesOneItem = ['recommended_tags', 'category', 'tags']; + if (chartAddSelector[propertyName] !== value) { + if (changesOneItem.includes(propertyName)) { + const oneSelectedItem = changesOneItem.reduce((acc, item) => { + acc[item] = propertyName === item ? value : ''; + return acc; + }, {}); + dispatch( + dvtSidebarChartAddSetProperty({ + chartAdd: { + ...chartAddSelector, + ...oneSelectedItem, + }, + }), + ); + } else { + dispatch( + dvtSidebarChartAddSetProperty({ + chartAdd: { + ...chartAddSelector, + [propertyName]: value, + }, + }), + ); + } + } + }; + + useEffect(() => { + if (pathTitles(pathName) === 'Chart Add') { + const fetchData = async () => { + try { + const response = await fetch('/api/v1/dataset/'); + const data = await response.json(); + const newEditedData = data.result.map((item: any) => ({ + value: item.table_name, + label: item.table_name, + })); + setEditedData(newEditedData); + } catch (error) { + console.error('Hata:', error); + } + }; + + fetchData(); + } + }, [pathName]); + + 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', + 'time_table', + 'histogram', + 'deck_scatter', + 'deck_hex', + 'time_pivot', + 'deck_arc', + 'heatmap', + 'deck_grid', + 'deck_screengrid', + 'treemap_v2', + 'box_plot', + 'sunburst', + 'sankey', + 'word_cloud', + 'mapbox', + 'kepler', + 'cal_heatmap', + 'rose', + 'bubble', + 'bubble_v2', + 'deck_geojson', + 'horizon', + 'deck_multi', + 'compare', + 'partition', + 'event_flow', + 'deck_path', + 'graph_chart', + 'world_map', + 'paired_ttest', + 'para', + 'country_map', + ]; + + const { mountedPluginMetadata } = usePluginContext(); + const typesWithDefaultOrder = new Set(DEFAULT_ORDER); + const RECOMMENDED_TAGS = [ + t('Popular'), + t('ECharts'), + t('Advanced-Analytics'), + ]; + const OTHER_CATEGORY = t('Other'); + + function vizSortFactor(entry: VizEntry) { + if (typesWithDefaultOrder.has(entry.key)) { + return DEFAULT_ORDER.indexOf(entry.key); + } + return DEFAULT_ORDER.length; + } + const chartMetadata: VizEntry[] = useMemo(() => { + const result = Object.entries(mountedPluginMetadata) + .map(([key, value]) => ({ key, value })) + .filter( + ({ value }) => + nativeFilterGate(value.behaviors || []) && !value.deprecated, + ); + result.sort((a, b) => vizSortFactor(a) - vizSortFactor(b)); + return result; + }, [mountedPluginMetadata]); + + const chartsByTags = useMemo(() => { + const result: Record = {}; + + chartMetadata.forEach(entry => { + const tags = entry.value.tags || []; + tags.forEach(tag => { + if (!result[tag]) { + result[tag] = []; + } + result[tag].push(entry); + }); + }); + + return result; + }, [chartMetadata]); + + const tags = useMemo( + () => + Object.keys(chartsByTags) + .sort((a, b) => a.localeCompare(b)) + .filter(tag => RECOMMENDED_TAGS.indexOf(tag) === -1), + [chartsByTags], + ); + + const chartsByCategory = useMemo(() => { + const result: Record = {}; + chartMetadata.forEach(entry => { + const category = entry.value.category || OTHER_CATEGORY; + if (!result[category]) { + result[category] = []; + } + result[category].push(entry); + }); + return result; + }, [chartMetadata]); + + const categories = useMemo( + () => + Object.keys(chartsByCategory).sort((a, b) => { + // make sure Other goes at the end + if (a === OTHER_CATEGORY) return 1; + if (b === OTHER_CATEGORY) return -1; + // sort alphabetically + return a.localeCompare(b); + }), + [chartsByCategory], + ); + + const tag: { value: string; label: string }[] = tags.map(tag => ({ + value: tag, + label: tag, + })); + + const category: { value: string; label: string }[] = categories.map( + categories => ({ value: categories, label: categories }), + ); + return ( @@ -140,6 +354,7 @@ const DvtSidebar: React.FC = ({ pathName }) => { pathTitles(pathName) === 'Reports' || pathTitles(pathName) === 'Connection' || pathTitles(pathName) === 'SQL Lab' || + pathTitles(pathName) === 'Chart Add' || pathTitles(pathName) === 'SQL History') && ( {sidebarDataFindPathname?.data.map( @@ -158,7 +373,15 @@ const DvtSidebar: React.FC = ({ pathName }) => { {!data.datePicker && !data.valuesList && ( = ({ pathName }) => { ? alertsSelector[data.name] : pathTitles(pathName) === 'Connection' ? connectionSelector[data.name] + : pathTitles(pathName) === 'Datasets' + ? datasetsSelector[data.name] + : pathTitles(pathName) === 'Chart Add' + ? chartAddSelector[data.name] : undefined } setSelectedValue={value => { @@ -177,6 +404,10 @@ const DvtSidebar: React.FC = ({ pathName }) => { updateAlertsProperty(value, data.name); } else if (pathTitles(pathName) === 'Connection') { updateConnectionProperty(value, data.name); + } else if (pathTitles(pathName) === 'Datasets') { + updateDatasetsProperty(value, data.name); + } else if (pathTitles(pathName) === 'Chart Add') { + updateChartAddProperty(value, data.name); } }} maxWidth diff --git a/superset-frontend/src/components/DvtTable/dvt-table-data.ts b/superset-frontend/src/components/DvtTable/dvt-table-data.ts new file mode 100644 index 0000000000000..58cf97900355b --- /dev/null +++ b/superset-frontend/src/components/DvtTable/dvt-table-data.ts @@ -0,0 +1,709 @@ +const TableData = { + favoritesData: [ + { + id: 1, + name: 'arac', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: false, + }, + { + id: 2, + name: 'hrrr2', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: true, + }, + { + id: 3, + name: 'channel_members', + type: 'Physical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: true, + }, + { + id: 4, + name: 'channel', + type: 'Physical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: true, + }, + { + id: 5, + name: 'cleaned_sales_data', + type: 'Physical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: true, + }, + { + id: 6, + name: 'covid_vaccines', + type: 'Physical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: true, + }, + { + id: 7, + name: 'exported_stats', + type: 'Physical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: true, + }, + { + id: 8, + name: 'members_channels_2', + type: 'Physical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: true, + }, + { + id: 9, + name: 'Fcc 2018 Survey', + type: 'Physical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + isFavorite: true, + }, + ], + defaultData: [ + { + name: 'arac', + type: 'Pysical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + { + name: 'hrrr2', + type: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + { + name: 'channel_members', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + { + name: 'channel', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + { + name: 'cleaned_sales_data', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + { + name: 'covid_vaccines', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + { + name: 'exported_stats', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + { + name: 'members_channels_2', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + { + name: 'Fcc 2018 Survey', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + link: '/dashboard/list/', + }, + ], + exampleData: [ + { + id: '64e417of7f25253d56019818b7e9fdcD', + is_software_dev: '0', + is_first_dev_job: 'Null', + months_job_search: 'Null', + months_job_search2: 'Null', + job_pref: 'freelance', + }, + { + id: '64e417of7f25253d56019818b7e9fdcD', + is_software_dev: '0', + is_first_dev_job: 'Null', + months_job_search: 'Null', + months_job_search2: 'Null', + job_pref: 'freelance', + }, + { + id: '64e417of7f25253d56019818b7e9fdcD', + is_software_dev: '0', + is_first_dev_job: 'Null', + months_job_search: 'Null', + months_job_search2: 'Null', + job_pref: 'freelance', + }, + { + id: '64e417of7f25253d56019818b7e9fdcD', + is_software_dev: '0', + is_first_dev_job: 'Null', + months_job_search: 'Null', + months_job_search2: 'Null', + job_pref: 'freelance', + }, + { + id: '64e417of7f25253d56019818b7e9fdcD', + is_software_dev: '0', + is_first_dev_job: 'Null', + months_job_search: 'Null', + months_job_search2: 'Null', + job_pref: 'freelance', + }, + { + id: '64e417of7f25253d56019818b7e9fdcD', + is_software_dev: '0', + is_first_dev_job: 'Null', + months_job_search: 'Null', + months_job_search2: 'Null', + job_pref: 'freelance', + }, + ], + iconExampleData: [ + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + { + date: '2023.05.29 15:53:47 * 03:00', + tabName: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + tables: 'hrr2', + user: 'Admin', + rows: '564', + sql: 'Select', + }, + ], + hoverExampleData: [ + { + name: 'arac', + type: 'Pysical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'hrrr2', + type: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'channel_members', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'channel', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'cleaned_sales_data', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'covid_vaccines', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'exported_stats', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'members_channels_2', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'Fcc 2018 Survey', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + ], + iconColourExampleData: [ + { + name: 'arac', + type: 'Pysical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'hrrr2', + type: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'channel_members', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'channel', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'cleaned_sales_data', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'covid_vaccines', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'exported_stats', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'members_channels_2', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + name: 'Fcc 2018 Survey', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + ], + checkboxExampleData: [ + { + id: 1, + name: 'arac', + type: 'Pysical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + id: 2, + name: 'hrrr2', + type: 'Pysical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + id: 3, + name: 'channel_members', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + id: 4, + name: 'channel', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + id: 5, + name: 'cleaned_sales_data', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + id: 6, + name: 'covid_vaccines', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + id: 7, + name: 'exported_stats', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + id: 8, + name: 'members_channels_2', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + { + id: 9, + name: 'Fcc 2018 Survey', + type: 'Pysical', + database: 'Examples', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + }, + ], + activeColumnData: [ + { + id: 1, + name: 'arac', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: true, + }, + { + id: 2, + name: 'hrrr2', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: false, + }, + { + id: 3, + name: 'arac', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: true, + }, + { + id: 4, + name: 'hrrr2', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: false, + }, + { + id: 5, + name: 'arac', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: true, + }, + { + id: 6, + name: 'hrrr2', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: false, + }, + { + id: 7, + name: 'arac', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: true, + }, + { + id: 8, + name: 'hrrr2', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: false, + }, + { + id: 9, + name: 'arac', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Dwh', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: true, + }, + { + id: 10, + name: 'hrrr2', + type: 'Physical', + database: 'PostgreSQL', + schema: 'Public', + date: '10.03.2023 12:45:00', + modifiedBy: 'Admin', + owners: 'A', + active: false, + }, + ], +}; + +export default TableData; diff --git a/superset-frontend/src/components/DvtTable/dvt-table.module.tsx b/superset-frontend/src/components/DvtTable/dvt-table.module.tsx index 99d0449a628ad..4c68921cfb34f 100644 --- a/superset-frontend/src/components/DvtTable/dvt-table.module.tsx +++ b/superset-frontend/src/components/DvtTable/dvt-table.module.tsx @@ -93,13 +93,8 @@ const StyledTableTh = styled.th` const StyledTableTbody = styled.tbody``; -interface StyledTableTdProps { - $onLink: boolean; -} - -const StyledTableTd = styled.td` - color: ${({ $onLink, theme }) => - $onLink ? theme.colors.dvt.primary.base : theme.colors.grayscale.dark2}; +const StyledTableTd = styled.td` + color: ${({ theme }) => theme.colors.grayscale.dark2}; font-size: 14px; font-weight: 400; &:first-of-type { @@ -118,6 +113,10 @@ const StyledTableCheckbox = styled.div` margin-right: 24px; `; +const StyledTableUrl = styled.div` + color: ${({ theme }) => theme.colors.dvt.primary.base}; +`; + export { StyledTable, StyledTableTable, @@ -129,4 +128,5 @@ export { StyledTableTitle, StyledTableIcon, StyledTableCheckbox, + StyledTableUrl, }; diff --git a/superset-frontend/src/components/DvtTable/dvt-table.stories.tsx b/superset-frontend/src/components/DvtTable/dvt-table.stories.tsx index 4689257206be7..cf26740612618 100644 --- a/superset-frontend/src/components/DvtTable/dvt-table.stories.tsx +++ b/superset-frontend/src/components/DvtTable/dvt-table.stories.tsx @@ -17,11 +17,21 @@ * under the License. */ import React, { useState } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import DvtTable, { DvtTableProps } from '.'; +import TableData from './dvt-table-data'; + export default { title: 'Dvt-Components/DvtTable', component: DvtTable, + decorators: [ + (Story: any) => ( + + + + ), + ], }; export const Default = (args: DvtTableProps) => ( @@ -33,7 +43,7 @@ export const Default = (args: DvtTableProps) => ( padding: 32, }} > - +
); @@ -42,8 +52,8 @@ Default.args = { { title: 'Name', field: 'name', - folderIcon: true, - onLink: true, + icon: 'dvt-folder', + urlField: 'link', flex: 3, }, { title: 'Type', field: 'type' }, @@ -74,87 +84,59 @@ Default.args = { ], }, ], - data: [ - { - name: 'arac', - type: 'Pysical', - database: 'PostgreSQL', - schema: 'Dwh', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'hrrr2', - type: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'channel_members', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'channel', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'cleaned_sales_data', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'covid_vaccines', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'exported_stats', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, +}; + +export const FavoriteExample = (args: DvtTableProps) => { + const [data, setData] = useState(TableData.favoritesData); + return ( +
+ +
+ ); +}; + +FavoriteExample.args = { + header: [ + { isFavorite: true, flex: 0.5 }, { - name: 'members_channels_2', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', + title: 'Name', + field: 'name', + urlField: 'link', + flex: 3, }, + { title: 'Type', field: 'type' }, + { title: 'Database', field: 'database' }, + { title: 'Schema', field: 'schema' }, + { title: 'Modified Date', field: 'date' }, + { title: 'Modified By', field: 'modifiedBy' }, + { title: 'Owners', field: 'owners' }, { - name: 'Fcc 2018 Survey', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', + title: 'Action', + clicks: [ + { + icon: 'edit_alt', + click: () => {}, + popperLabel: 'Edit', + }, + + { + icon: 'share', + click: () => {}, + popperLabel: 'Export', + }, + { + icon: 'trash', + click: () => {}, + popperLabel: 'Share', + }, + ], }, ], }; @@ -168,7 +150,7 @@ export const Example = (args: DvtTableProps) => ( padding: 32, }} > - + ); @@ -185,56 +167,6 @@ Example.args = { { title: 'months_job_search', field: 'months_job_search2' }, { title: 'job_pref', field: 'job_pref' }, ], - data: [ - { - id: '64e417of7f25253d56019818b7e9fdcD', - is_software_dev: '0', - is_first_dev_job: 'Null', - months_job_search: 'Null', - months_job_search2: 'Null', - job_pref: 'freelance', - }, - { - id: '64e417of7f25253d56019818b7e9fdcD', - is_software_dev: '0', - is_first_dev_job: 'Null', - months_job_search: 'Null', - months_job_search2: 'Null', - job_pref: 'freelance', - }, - { - id: '64e417of7f25253d56019818b7e9fdcD', - is_software_dev: '0', - is_first_dev_job: 'Null', - months_job_search: 'Null', - months_job_search2: 'Null', - job_pref: 'freelance', - }, - { - id: '64e417of7f25253d56019818b7e9fdcD', - is_software_dev: '0', - is_first_dev_job: 'Null', - months_job_search: 'Null', - months_job_search2: 'Null', - job_pref: 'freelance', - }, - { - id: '64e417of7f25253d56019818b7e9fdcD', - is_software_dev: '0', - is_first_dev_job: 'Null', - months_job_search: 'Null', - months_job_search2: 'Null', - job_pref: 'freelance', - }, - { - id: '64e417of7f25253d56019818b7e9fdcD', - is_software_dev: '0', - is_first_dev_job: 'Null', - months_job_search: 'Null', - months_job_search2: 'Null', - job_pref: 'freelance', - }, - ], }; export const IconExample = (args: DvtTableProps) => ( @@ -246,7 +178,7 @@ export const IconExample = (args: DvtTableProps) => ( padding: 32, }} > - + ); @@ -262,7 +194,7 @@ IconExample.args = { { title: 'Tables', field: 'tables' }, { title: 'User', field: 'user' }, { title: 'Rows', field: 'rows' }, - { title: 'SQL', field: 'sql', onLink: true }, + { title: 'SQL', field: 'sql', urlField: 'link' }, { title: 'Action', clicks: [ @@ -273,98 +205,6 @@ IconExample.args = { ], }, ], - data: [ - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - { - date: '2023.05.29 15:53:47 * 03:00', - tabName: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - tables: 'hrr2', - user: 'Admin', - rows: '564', - sql: 'Select', - }, - ], }; export const HoverExample = (args: DvtTableProps) => ( @@ -376,7 +216,7 @@ export const HoverExample = (args: DvtTableProps) => ( padding: 32, }} > - + ); @@ -385,8 +225,8 @@ HoverExample.args = { { title: 'Name', field: 'name', - folderIcon: true, - onLink: true, + icon: 'dvt-folder', + urlField: 'link', flex: 3, }, { title: 'Type', field: 'type' }, @@ -415,89 +255,6 @@ HoverExample.args = { ], }, ], - data: [ - { - name: 'arac', - type: 'Pysical', - database: 'PostgreSQL', - schema: 'Dwh', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'hrrr2', - type: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'channel_members', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'channel', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'cleaned_sales_data', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'covid_vaccines', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'exported_stats', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'members_channels_2', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'Fcc 2018 Survey', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - ], }; export const IconColourExample = (args: DvtTableProps) => ( @@ -509,7 +266,7 @@ export const IconColourExample = (args: DvtTableProps) => ( padding: 32, }} > - + ); @@ -518,8 +275,8 @@ IconColourExample.args = { { title: 'Name', field: 'name', - folderIcon: true, - onLink: true, + icon: 'dvt-folder', + urlField: 'link', flex: 3, }, { title: 'Type', field: 'type' }, @@ -548,89 +305,6 @@ IconColourExample.args = { ], }, ], - data: [ - { - name: 'arac', - type: 'Pysical', - database: 'PostgreSQL', - schema: 'Dwh', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'hrrr2', - type: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'channel_members', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'channel', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'cleaned_sales_data', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'covid_vaccines', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'exported_stats', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'members_channels_2', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - name: 'Fcc 2018 Survey', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - ], }; export const CheckboxExample = (args: DvtTableProps) => { @@ -645,7 +319,12 @@ export const CheckboxExample = (args: DvtTableProps) => { padding: 32, }} > - + ); }; @@ -656,8 +335,8 @@ CheckboxExample.args = { { title: 'Name', field: 'name', - folderIcon: true, - onLink: true, + icon: 'dvt-folder', + urlField: 'link', flex: 3, }, { title: 'Type', field: 'type' }, @@ -685,96 +364,63 @@ CheckboxExample.args = { ], }, ], - data: [ - { - id: 1, - name: 'arac', - type: 'Pysical', - database: 'PostgreSQL', - schema: 'Dwh', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - id: 2, - name: 'hrrr2', - type: 'Pysical', - database: 'PostgreSQL', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - id: 3, - name: 'channel_members', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - id: 4, - name: 'channel', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - id: 5, - name: 'cleaned_sales_data', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - id: 6, - name: 'covid_vaccines', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, - { - id: 7, - name: 'exported_stats', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', - }, +}; + +export const ActiveColumn = (args: DvtTableProps) => { + const [selected, setSelected] = useState([]); + + return ( +
+ +
+ ); +}; + +ActiveColumn.args = { + header: [ { - id: 8, - name: 'members_channels_2', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', + title: 'Name', + field: 'name', + icon: 'dvt-folder', + iconActive: 'dvt-file', + urlField: 'link', + flex: 3, }, + { title: 'Type', field: 'type' }, + { title: 'Database', field: 'database' }, + { title: 'Schema', field: 'schema' }, + { title: 'Modified Date', field: 'date' }, + { title: 'Modified By', field: 'modifiedBy' }, + { title: 'Owners', field: 'owners' }, { - id: 9, - name: 'Fcc 2018 Survey', - type: 'Pysical', - database: 'Examples', - schema: 'Public', - date: '10.03.2023 12:45:00', - modifiedBy: 'Admin', - owners: 'A', + title: 'Action', + clicks: [ + { + icon: 'edit_alt', + click: () => {}, + }, + + { + icon: 'share', + click: () => {}, + }, + { + icon: 'trash', + click: () => {}, + }, + ], }, ], }; diff --git a/superset-frontend/src/components/DvtTable/index.tsx b/superset-frontend/src/components/DvtTable/index.tsx index d716d804686fa..1f36d391a5001 100644 --- a/superset-frontend/src/components/DvtTable/index.tsx +++ b/superset-frontend/src/components/DvtTable/index.tsx @@ -17,10 +17,11 @@ * under the License. */ import React, { useState } from 'react'; +import { useHistory } from 'react-router-dom'; import type { CheckboxChangeEvent } from 'antd/es/checkbox'; import { SupersetTheme, supersetTheme } from '@superset-ui/core'; -import { FolderOutlined, HeartOutlined } from '@ant-design/icons'; import { Checkbox } from 'antd'; +import Icons from '../Icons'; import Icon from '../Icons/Icon'; import { StyledTable, @@ -33,25 +34,30 @@ import { StyledTableTitle, StyledTableIcon, StyledTableCheckbox, + StyledTableUrl, } from './dvt-table.module'; import DvtPopper from '../DvtPopper'; +import { useToasts } from '../MessageToasts/withToasts'; interface HeaderProps { id: number; title: string; field?: string; - folderIcon?: boolean; - heartIcon?: boolean; - onLink?: boolean; + icon?: string; + iconActive?: string; + iconClick?: () => {}; + urlField?: string; flex?: number; clicks?: { icon: string; - click: () => void; + click: (row: any) => void; colour?: string; popperLabel?: string; }[]; showHover?: boolean; checkbox?: boolean; + isFavorite?: boolean; + isFavoriteApiUrl?: string; } export interface DvtTableProps { @@ -61,6 +67,7 @@ export interface DvtTableProps { selected?: any[]; setSelected?: (newSelected: any[]) => void; checkboxActiveField?: string; + setFavoriteData?: (item: any) => void; } const DvtTable: React.FC = ({ @@ -70,7 +77,9 @@ const DvtTable: React.FC = ({ selected = [], setSelected = () => {}, checkboxActiveField = 'id', + setFavoriteData, }) => { + const { addDangerToast } = useToasts(); const [openRow, setOpenRow] = useState(null); const formatDateTime = (dateTimeString: string) => { @@ -91,6 +100,8 @@ const DvtTable: React.FC = ({ 0, ); + const history = useHistory(); + const columnsWithDefaults = 100 / totalFlex; const checkAll = data.length === selected.length; @@ -101,6 +112,39 @@ const DvtTable: React.FC = ({ setSelected(e.target.checked ? data.slice() : []); }; + const handleFavouriteData = async (item: any, apiUrl: string | undefined) => { + if (setFavoriteData) { + const findItem = data.find(row => row.id === item.id); + const findItemRemovedData = data.filter(row => row.id !== item.id); + + const changedItemFavorite = () => { + setFavoriteData( + [ + ...findItemRemovedData, + { ...findItem, isFavorite: !item.isFavorite }, + ].sort((a, b) => a.id - b.id), + ); + }; + + if (apiUrl) { + try { + const res = await fetch( + `${apiUrl}/${item.id}/${item.isFavorite ? 'unselect' : 'select'}`, + ); + if (res.ok) { + changedItemFavorite(); + } else { + addDangerToast(`${res.status} ${res.statusText}`); + } + } catch (error) { + addDangerToast(error.message); + } + } else { + changedItemFavorite(); + } + } + }; + return ( @@ -126,81 +170,131 @@ const DvtTable: React.FC = ({ - {data.map((row, rowIndex) => ( - onRowClick?.(row)} - onMouseOver={() => setOpenRow(rowIndex)} - onMouseOut={() => setOpenRow(null)} - > - {header.map((column, columnIndex) => ( - - - {column.checkbox && columnIndex === 0 && ( - - - item[checkboxActiveField] === - row[checkboxActiveField], + {data + .sort((a, b) => a.id - b.id) + .map((row, rowIndex) => ( + onRowClick?.(row)} + onMouseOver={() => setOpenRow(rowIndex)} + onMouseOut={() => setOpenRow(null)} + > + {header.map((column, columnIndex) => ( + + + {column.checkbox && columnIndex === 0 && ( + + + item[checkboxActiveField] === + row[checkboxActiveField], + )} + onChange={e => { + const checkedRows = e.target.checked + ? [...selected, row] + : selected.filter( + item => + item[checkboxActiveField] !== + row[checkboxActiveField], + ); + setSelected(checkedRows); + }} + /> + + )} + {column.icon && ( + ({ + color: theme.colors.grayscale.dark2, + marginRight: '14px', + fontSize: '20px', + })} + /> + )} + {column.isFavorite && ( + + handleFavouriteData(row, column?.isFavoriteApiUrl) + } + > + {row.isFavorite ? ( + + ) : ( + )} - onChange={e => { - const checkedRows = e.target.checked - ? [...selected, row] - : selected.filter( - item => - item[checkboxActiveField] !== - row[checkboxActiveField], - ); - setSelected(checkedRows); + + )} + {column.urlField && column.field && ( + { + if (column.urlField) { + history.push(row[column.urlField]); + } }} - /> - - )} - {column.folderIcon && ( - ({ - color: theme.colors.grayscale.dark2, - marginRight: '14px', - fontSize: '20px', - })} - /> - )} - {column.heartIcon && ( - ({ - color: theme.colors.grayscale.dark2, - marginRight: '14px', - fontSize: '20px', - })} - /> - )} - {column.field === 'date' ? ( - <> - {formatDateTime(row[column.field]).date} -
- {formatDateTime(row[column.field]).time} - - ) : ( - <> - {column.clicks?.map( - ( - clicks: { - icon: string; - click: () => void; - colour: string; - popperLabel?: string; - }, - index, - ) => ( - - {clicks.popperLabel && ( - + > + {row[column.field]} + + )} + {column.field === 'date' ? ( + <> + {formatDateTime(row[column.field]).date} +
+ {formatDateTime(row[column.field]).time} + + ) : ( + <> + {column.clicks?.map( + ( + clicks: { + icon: string; + click: (row: any) => void; + colour: string; + popperLabel?: string; + }, + index, + ) => ( + + {clicks.popperLabel && ( + + clicks.click(row)} + fileName={clicks.icon} + iconColor={ + clicks.colour || + supersetTheme.colors.grayscale.dark2 + } + iconSize="xl" + style={{ + marginRight: 3.6, + visibility: column.showHover + ? openRow === rowIndex + ? 'visible' + : 'hidden' + : 'visible', + height: '56px', + display: 'flex', + alignItems: 'center', + }} + /> + + )} + {!clicks.popperLabel && ( clicks.click(row)} fileName={clicks.icon} iconColor={ clicks.colour || @@ -214,46 +308,23 @@ const DvtTable: React.FC = ({ ? 'visible' : 'hidden' : 'visible', - height: '56px', - display: 'flex', - alignItems: 'center', }} /> -
- )} - {!clicks.popperLabel && ( - - )} -
- ), - )} + )} + + ), + )} - {column.field !== 'action' && column.field && ( - <>{row[column.field]} - )} - - )} -
-
- ))} -
- ))} + {column.field !== 'action' && + column.field && + !column.urlField && <>{row[column.field]}} + + )} + + + ))} + + ))}
diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx index a17b168374592..51de15c7a1fcd 100644 --- a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardBuilder.tsx @@ -86,72 +86,10 @@ import { import { getRootLevelTabsComponent, shouldFocusTabs } from './utils'; import DashboardContainer from './DashboardContainer'; import { useNativeFilters } from './state'; +import DashboardWrapper from './DashboardWrapper'; type DashboardBuilderProps = {}; -const StyledDiv = styled.div` - ${({ theme }) => css` - display: grid; - grid-template-columns: auto 1fr; - grid-template-rows: auto 1fr; - flex: 1; - /* Special cases */ - - /* A row within a column has inset hover menu */ - .dragdroppable-column .dragdroppable-row .hover-menu--left { - left: ${theme.gridUnit * -3}px; - background: ${theme.colors.grayscale.light5}; - border: 1px solid ${theme.colors.grayscale.light2}; - } - - .dashboard-component-tabs { - position: relative; - } - - /* A column within a column or tabs has inset hover menu */ - .dragdroppable-column .dragdroppable-column .hover-menu--top, - .dashboard-component-tabs .dragdroppable-column .hover-menu--top { - top: ${theme.gridUnit * -3}px; - background: ${theme.colors.grayscale.light5}; - border: 1px solid ${theme.colors.grayscale.light2}; - } - - /* move Tabs hover menu to top near actual Tabs */ - .dashboard-component-tabs > .hover-menu-container > .hover-menu--left { - top: 0; - transform: unset; - background: transparent; - } - - /* push Chart actions to upper right */ - .dragdroppable-column .dashboard-component-chart-holder .hover-menu--top, - .dragdroppable .dashboard-component-header .hover-menu--top { - right: ${theme.gridUnit * 2}px; - top: ${theme.gridUnit * 2}px; - background: transparent; - border: none; - transform: unset; - left: unset; - } - div:hover > .hover-menu-container .hover-menu, - .hover-menu-container .hover-menu:hover { - opacity: 1; - } - - p { - margin: 0 0 ${theme.gridUnit * 2}px 0; - } - - i.danger { - color: ${theme.colors.error.base}; - } - - i.warning { - color: ${theme.colors.alert.base}; - } - `} -`; - // @z-index-above-dashboard-charts + 1 = 11 const FiltersPanel = styled.div<{ width: number; hidden: boolean }>` grid-column: 1; @@ -317,7 +255,7 @@ const DashboardContentWrapper = styled.div` width: 100%; } - & > .empty-droptarget:first-child { + & > .empty-droptarget:first-child:not(.empty-droptarget--full) { height: ${theme.gridUnit * 4}px; top: -2px; z-index: 10; @@ -640,7 +578,7 @@ const DashboardBuilder: FC = () => { : theme.gridUnit * 8; return ( - + {showFilterBar && filterBarOrientation === FilterBarOrientation.VERTICAL && ( <> = () => { `} /> )} - + ); }; diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx new file mode 100644 index 0000000000000..fb913b46273d4 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.test.tsx @@ -0,0 +1,75 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React from 'react'; +import { fireEvent, render } from 'spec/helpers/testing-library'; +import { OptionControlLabel } from 'src/explore/components/controls/OptionControls'; + +import DashboardWrapper from './DashboardWrapper'; + +test('should render children', () => { + const { getByTestId } = render( + +
+ , + { useRedux: true, useDnd: true }, + ); + expect(getByTestId('mock-children')).toBeInTheDocument(); +}); + +test('should update the style on dragging state', () => { + const defaultProps = { + label: Test label, + tooltipTitle: 'This is a tooltip title', + onRemove: jest.fn(), + onMoveLabel: jest.fn(), + onDropLabel: jest.fn(), + type: 'test', + index: 0, + }; + const { container, getByText } = render( + + Label 1} + /> + Label 2} + /> + , + { + useRedux: true, + useDnd: true, + initialState: { + dashboardState: { + editMode: true, + }, + }, + }, + ); + expect( + container.getElementsByClassName('dragdroppable--dragging'), + ).toHaveLength(0); + fireEvent.dragStart(getByText('Label 1')); + expect( + container.getElementsByClassName('dragdroppable--dragging'), + ).toHaveLength(1); +}); diff --git a/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx new file mode 100644 index 0000000000000..f39c7ed630277 --- /dev/null +++ b/superset-frontend/src/dashboard/components/DashboardBuilder/DashboardWrapper.tsx @@ -0,0 +1,128 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useEffect } from 'react'; +import { css, styled } from '@superset-ui/core'; +import { RootState } from 'src/dashboard/types'; +import { useSelector } from 'react-redux'; +import { useDragDropManager } from 'react-dnd'; +import classNames from 'classnames'; + +const StyledDiv = styled.div` + ${({ theme }) => css` + display: grid; + grid-template-columns: auto 1fr; + grid-template-rows: auto 1fr; + flex: 1; + /* Special cases */ + + &.dragdroppable--dragging + .dashboard-component-tabs-content + > .empty-droptarget.empty-droptarget--full { + height: 100%; + } + + /* A row within a column has inset hover menu */ + .dragdroppable-column .dragdroppable-row .hover-menu--left { + left: ${theme.gridUnit * -3}px; + background: ${theme.colors.grayscale.light5}; + border: 1px solid ${theme.colors.grayscale.light2}; + } + + .dashboard-component-tabs { + position: relative; + } + + /* A column within a column or tabs has inset hover menu */ + .dragdroppable-column .dragdroppable-column .hover-menu--top, + .dashboard-component-tabs .dragdroppable-column .hover-menu--top { + top: ${theme.gridUnit * -3}px; + background: ${theme.colors.grayscale.light5}; + border: 1px solid ${theme.colors.grayscale.light2}; + } + + /* move Tabs hover menu to top near actual Tabs */ + .dashboard-component-tabs > .hover-menu-container > .hover-menu--left { + top: 0; + transform: unset; + background: transparent; + } + + /* push Chart actions to upper right */ + .dragdroppable-column .dashboard-component-chart-holder .hover-menu--top, + .dragdroppable .dashboard-component-header .hover-menu--top { + right: ${theme.gridUnit * 2}px; + top: ${theme.gridUnit * 2}px; + background: transparent; + border: none; + transform: unset; + left: unset; + } + div:hover > .hover-menu-container .hover-menu, + .hover-menu-container .hover-menu:hover { + opacity: 1; + } + + p { + margin: 0 0 ${theme.gridUnit * 2}px 0; + } + + i.danger { + color: ${theme.colors.error.base}; + } + + i.warning { + color: ${theme.colors.alert.base}; + } + `} +`; + +type Props = {}; + +const DashboardWrapper: React.FC = ({ children }) => { + const editMode = useSelector( + state => state.dashboardState.editMode, + ); + const dragDropManager = useDragDropManager(); + const [isDragged, setIsDragged] = React.useState( + dragDropManager.getMonitor().isDragging(), + ); + + useEffect(() => { + const monitor = dragDropManager.getMonitor(); + const unsub = monitor.subscribeToStateChange(() => { + setIsDragged(monitor.isDragging()); + }); + + return () => { + unsub(); + }; + }, [dragDropManager]); + + return ( + + {children} + + ); +}; + +export default DashboardWrapper; diff --git a/superset-frontend/src/dashboard/components/DashboardGrid.jsx b/superset-frontend/src/dashboard/components/DashboardGrid.jsx index 601dbac4a4cdc..70cf65218f2c3 100644 --- a/superset-frontend/src/dashboard/components/DashboardGrid.jsx +++ b/superset-frontend/src/dashboard/components/DashboardGrid.jsx @@ -18,6 +18,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { addAlpha, css, styled, t } from '@superset-ui/core'; import { EmptyStateBig } from 'src/components/EmptyState'; import { componentShape } from '../util/propShapes'; @@ -76,10 +77,14 @@ const GridContent = styled.div` & > .empty-droptarget:first-child { height: ${theme.gridUnit * 12}px; margin-top: ${theme.gridUnit * -6}px; - margin-bottom: ${theme.gridUnit * -6}px; } - & > .empty-droptarget:only-child { + & > .empty-droptarget:last-child { + height: ${theme.gridUnit * 12}px; + margin-top: ${theme.gridUnit * -6}px; + } + + & > .empty-droptarget.empty-droptarget--full:only-child { height: 80vh; } `} @@ -270,10 +275,14 @@ class DashboardGrid extends React.PureComponent { index={0} orientation="column" onDrop={this.handleTopDropTargetDrop} - className="empty-droptarget" + className={classNames({ + 'empty-droptarget': true, + 'empty-droptarget--full': + gridComponent?.children?.length === 0, + })} editMode > - {renderDraggableContentBottom} + {renderDraggableContentTop} )} {gridComponent?.children?.map((id, index) => ( @@ -304,7 +313,7 @@ class DashboardGrid extends React.PureComponent { className="empty-droptarget" editMode > - {renderDraggableContentTop} + {renderDraggableContentBottom} )} {isResizing && diff --git a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx index 287d83692f9d2..17d5bdc83e05c 100644 --- a/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx +++ b/superset-frontend/src/dashboard/components/SliceHeaderControls/index.tsx @@ -373,6 +373,12 @@ const SliceHeaderControls = (props: SliceHeaderControlsPropsWithRouter) => { ? t('Exit fullscreen') : t('Enter fullscreen'); + // @z-index-below-dashboard-header (100) - 1 = 99 for !isFullSize and 101 for isFullSize + const dropdownOverlayStyle = { + zIndex: isFullSize ? 101 : 99, + animationDuration: '0s', + }; + const menu = ( { )} diff --git a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx index 3bc9f4d299a00..6a49f9887550f 100644 --- a/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx +++ b/superset-frontend/src/dashboard/components/dnd/DragDroppable.jsx @@ -90,6 +90,11 @@ const DragDroppableStyles = styled.div` z-index: 10; } + &.empty-droptarget--full > .drop-indicator--top { + height: 100%; + opacity: 0.3; + } + & { .drop-indicator { display: block; @@ -99,7 +104,7 @@ const DragDroppableStyles = styled.div` } .drop-indicator--top { - top: 0; + top: ${-theme.gridUnit - 2}px; left: 0; height: ${theme.gridUnit}px; width: 100%; @@ -107,7 +112,7 @@ const DragDroppableStyles = styled.div` } .drop-indicator--bottom { - top: 100%; + bottom: ${-theme.gridUnit - 2}px; left: 0; height: ${theme.gridUnit}px; width: 100%; @@ -116,7 +121,7 @@ const DragDroppableStyles = styled.div` .drop-indicator--right { top: 0; - left: 100%; + left: calc(100% - ${theme.gridUnit}px); height: 100%; width: ${theme.gridUnit}px; min-height: ${theme.gridUnit * 4}px; diff --git a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx index 32ac77936c6ad..d1d08176baa93 100644 --- a/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx +++ b/superset-frontend/src/dashboard/components/gridComponents/Tab.jsx @@ -18,6 +18,7 @@ */ import React from 'react'; import PropTypes from 'prop-types'; +import classNames from 'classnames'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import { styled, t } from '@superset-ui/core'; @@ -173,7 +174,10 @@ class Tab extends React.PureComponent { depth={depth} onDrop={this.handleTopDropTargetDrop} editMode - className="empty-droptarget" + className={classNames({ + 'empty-droptarget': true, + 'empty-droptarget--full': tabComponent.children.length === 0, + })} > {renderDraggableContentTop} @@ -234,7 +238,7 @@ class Tab extends React.PureComponent { /> ))} {/* Make bottom of tab droppable */} - {editMode && ( + {editMode && tabComponent.children.length > 0 && ( , + ) => ({ + ...state, + datasets: { + ...state.datasets, + ...action.payload.datasets, + }, + }), + dvtSidebarChartAddSetProperty: ( + state, + action: PayloadAction<{ chartAdd: DvtSidebarState['chartAdd'] }>, + ) => ({ + ...state, + chartAdd: { + ...state.connection, + ...action.payload.chartAdd, + }, + }), }, }); @@ -109,6 +155,8 @@ export const { dvtSidebarReportsSetProperty, dvtSidebarAlertsSetProperty, dvtSidebarConnectionSetProperty, + dvtSidebarDatasetsSetProperty, + dvtSidebarChartAddSetProperty, } = dvtSidebarSlice.actions; export default dvtSidebarSlice.reducer; diff --git a/superset-frontend/src/explore/actions/exploreActions.test.js b/superset-frontend/src/explore/actions/exploreActions.test.js index 9dd53756800d1..54cf8f16c5c35 100644 --- a/superset-frontend/src/explore/actions/exploreActions.test.js +++ b/superset-frontend/src/explore/actions/exploreActions.test.js @@ -21,6 +21,63 @@ import { defaultState } from 'src/explore/store'; import exploreReducer from 'src/explore/reducers/exploreReducer'; import * as actions from 'src/explore/actions/exploreActions'; +const METRICS = [ + { + expressionType: 'SIMPLE', + column: { + advanced_data_type: null, + certification_details: null, + certified_by: null, + column_name: 'a', + description: null, + expression: null, + filterable: true, + groupby: true, + id: 1, + is_certified: false, + is_dttm: false, + python_date_format: null, + type: 'DOUBLE PRECISION', + type_generic: 0, + verbose_name: null, + warning_markdown: null, + }, + aggregate: 'SUM', + sqlExpression: null, + datasourceWarning: false, + hasCustomLabel: false, + label: 'SUM(a)', + optionName: 'metric_1a2b3c4d5f_1a2b3c4d5f', + }, + { + expressionType: 'SIMPLE', + column: { + advanced_data_type: null, + certification_details: null, + certified_by: null, + column_name: 'b', + description: null, + expression: null, + filterable: true, + groupby: true, + id: 2, + is_certified: false, + is_dttm: false, + python_date_format: null, + type: 'BIGINT', + type_generic: 0, + verbose_name: null, + warning_markdown: null, + }, + aggregate: 'AVG', + sqlExpression: null, + datasourceWarning: false, + hasCustomLabel: false, + label: 'AVG(b)', + optionName: 'metric_6g7h8i9j0k_6g7h8i9j0k', + }, +]; + describe('reducers', () => { it('Does not set a control value if control does not exist', () => { const newState = exploreReducer( @@ -37,4 +94,127 @@ describe('reducers', () => { expect(newState.controls.y_axis_format.value).toBe('$,.2f'); expect(newState.form_data.y_axis_format).toBe('$,.2f'); }); + it('Keeps the column config when metric column positions are swapped', () => { + const mockedState = { + ...defaultState, + controls: { + ...defaultState.controls, + metrics: { + ...defaultState.controls.metrics, + value: METRICS, + }, + column_config: { + ...defaultState.controls.column_config, + value: { + 'AVG(b)': { + currencyFormat: { + symbolPosition: 'prefix', + symbol: 'USD', + }, + }, + }, + }, + }, + form_data: { + ...defaultState.form_data, + metrics: METRICS, + column_config: { + 'AVG(b)': { + currencyFormat: { + symbolPosition: 'prefix', + symbol: 'USD', + }, + }, + }, + }, + }; + + const swappedMetrics = [METRICS[1], METRICS[0]]; + const newState = exploreReducer( + mockedState, + actions.setControlValue('metrics', swappedMetrics, []), + ); + + const expectedColumnConfig = { + 'AVG(b)': { + currencyFormat: { + symbolPosition: 'prefix', + symbol: 'USD', + }, + }, + }; + + expect(newState.controls.metrics.value).toStrictEqual(swappedMetrics); + expect(newState.form_data.metrics).toStrictEqual(swappedMetrics); + expect(newState.controls.column_config.value).toStrictEqual( + expectedColumnConfig, + ); + expect(newState.form_data.column_config).toStrictEqual( + expectedColumnConfig, + ); + }); + + it('Keeps the column config when metric column name is updated', () => { + const mockedState = { + ...defaultState, + controls: { + ...defaultState.controls, + metrics: { + ...defaultState.controls.metrics, + value: METRICS, + }, + column_config: { + ...defaultState.controls.column_config, + value: { + 'AVG(b)': { + currencyFormat: { + symbolPosition: 'prefix', + symbol: 'USD', + }, + }, + }, + }, + }, + form_data: { + ...defaultState.form_data, + metrics: METRICS, + column_config: { + 'AVG(b)': { + currencyFormat: { + symbolPosition: 'prefix', + symbol: 'USD', + }, + }, + }, + }, + }; + + const updatedMetrics = [ + METRICS[0], + { + ...METRICS[1], + hasCustomLabel: true, + label: 'AVG of b', + }, + ]; + + const newState = exploreReducer( + mockedState, + actions.setControlValue('metrics', updatedMetrics, []), + ); + + const expectedColumnConfig = { + 'AVG of b': { + currencyFormat: { + symbolPosition: 'prefix', + symbol: 'USD', + }, + }, + }; + expect(newState.controls.metrics.value).toStrictEqual(updatedMetrics); + expect(newState.form_data.metrics).toStrictEqual(updatedMetrics); + expect(newState.form_data.column_config).toStrictEqual( + expectedColumnConfig, + ); + }); }); diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/DvtVizTypeGallery.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/DvtVizTypeGallery.tsx new file mode 100644 index 0000000000000..ce2bc556b1f93 --- /dev/null +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/DvtVizTypeGallery.tsx @@ -0,0 +1,296 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import React, { useEffect, useMemo, useState } from 'react'; +import { useAppSelector } from 'src/hooks/useAppSelector'; +import { + styled, + css, + ChartMetadata, + SupersetTheme, + useTheme, +} from '@superset-ui/core'; +import { usePluginContext } from 'src/components/DynamicPlugins'; +import { nativeFilterGate } from 'src/dashboard/components/nativeFilters/utils'; +import DvtIconDataLabel from 'src/components/DvtIconDataLabel'; +import { useDispatch } from 'react-redux'; +import { dvtSidebarChartAddSetProperty } from 'src/dvt-redux/dvt-sidebarReducer'; + +interface VizTypeGalleryProps { + onChange: (vizType: string | null) => void; + onDoubleClick: () => void; + selectedViz: string | null; + className?: string; + denyList: string[]; +} + +type VizEntry = { + key: string; + value: ChartMetadata; +}; + +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', + 'time_table', + 'histogram', + 'deck_scatter', + 'deck_hex', + 'time_pivot', + 'deck_arc', + 'heatmap', + 'deck_grid', + 'deck_screengrid', + 'treemap_v2', + 'box_plot', + 'sunburst', + 'sankey', + 'word_cloud', + 'mapbox', + 'kepler', + 'cal_heatmap', + 'rose', + 'bubble', + 'bubble_v2', + 'deck_geojson', + 'horizon', + 'deck_multi', + 'compare', + 'partition', + 'event_flow', + 'deck_path', + 'graph_chart', + 'world_map', + 'paired_ttest', + 'para', + 'country_map', +]; + +const typesWithDefaultOrder = new Set(DEFAULT_ORDER); + +const THUMBNAIL_GRID_UNITS = 100; + +export const MAX_ADVISABLE_VIZ_GALLERY_WIDTH = 1090; + +export const VIZ_TYPE_CONTROL_TEST_ID = 'viz-type-control'; + +const VizPickerLayout = styled.div` + display: grid; + height: 100%; +`; + +const RightPane = styled.div``; + +const IconsPane = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 15px; + padding-top: 60px; +`; + +// overflow hidden on the details pane and overflow auto on the description +// (plus grid layout) enables the description to scroll while the header stays in place. + +const thumbnailContainerCss = (theme: SupersetTheme) => css` + cursor: pointer; + width: ${theme.gridUnit * THUMBNAIL_GRID_UNITS}px; + position: relative; + padding-bottom: 62px; + width: 547px; + + img { + border: 1px solid ${theme.colors.grayscale.light2}; + border-radius: ${theme.gridUnit}px; + transition: border-color ${theme.transitionTiming}; + background-color: transparent; + } + + &.selected img { + border: 2px solid ${theme.colors.primary.light2}; + } + + &:hover:not(.selected) img { + border: 1px solid ${theme.colors.grayscale.light1}; + } + + .viztype-label { + font-size: 18px; + font-weight: 700; + line-height: 140%; + letter-spacing: 0.2px; + padding-bottom: 62px; + } +`; + +function vizSortFactor(entry: VizEntry) { + if (typesWithDefaultOrder.has(entry.key)) { + return DEFAULT_ORDER.indexOf(entry.key); + } + return DEFAULT_ORDER.length; +} + +interface ThumbnailProps { + entry: VizEntry; + selectedViz: string | null; + setSelectedViz: (viz: string) => void; + onDoubleClick: () => void; +} + +const Thumbnail: React.FC = ({ + entry, + selectedViz, + setSelectedViz, + onDoubleClick, +}) => { + const theme = useTheme(); + const { key, value: type } = entry; + const isSelected = selectedViz === entry.key; + + return ( +
setSelectedViz(key)} + onDoubleClick={onDoubleClick} + data-test="viztype-selector-container" + > +
+ {type.name} +
+ {type.name} +
+ ); +}; + +interface ThumbnailGalleryProps { + vizEntries: VizEntry[]; + selectedViz: string | null; + setSelectedViz: (viz: string) => void; + onDoubleClick: () => void; +} + +/** A list of viz thumbnails, used within the viz picker modal */ +const ThumbnailGallery: React.FC = ({ + vizEntries, + ...props +}) => ( + + {vizEntries.map(entry => ( + + ))} + +); + +export default function VizTypeGallery(props: VizTypeGalleryProps) { + const { selectedViz, onChange, onDoubleClick, className } = props; + const { mountedPluginMetadata } = usePluginContext(); + + const chartMetadata: VizEntry[] = useMemo(() => { + const result = Object.entries(mountedPluginMetadata) + .map(([key, value]) => ({ key, value })) + .filter(({ key }) => !props.denyList.includes(key)) + .filter( + ({ value }) => + nativeFilterGate(value.behaviors || []) && !value.deprecated, + ); + result.sort((a, b) => vizSortFactor(a) - vizSortFactor(b)); + return result; + }, [mountedPluginMetadata]); + + const chartAddSelector = useAppSelector(state => state.dvtSidebar.chartAdd); + const sortedMetadata = useMemo( + () => chartMetadata.sort((a, b) => a.key.localeCompare(b.key)), + [chartMetadata], + ); + const [editedData, setEditedData] = useState(sortedMetadata); + const dispatch = useDispatch(); + + const clearAlerts = () => { + dispatch( + dvtSidebarChartAddSetProperty({ + chartAdd: { + ...chartAddSelector, + dataset: '', + recommended_tags: '', + category: '', + tags: '', + }, + }), + ); + }; + useEffect(() => { + const filteredData = chartMetadata.filter( + (item: any) => + (chartAddSelector.category + ? item.value.category === chartAddSelector.category + : true) && + (chartAddSelector.tags + ? item.value.tags.includes(chartAddSelector.tags) + : true), + ); + setEditedData(filteredData); + }, [chartAddSelector]); + + return editedData.length > 0 ? ( + + + + + + ) : ( + { + clearAlerts(); + }} + /> + ); +} diff --git a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx index 2563dba01cb7a..2d14376516931 100644 --- a/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx +++ b/superset-frontend/src/explore/components/controls/VizTypeControl/VizTypeGallery.tsx @@ -849,10 +849,18 @@ export default function VizTypeGallery(props: VizTypeGalleryProps) { grid-area: examples-header; `} > - {!!selectedVizMetadata?.exampleGallery?.length && t('Examples')} + {t('Examples')} - {(selectedVizMetadata?.exampleGallery || []).map(example => ( + {(selectedVizMetadata?.exampleGallery?.length + ? selectedVizMetadata.exampleGallery + : [ + { + url: selectedVizMetadata?.thumbnail, + caption: selectedVizMetadata?.name, + }, + ] + ).map(example => ( { + const itemExist = old_metrics_data.some( + oldItem => oldItem?.label === item?.label, + ); + if ( + !itemExist && item?.label !== old_metrics_data[index]?.label && !!new_column_config[old_metrics_data[index]?.label] ) { diff --git a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx index b38d44a710b78..84d19bae69504 100644 --- a/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx +++ b/superset-frontend/src/features/reports/ReportModal/HeaderReportDropdown/index.tsx @@ -191,6 +191,12 @@ export default function HeaderReportDropDown({ const showReportSubMenu = report && setShowReportSubMenu && canAddReports(); + // @z-index-below-dashboard-header (100) - 1 = 99 + const dropdownOverlayStyle = { + zIndex: 99, + animationDuration: '0s', + }; + useEffect(() => { if (showReportSubMenu) { setShowReportSubMenu(true); @@ -288,6 +294,7 @@ export default function HeaderReportDropDown({ <> triggerNode.closest('.action-button') diff --git a/superset-frontend/src/pages/DvtChartAdd/dvt-chart-add.module.tsx b/superset-frontend/src/pages/DvtChartAdd/dvt-chart-add.module.tsx new file mode 100644 index 0000000000000..27115c52649d5 --- /dev/null +++ b/superset-frontend/src/pages/DvtChartAdd/dvt-chart-add.module.tsx @@ -0,0 +1,29 @@ +import { styled } from '@superset-ui/core'; + +const StyledChartAdd = styled.div` + display: flex; + flex-direction: column; + position: relative; + padding: 15px; +`; + +const StyledChart = styled.div` + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 30px; + height: 100%; +`; + +const StyledImgRender = styled.div` + display: flex; + flex-direction: column; + gap: 22px; + font-size: 18px; + font-weight: 700; + line-height: 140%; + letter-spacing: 0.2px; + max-width: 547px; +`; + +export { StyledChartAdd, StyledChart, StyledImgRender }; diff --git a/superset-frontend/src/pages/DvtChartAdd/index.tsx b/superset-frontend/src/pages/DvtChartAdd/index.tsx new file mode 100644 index 0000000000000..3026c613be325 --- /dev/null +++ b/superset-frontend/src/pages/DvtChartAdd/index.tsx @@ -0,0 +1,116 @@ +import React, { useState, useEffect } from 'react'; +import rison from 'rison'; +import querystring from 'query-string'; +import { + isFeatureEnabled, + FeatureFlag, + JsonResponse, + SupersetClient, + t, +} from '@superset-ui/core'; +import { RouteComponentProps } from 'react-router-dom'; +import withToasts from 'src/components/MessageToasts/withToasts'; +import { findPermission } from 'src/utils/findPermission'; +import { UserWithPermissionsAndRoles } from 'src/types/bootstrapTypes'; +import getBootstrapData from 'src/utils/getBootstrapData'; +import { + Dataset, + DatasetSelectLabel, +} from 'src/features/datasets/DatasetSelectLabel'; +import DvtVizTypeGallery from 'src/explore/components/controls/VizTypeControl/DvtVizTypeGallery'; + +interface ChartCreationProps extends RouteComponentProps { + user: UserWithPermissionsAndRoles; + addSuccessToast: (arg: string) => void; +} + +interface ChartCreationState { + datasource?: { label: string; value: string }; + datasetName?: string | string[] | null; + vizType: string | null; + canCreateDataset: boolean; +} + +const bootstrapData = getBootstrapData(); +const denyList: string[] = bootstrapData.common.conf.VIZ_TYPE_DENYLIST || []; + +if ( + isFeatureEnabled(FeatureFlag.DASHBOARD_NATIVE_FILTERS) && + !denyList.includes('filter_box') +) { + denyList.push('filter_box'); +} + +const DvtChartAdd: React.FC = ({ + user, + addSuccessToast, +}) => { + const [chart, setChart] = useState({ + vizType: null, + canCreateDataset: findPermission('can_write', 'Dataset', user.roles), + }); + + useEffect(() => { + const params = querystring.parse(window.location.search)?.dataset as string; + if (params) { + loadDatasources(params, 0, 1).then(r => { + const datasource = r.data[0]; + datasource.label = datasource.customLabel; + setChart(prevState => ({ ...prevState, datasource })); + }); + addSuccessToast(t('The dataset has been saved')); + } + }, []); + + const changeVizType = (vizType: string | null) => { + setChart(prevState => ({ ...prevState, vizType })); + }; + + const loadDatasources = (search: string, page: number, pageSize: number) => { + const query = rison.encode({ + columns: [ + 'id', + 'table_name', + 'datasource_type', + 'database.database_name', + 'schema', + ], + filters: [{ col: 'table_name', opr: 'ct', value: search }], + page, + page_size: pageSize, + order_column: 'table_name', + order_direction: 'asc', + }); + return SupersetClient.get({ + endpoint: `/api/v1/dataset/?q=${query}`, + }).then((response: JsonResponse) => { + const list: { + customLabel: string; + id: number; + label: string; + value: string; + }[] = response.json.result.map((item: Dataset) => ({ + id: item.id, + value: `${item.id}__${item.datasource_type}`, + customLabel: DatasetSelectLabel(item), + label: item.table_name, + })); + return { + data: list, + totalCount: response.json.count, + }; + }); + }; + + return ( + {}} + selectedViz={chart.vizType} + /> + ); +}; + +export default withToasts(DvtChartAdd); diff --git a/superset-frontend/src/pages/DvtDashboardList/index.tsx b/superset-frontend/src/pages/DvtDashboardList/index.tsx index 5c19fc4e5914b..646ef52faa227 100644 --- a/superset-frontend/src/pages/DvtDashboardList/index.tsx +++ b/superset-frontend/src/pages/DvtDashboardList/index.tsx @@ -37,8 +37,20 @@ import { } from './dvtdashboardlist.module'; const headerData = [ - { id: 1, title: 'Title', field: 'dashboard_title', flex: 3, checkbox: true }, - { id: 2, title: 'Modified By', field: 'changed_by_name' }, + { + id: 1, + title: 'Title', + field: 'dashboard_title', + flex: 3, + checkbox: true, + urlField: 'url', + }, + { + id: 2, + title: 'Modified By', + field: 'changed_by_name', + urlField: 'changed_by_url', + }, { id: 3, title: 'Status', field: 'status' }, { id: 4, title: 'Modified', field: 'created_on_delta_humanized' }, { id: 5, title: 'Created By', field: 'createdbyName' }, @@ -46,6 +58,7 @@ const headerData = [ { id: 7, title: 'Action', + showHover: true, clicks: [ { icon: 'edit_alt', @@ -85,11 +98,7 @@ function DvtDashboardList() { const data = await response.json(); setData( data.result.map((item: any) => ({ - id: item.id, - dashboard_title: item.dashboard_title, - changed_by_name: item.changed_by_name, - status: item.status, - created_on_delta_humanized: item.created_on_delta_humanized, + ...item, owners: item.owners.length ? item.owners .map( diff --git a/superset-frontend/src/pages/DvtDatasets/dvt-datasets.module.tsx b/superset-frontend/src/pages/DvtDatasets/dvt-datasets.module.tsx new file mode 100644 index 0000000000000..4e7ef0e4bf5cc --- /dev/null +++ b/superset-frontend/src/pages/DvtDatasets/dvt-datasets.module.tsx @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +import { styled } from '@superset-ui/core'; + +const StyledDvtDatasets = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledButtons = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +`; +const StyledSelected = styled.div` + display: flex; + align-items: center; +`; + +const StyledDeselect = styled.div` + display: flex; + align-items: center; +`; +const StyledDeselectButton = styled.div` + display: flex; + padding: 15px; + gap: 5px; + flex-direction: row; + align-items: center; +`; + +export { + StyledDvtDatasets, + StyledButtons, + StyledSelected, + StyledDeselect, + StyledDeselectButton, +}; diff --git a/superset-frontend/src/pages/DvtDatasets/index.tsx b/superset-frontend/src/pages/DvtDatasets/index.tsx new file mode 100644 index 0000000000000..9eeb6d18cf689 --- /dev/null +++ b/superset-frontend/src/pages/DvtDatasets/index.tsx @@ -0,0 +1,141 @@ +import React, { useEffect, useState } from 'react'; +import moment from 'moment'; +import DvtButton from 'src/components/DvtButton'; +import DvtPagination from 'src/components/DvtPagination'; +import DvtTable from 'src/components/DvtTable'; +import { + StyledButtons, + StyledDeselect, + StyledDeselectButton, + StyledDvtDatasets, + StyledSelected, +} from './dvt-datasets.module'; + +const header = [ + { + id: 1, + title: 'Name', + field: 'table_name', + flex: 3, + checkbox: true, + folderIcon: true, + onLink: true, + }, + { id: 2, title: 'Type', field: 'kind' }, + { id: 3, title: 'Database', field: 'database' }, + { id: 4, title: 'Schema', field: 'schema' }, + { id: 5, title: 'Modified Date', field: 'changed_on_utc' }, + { id: 6, title: 'Modified by', field: 'changed_by_name' }, + { id: 7, title: 'Owners', field: 'owners' }, + { + id: 8, + title: 'Actions', + clicks: [ + { + icon: 'edit_alt', + click: () => {}, + popperLabel: 'Edit', + }, + { + icon: 'share', + click: () => {}, + popperLabel: 'Export', + }, + { + icon: 'trash', + click: () => {}, + popperLabel: 'Delete', + }, + ], + }, +]; + +function DvtDatasets() { + const [currentPage, setCurrentPage] = useState(1); + const [data, setData] = useState([]); + const [count, setCount] = useState(0); + const [selectedRows, setSelectedRows] = useState([]); + + useEffect(() => { + const apiUrl = `/api/v1/dataset/?q=(order_column:changed_on_delta_humanized,order_direction:desc,page:${ + currentPage - 1 + },page_size:10)`; + + const fetchApi = async () => { + try { + const response = await fetch(apiUrl); + const data = await response.json(); + setData( + data.result.map((item: any) => ({ + ...item, + database: `${item.database.database_name}`, + changed_on_utc: moment(item.changed_on_utc).fromNow(), + owners: item.owners.length + ? item.owners + .map( + (item: { first_name: string; last_name: string }) => + `${item.first_name} ${item.last_name}`, + ) + .join(',') + : '', + })), + ); + setCount(data.count); + } catch (error) { + console.log('Error:', error); + } + }; + fetchApi(); + setSelectedRows([]); + }, [currentPage]); + + const handleDeselectAll = () => { + setSelectedRows([]); + }; + + return ( + + + + {`${selectedRows.length} Selected`} + + + + + +
+ +
+ + {}} + colour="grayscale" + typeColour="basic" + size="small" + /> + + +
+ ); +} + +export default DvtDatasets; diff --git a/superset-frontend/src/views/dvt-routes.tsx b/superset-frontend/src/views/dvt-routes.tsx index 7675982d6a7ba..4de257f190c89 100644 --- a/superset-frontend/src/views/dvt-routes.tsx +++ b/superset-frontend/src/views/dvt-routes.tsx @@ -23,8 +23,7 @@ import React, { lazy } from 'react'; import DvtHome from 'src/pages/DvtHome'; const ChartCreation = lazy( - () => - import(/* webpackChunkName: "ChartCreation" */ 'src/pages/ChartCreation'), + () => import(/* webpackChunkName: "ChartCreation" */ 'src/pages/DvtChartAdd'), ); const AnnotationLayerList = lazy( @@ -76,7 +75,7 @@ const DatabaseList = lazy( ); const DatasetList = lazy( - () => import(/* webpackChunkName: "DatasetList" */ 'src/pages/DatasetList'), + () => import(/* webpackChunkName: "DatasetList" */ 'src/pages/DvtDatasets'), ); const DatasetCreation = lazy( diff --git a/superset-websocket/package-lock.json b/superset-websocket/package-lock.json index c802b5d66543e..890fd3ec769b1 100644 --- a/superset-websocket/package-lock.json +++ b/superset-websocket/package-lock.json @@ -24,7 +24,7 @@ "@types/ioredis": "^4.27.8", "@types/jest": "^27.0.2", "@types/jsonwebtoken": "^9.0.5", - "@types/node": "^20.10.6", + "@types/node": "^20.10.7", "@types/uuid": "^9.0.7", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^5.61.0", @@ -1429,9 +1429,9 @@ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" }, "node_modules/@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "version": "20.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz", + "integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -7283,9 +7283,9 @@ "integrity": "sha512-OvlIYQK9tNneDlS0VN54LLd5uiPCBOp7gS5Z0f1mjoJYBrtStzgmJBxONW3U6OZqdtNzZPmn9BS/7WI7BFFcFQ==" }, "@types/node": { - "version": "20.10.6", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", - "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "version": "20.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.7.tgz", + "integrity": "sha512-fRbIKb8C/Y2lXxB5eVMj4IU7xpdox0Lh8bUPEdtLysaylsml1hOOx1+STloRs/B9nf7C6kPRmmg/V7aQW7usNg==", "dev": true, "requires": { "undici-types": "~5.26.4" diff --git a/superset-websocket/package.json b/superset-websocket/package.json index 34c5d5b913ba6..53f5f02225496 100644 --- a/superset-websocket/package.json +++ b/superset-websocket/package.json @@ -31,7 +31,7 @@ "@types/ioredis": "^4.27.8", "@types/jest": "^27.0.2", "@types/jsonwebtoken": "^9.0.5", - "@types/node": "^20.10.6", + "@types/node": "^20.10.7", "@types/uuid": "^9.0.7", "@types/ws": "^8.5.10", "@typescript-eslint/eslint-plugin": "^5.61.0", diff --git a/superset/async_events/async_query_manager.py b/superset/async_events/async_query_manager.py index 94941541fb4f9..32cf247cf304f 100644 --- a/superset/async_events/async_query_manager.py +++ b/superset/async_events/async_query_manager.py @@ -191,9 +191,14 @@ def submit_explore_json_job( force: Optional[bool] = False, user_id: Optional[int] = None, ) -> dict[str, Any]: + # pylint: disable=import-outside-toplevel + from superset import security_manager + job_metadata = self.init_job(channel_id, user_id) self._load_explore_json_into_cache_job.delay( - job_metadata, + {**job_metadata, "guest_token": guest_user.guest_token} + if (guest_user := security_manager.get_current_guest_user_if_guest()) + else job_metadata, form_data, response_type, force, @@ -201,10 +206,25 @@ def submit_explore_json_job( return job_metadata def submit_chart_data_job( - self, channel_id: str, form_data: dict[str, Any], user_id: Optional[int] + self, + channel_id: str, + form_data: dict[str, Any], + user_id: Optional[int] = None, ) -> dict[str, Any]: + # pylint: disable=import-outside-toplevel + from superset import security_manager + + # if it's guest user, we want to pass the guest token to the celery task + # chart data cache key is calculated based on the current user + # this way we can keep the cache key consistent between sync and async command + # so that it can be looked up consistently job_metadata = self.init_job(channel_id, user_id) - self._load_chart_data_into_cache_job.delay(job_metadata, form_data) + self._load_chart_data_into_cache_job.delay( + {**job_metadata, "guest_token": guest_user.guest_token} + if (guest_user := security_manager.get_current_guest_user_if_guest()) + else job_metadata, + form_data, + ) return job_metadata def read_events( diff --git a/superset/common/query_context_processor.py b/superset/common/query_context_processor.py index 5b1414d53b396..d8b5bea4bb729 100644 --- a/superset/common/query_context_processor.py +++ b/superset/common/query_context_processor.py @@ -600,7 +600,15 @@ def get_payload( set_and_log_cache( cache_manager.cache, cache_key, - {"data": self._query_context.cache_values}, + { + "data": { + # setting form_data into query context cache value as well + # so that it can be used to reconstruct form_data field + # for query context object when reading from cache + "form_data": self._query_context.form_data, + **self._query_context.cache_values, + }, + }, self.get_cache_timeout(), ) return_value["cache_key"] = cache_key # type: ignore diff --git a/superset/result_set.py b/superset/result_set.py index 82832eb8ea4ac..5483271035031 100644 --- a/superset/result_set.py +++ b/superset/result_set.py @@ -29,6 +29,7 @@ from superset.db_engine_specs import BaseEngineSpec from superset.superset_typing import DbapiDescription, DbapiResult, ResultSetColumnType from superset.utils import core as utils +from superset.utils.core import GenericDataType logger = logging.getLogger(__name__) @@ -222,6 +223,18 @@ def is_temporal(self, db_type_str: Optional[str]) -> bool: return False return column_spec.is_dttm + def type_generic( + self, db_type_str: Optional[str] + ) -> Optional[utils.GenericDataType]: + column_spec = self.db_engine_spec.get_column_spec(db_type_str) + if column_spec is None: + return None + + if column_spec.is_dttm: + return GenericDataType.TEMPORAL + + return column_spec.generic_type + def data_type(self, col_name: str, pa_dtype: pa.DataType) -> Optional[str]: """Given a pyarrow data type, Returns a generic database type""" if set_type := self._type_dict.get(col_name): @@ -255,7 +268,8 @@ def columns(self) -> list[ResultSetColumnType]: "column_name": col.name, "name": col.name, "type": db_type_str, - "is_dttm": self.is_temporal(db_type_str), + "type_generic": self.type_generic(db_type_str), + "is_dttm": self.is_temporal(db_type_str) or False, } columns.append(column) return columns diff --git a/superset/tasks/async_queries.py b/superset/tasks/async_queries.py index 61970ca1f3801..b804847cd84e3 100644 --- a/superset/tasks/async_queries.py +++ b/superset/tasks/async_queries.py @@ -22,6 +22,7 @@ from celery.exceptions import SoftTimeLimitExceeded from flask import current_app, g +from flask_appbuilder.security.sqla.models import User from marshmallow import ValidationError from superset.charts.schemas import ChartDataQueryContextSchema @@ -58,6 +59,20 @@ def _create_query_context_from_form(form_data: dict[str, Any]) -> QueryContext: raise error +def _load_user_from_job_metadata(job_metadata: dict[str, Any]) -> User: + if user_id := job_metadata.get("user_id"): + # logged in user + user = security_manager.get_user_by_id(user_id) + elif guest_token := job_metadata.get("guest_token"): + # embedded guest user + user = security_manager.get_guest_user_from_token(guest_token) + del job_metadata["guest_token"] + else: + # default to anonymous user if no user is found + user = security_manager.get_anonymous_user() + return user + + @celery_app.task(name="load_chart_data_into_cache", soft_time_limit=query_timeout) def load_chart_data_into_cache( job_metadata: dict[str, Any], @@ -66,12 +81,7 @@ def load_chart_data_into_cache( # pylint: disable=import-outside-toplevel from superset.commands.chart.data.get_data_command import ChartDataCommand - user = ( - security_manager.get_user_by_id(job_metadata.get("user_id")) - or security_manager.get_anonymous_user() - ) - - with override_user(user, force=False): + with override_user(_load_user_from_job_metadata(job_metadata), force=False): try: set_form_data(form_data) query_context = _create_query_context_from_form(form_data) @@ -106,12 +116,7 @@ def load_explore_json_into_cache( # pylint: disable=too-many-locals ) -> None: cache_key_prefix = "ejr-" # ejr: explore_json request - user = ( - security_manager.get_user_by_id(job_metadata.get("user_id")) - or security_manager.get_anonymous_user() - ) - - with override_user(user, force=False): + with override_user(_load_user_from_job_metadata(job_metadata), force=False): try: set_form_data(form_data) datasource_id, datasource_type = get_datasource_info(None, None, form_data) @@ -140,7 +145,13 @@ def load_explore_json_into_cache( # pylint: disable=too-many-locals "response_type": response_type, } cache_key = generate_cache_key(cache_value, cache_key_prefix) - set_and_log_cache(cache_manager.cache, cache_key, cache_value) + cache_instance = cache_manager.cache + cache_timeout = ( + cache_instance.cache.default_timeout if cache_instance.cache else None + ) + set_and_log_cache( + cache_instance, cache_key, cache_value, cache_timeout=cache_timeout + ) result_url = f"/superset/explore_json/data/{cache_key}" async_query_manager.update_job( job_metadata, diff --git a/superset/templates/superset/login.html b/superset/templates/superset/login.html index 22d6f9101d361..742096545a0dd 100644 --- a/superset/templates/superset/login.html +++ b/superset/templates/superset/login.html @@ -245,7 +245,7 @@ /> -

AppName

+

Data Visualization Tool

@@ -260,39 +260,39 @@

Reporting tool

-
- - - -
-
- - - -
-
- - - -
+{#
#} +{# #} +{# #} +{# #} +{#
#} +{#
#} +{# #} +{# #} +{# #} +{#
#} +{#
#} +{# #} +{# #} +{# #} +{#
#}
@@ -377,9 +377,9 @@

Sign In to your Account

Remember me - +{#
#} +{# Forgot Password?#} +{#
#} {% if appbuilder.sm.auth_user_registration %} diff --git a/superset/translations/zh/LC_MESSAGES/messages.po b/superset/translations/zh/LC_MESSAGES/messages.po index e21a83f32b05c..791ea2c4db7be 100644 --- a/superset/translations/zh/LC_MESSAGES/messages.po +++ b/superset/translations/zh/LC_MESSAGES/messages.po @@ -3723,12 +3723,12 @@ msgstr "清除" #: superset-frontend/src/dashboard/components/nativeFilters/FilterBar/ActionButtons/index.tsx:152 msgid "Clear all" -msgstr "清楚所有" +msgstr "清除所有" #: superset-frontend/src/components/Table/index.tsx:210 #, fuzzy msgid "Clear all data" -msgstr "清楚所有" +msgstr "清除所有" #: superset-frontend/src/explore/components/ControlPanelsContainer.tsx:675 #, fuzzy @@ -19600,7 +19600,7 @@ msgstr "无法更新您的查询" #: superset-frontend/plugins/legacy-plugin-chart-horizon/src/controlPanel.ts:86 #, fuzzy msgid "overall" -msgstr "清楚所有" +msgstr "清除所有" #: superset-frontend/plugins/legacy-plugin-chart-paired-t-test/src/controlPanel.ts:77 msgid "p-value precision" diff --git a/tests/integration_tests/datasets/api_tests.py b/tests/integration_tests/datasets/api_tests.py index d969895489d9f..3530bdec1a23c 100644 --- a/tests/integration_tests/datasets/api_tests.py +++ b/tests/integration_tests/datasets/api_tests.py @@ -114,10 +114,6 @@ def get_fixture_virtual_datasets(self) -> list[SqlaTable]: @pytest.fixture() def create_virtual_datasets(self): with self.create_app().app_context(): - if backend() == "sqlite": - yield - return - datasets = [] admin = self.get_user("admin") main_db = get_main_database() @@ -140,10 +136,6 @@ def create_virtual_datasets(self): @pytest.fixture() def create_datasets(self): with self.create_app().app_context(): - if backend() == "sqlite": - yield - return - datasets = [] admin = self.get_user("admin") main_db = get_main_database() @@ -192,8 +184,6 @@ def test_get_dataset_list(self): """ Dataset API: Test get dataset list """ - if backend() == "sqlite": - return example_db = get_example_database() self.login(username="admin") @@ -232,8 +222,6 @@ def test_get_dataset_list_gamma(self): """ Dataset API: Test get dataset list gamma """ - if backend() == "sqlite": - return self.login(username="gamma") uri = "api/v1/dataset/" @@ -246,8 +234,6 @@ def test_get_dataset_list_gamma_has_database_access(self): """ Dataset API: Test get dataset list with database access """ - if backend() == "sqlite": - return self.login(username="gamma") @@ -289,8 +275,6 @@ def test_get_dataset_related_database_gamma(self): """ Dataset API: Test get dataset related databases gamma """ - if backend() == "sqlite": - return # Add main database access to gamma role main_db = get_main_database() @@ -320,8 +304,6 @@ def test_get_dataset_item(self): """ Dataset API: Test get dataset item """ - if backend() == "sqlite": - return table = self.get_energy_usage_dataset() main_db = get_main_database() @@ -385,8 +367,6 @@ def test_get_dataset_distinct_schema(self): """ Dataset API: Test get dataset distinct schema """ - if backend() == "sqlite": - return def pg_test_query_parameter(query_parameter, expected_response): uri = f"api/v1/dataset/distinct/schema?q={prison.dumps(query_parameter)}" @@ -459,8 +439,6 @@ def test_get_dataset_distinct_not_allowed(self): """ Dataset API: Test get dataset distinct not allowed """ - if backend() == "sqlite": - return self.login(username="admin") uri = "api/v1/dataset/distinct/table_name" @@ -471,8 +449,6 @@ def test_get_dataset_distinct_gamma(self): """ Dataset API: Test get dataset distinct with gamma """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() @@ -491,8 +467,6 @@ def test_get_dataset_info(self): """ Dataset API: Test get dataset info """ - if backend() == "sqlite": - return self.login(username="admin") uri = "api/v1/dataset/_info" @@ -503,8 +477,6 @@ def test_info_security_dataset(self): """ Dataset API: Test info security """ - if backend() == "sqlite": - return self.login(username="admin") params = {"keys": ["permissions"]} @@ -525,8 +497,6 @@ def test_create_dataset_item(self): """ Dataset API: Test create dataset item """ - if backend() == "sqlite": - return main_db = get_main_database() self.login(username="admin") @@ -572,8 +542,6 @@ def test_create_dataset_item_normalize(self): """ Dataset API: Test create dataset item with column normalization enabled """ - if backend() == "sqlite": - return main_db = get_main_database() self.login(username="admin") @@ -601,8 +569,6 @@ def test_create_dataset_item_gamma(self): """ Dataset API: Test create dataset item gamma """ - if backend() == "sqlite": - return self.login(username="gamma") main_db = get_main_database() @@ -619,8 +585,6 @@ def test_create_dataset_item_owner(self): """ Dataset API: Test create item owner """ - if backend() == "sqlite": - return main_db = get_main_database() self.login(username="alpha") @@ -647,8 +611,6 @@ def test_create_dataset_item_owners_invalid(self): """ Dataset API: Test create dataset item owner invalid """ - if backend() == "sqlite": - return admin = self.get_user("admin") main_db = get_main_database() @@ -671,8 +633,6 @@ def test_create_dataset_validate_uniqueness(self): """ Dataset API: Test create dataset validate table uniqueness """ - if backend() == "sqlite": - return energy_usage_ds = self.get_energy_usage_dataset() self.login(username="admin") @@ -694,8 +654,6 @@ def test_create_dataset_with_sql_validate_uniqueness(self): """ Dataset API: Test create dataset with sql """ - if backend() == "sqlite": - return energy_usage_ds = self.get_energy_usage_dataset() self.login(username="admin") @@ -718,8 +676,6 @@ def test_create_dataset_with_sql(self): """ Dataset API: Test create dataset with sql """ - if backend() == "sqlite": - return energy_usage_ds = self.get_energy_usage_dataset() self.login(username="alpha") @@ -777,8 +733,6 @@ def test_create_dataset_validate_database(self): """ Dataset API: Test create dataset validate database exists """ - if backend() == "sqlite": - return self.login(username="admin") dataset_data = {"database": 1000, "schema": "", "table_name": "birth_names"} @@ -792,8 +746,6 @@ def test_create_dataset_validate_tables_exists(self): """ Dataset API: Test create dataset validate table exists """ - if backend() == "sqlite": - return example_db = get_example_database() self.login(username="admin") @@ -820,8 +772,6 @@ def test_create_dataset_validate_view_exists( """ Dataset API: Test create dataset validate view exists """ - if backend() == "sqlite": - return mock_get_columns.return_value = [ { @@ -868,8 +818,6 @@ def test_create_dataset_sqlalchemy_error(self, mock_dao_create): """ Dataset API: Test create dataset sqlalchemy error """ - if backend() == "sqlite": - return mock_dao_create.side_effect = DAOCreateFailedError() self.login(username="admin") @@ -889,8 +837,6 @@ def test_update_dataset_item(self): """ Dataset API: Test update dataset item """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="admin") @@ -908,8 +854,6 @@ def test_update_dataset_item_w_override_columns(self): """ Dataset API: Test update dataset with override columns """ - if backend() == "sqlite": - return # Add default dataset dataset = self.insert_default_dataset() @@ -947,8 +891,6 @@ def test_update_dataset_item_w_override_columns_same_columns(self): """ Dataset API: Test update dataset with override columns """ - if backend() == "sqlite": - return # Add default dataset main_db = get_main_database() @@ -997,8 +939,6 @@ def test_update_dataset_create_column_and_metric(self): """ Dataset API: Test update dataset create column """ - if backend() == "sqlite": - return # create example dataset by Command dataset = self.insert_default_dataset() @@ -1095,8 +1035,6 @@ def test_update_dataset_delete_column(self): """ Dataset API: Test update dataset delete column """ - if backend() == "sqlite": - return # create example dataset by Command dataset = self.insert_default_dataset() @@ -1147,8 +1085,6 @@ def test_update_dataset_update_column(self): """ Dataset API: Test update dataset columns """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() @@ -1186,8 +1122,6 @@ def test_update_dataset_delete_metric(self): """ Dataset API: Test update dataset delete metric """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() metrics_query = ( @@ -1232,8 +1166,6 @@ def test_update_dataset_update_column_uniqueness(self): """ Dataset API: Test update dataset columns uniqueness """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() @@ -1255,8 +1187,6 @@ def test_update_dataset_update_metric_uniqueness(self): """ Dataset API: Test update dataset metric uniqueness """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() @@ -1278,8 +1208,6 @@ def test_update_dataset_update_column_duplicate(self): """ Dataset API: Test update dataset columns duplicate """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() @@ -1306,8 +1234,6 @@ def test_update_dataset_update_metric_duplicate(self): """ Dataset API: Test update dataset metric duplicate """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() @@ -1334,8 +1260,6 @@ def test_update_dataset_item_gamma(self): """ Dataset API: Test update dataset item gamma """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="gamma") @@ -1350,8 +1274,6 @@ def test_dataset_get_list_no_username(self): """ Dataset API: Tests that no username is returned """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="admin") @@ -1374,8 +1296,6 @@ def test_dataset_get_no_username(self): """ Dataset API: Tests that no username is returned """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="admin") @@ -1397,8 +1317,6 @@ def test_update_dataset_item_not_owned(self): """ Dataset API: Test update dataset item not owned """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="alpha") @@ -1413,8 +1331,6 @@ def test_update_dataset_item_owners_invalid(self): """ Dataset API: Test update dataset item owner invalid """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="admin") @@ -1429,8 +1345,6 @@ def test_update_dataset_item_uniqueness(self): """ Dataset API: Test update dataset uniqueness """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="admin") @@ -1455,8 +1369,6 @@ def test_update_dataset_sqlalchemy_error(self, mock_dao_update): """ Dataset API: Test update dataset sqlalchemy error """ - if backend() == "sqlite": - return mock_dao_update.side_effect = DAOUpdateFailedError() @@ -1476,8 +1388,6 @@ def test_delete_dataset_item(self): """ Dataset API: Test delete dataset item """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() view_menu = security_manager.find_view_menu(dataset.get_perm()) @@ -1496,8 +1406,6 @@ def test_delete_item_dataset_not_owned(self): """ Dataset API: Test delete item not owned """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="alpha") @@ -1511,8 +1419,6 @@ def test_delete_dataset_item_not_authorized(self): """ Dataset API: Test delete item not authorized """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="gamma") @@ -1527,8 +1433,6 @@ def test_delete_dataset_sqlalchemy_error(self, mock_dao_delete): """ Dataset API: Test delete dataset sqlalchemy error """ - if backend() == "sqlite": - return mock_dao_delete.side_effect = DAODeleteFailedError() @@ -1547,8 +1451,6 @@ def test_delete_dataset_column(self): """ Dataset API: Test delete dataset column """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] column_id = dataset.columns[0].id @@ -1563,8 +1465,6 @@ def test_delete_dataset_column_not_found(self): """ Dataset API: Test delete dataset column not found """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] non_id = self.get_nonexistent_numeric_id(TableColumn) @@ -1587,8 +1487,6 @@ def test_delete_dataset_column_not_owned(self): """ Dataset API: Test delete dataset column not owned """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] column_id = dataset.columns[0].id @@ -1604,8 +1502,6 @@ def test_delete_dataset_column_fail(self, mock_dao_delete): """ Dataset API: Test delete dataset column """ - if backend() == "sqlite": - return mock_dao_delete.side_effect = DAODeleteFailedError() dataset = self.get_fixture_datasets()[0] @@ -1622,8 +1518,6 @@ def test_delete_dataset_metric(self): """ Dataset API: Test delete dataset metric """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] test_metric = SqlMetric( @@ -1643,8 +1537,6 @@ def test_delete_dataset_metric_not_found(self): """ Dataset API: Test delete dataset metric not found """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] non_id = self.get_nonexistent_numeric_id(SqlMetric) @@ -1667,8 +1559,6 @@ def test_delete_dataset_metric_not_owned(self): """ Dataset API: Test delete dataset metric not owned """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] metric_id = dataset.metrics[0].id @@ -1684,8 +1574,6 @@ def test_delete_dataset_metric_fail(self, mock_dao_delete): """ Dataset API: Test delete dataset metric """ - if backend() == "sqlite": - return mock_dao_delete.side_effect = DAODeleteFailedError() dataset = self.get_fixture_datasets()[0] @@ -1702,8 +1590,6 @@ def test_bulk_delete_dataset_items(self): """ Dataset API: Test bulk delete dataset items """ - if backend() == "sqlite": - return datasets = self.get_fixture_datasets() dataset_ids = [dataset.id for dataset in datasets] @@ -1734,8 +1620,6 @@ def test_bulk_delete_item_dataset_not_owned(self): """ Dataset API: Test bulk delete item not owned """ - if backend() == "sqlite": - return datasets = self.get_fixture_datasets() dataset_ids = [dataset.id for dataset in datasets] @@ -1750,8 +1634,6 @@ def test_bulk_delete_item_not_found(self): """ Dataset API: Test bulk delete item not found """ - if backend() == "sqlite": - return datasets = self.get_fixture_datasets() dataset_ids = [dataset.id for dataset in datasets] @@ -1767,8 +1649,6 @@ def test_bulk_delete_dataset_item_not_authorized(self): """ Dataset API: Test bulk delete item not authorized """ - if backend() == "sqlite": - return datasets = self.get_fixture_datasets() dataset_ids = [dataset.id for dataset in datasets] @@ -1783,8 +1663,6 @@ def test_bulk_delete_dataset_item_incorrect(self): """ Dataset API: Test bulk delete item incorrect request """ - if backend() == "sqlite": - return datasets = self.get_fixture_datasets() dataset_ids = [dataset.id for dataset in datasets] @@ -1799,8 +1677,6 @@ def test_dataset_item_refresh(self): """ Dataset API: Test item refresh """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() # delete a column @@ -1830,8 +1706,6 @@ def test_dataset_item_refresh_not_found(self): """ Dataset API: Test item refresh not found dataset """ - if backend() == "sqlite": - return max_id = db.session.query(func.max(SqlaTable.id)).scalar() @@ -1844,8 +1718,6 @@ def test_dataset_item_refresh_not_owned(self): """ Dataset API: Test item refresh not owned dataset """ - if backend() == "sqlite": - return dataset = self.insert_default_dataset() self.login(username="alpha") @@ -1861,8 +1733,6 @@ def test_export_dataset(self): """ Dataset API: Test export dataset """ - if backend() == "sqlite": - return birth_names_dataset = self.get_birth_names_dataset() # TODO: fix test for presto @@ -1896,8 +1766,6 @@ def test_export_dataset_not_found(self): """ Dataset API: Test export dataset not found """ - if backend() == "sqlite": - return max_id = db.session.query(func.max(SqlaTable.id)).scalar() # Just one does not exist and we get 404 @@ -1912,8 +1780,6 @@ def test_export_dataset_gamma(self): """ Dataset API: Test export dataset has gamma """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] @@ -1943,8 +1809,6 @@ def test_export_dataset_bundle(self): """ Dataset API: Test export dataset """ - if backend() == "sqlite": - return birth_names_dataset = self.get_birth_names_dataset() # TODO: fix test for presto @@ -1967,8 +1831,6 @@ def test_export_dataset_bundle_not_found(self): """ Dataset API: Test export dataset not found """ - if backend() == "sqlite": - return # Just one does not exist and we get 404 argument = [-1, 1] @@ -1983,8 +1845,6 @@ def test_export_dataset_bundle_gamma(self): """ Dataset API: Test export dataset has gamma """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] @@ -2003,8 +1863,6 @@ def test_get_dataset_related_objects(self): Dataset API: Test get chart and dashboard count related to a dataset :return: """ - if backend() == "sqlite": - return self.login(username="admin") table = self.get_birth_names_dataset() @@ -2019,8 +1877,6 @@ def test_get_dataset_related_objects_not_found(self): """ Dataset API: Test related objects not found """ - if backend() == "sqlite": - return max_id = db.session.query(func.max(SqlaTable.id)).scalar() # id does not exist and we get 404 @@ -2042,8 +1898,6 @@ def test_get_datasets_custom_filter_sql(self): """ Dataset API: Test custom dataset_is_null_or_empty filter for sql """ - if backend() == "sqlite": - return arguments = { "filters": [ @@ -2078,8 +1932,6 @@ def test_import_dataset(self): """ Dataset API: Test import dataset """ - if backend() == "sqlite": - return self.login(username="admin") uri = "api/v1/dataset/import/" @@ -2113,9 +1965,6 @@ def test_import_dataset(self): db.session.commit() def test_import_dataset_v0_export(self): - if backend() == "sqlite": - return - num_datasets = db.session.query(SqlaTable).count() self.login(username="admin") @@ -2146,8 +1995,6 @@ def test_import_dataset_overwrite(self): """ Dataset API: Test import existing dataset """ - if backend() == "sqlite": - return self.login(username="admin") uri = "api/v1/dataset/import/" @@ -2217,8 +2064,6 @@ def test_import_dataset_invalid(self): """ Dataset API: Test import invalid dataset """ - if backend() == "sqlite": - return self.login(username="admin") uri = "api/v1/dataset/import/" @@ -2270,8 +2115,6 @@ def test_import_dataset_invalid_v0_validation(self): """ Dataset API: Test import invalid dataset """ - if backend() == "sqlite": - return self.login(username="admin") uri = "api/v1/dataset/import/" @@ -2318,8 +2161,6 @@ def test_get_datasets_is_certified_filter(self): """ Dataset API: Test custom dataset_is_certified filter """ - if backend() == "sqlite": - return table_w_certification = SqlaTable( table_name="foo", @@ -2351,8 +2192,6 @@ def test_duplicate_virtual_dataset(self): """ Dataset API: Test duplicate virtual dataset """ - if backend() == "sqlite": - return dataset = self.get_fixture_virtual_datasets()[0] @@ -2379,8 +2218,6 @@ def test_duplicate_physical_dataset(self): """ Dataset API: Test duplicate physical dataset """ - if backend() == "sqlite": - return dataset = self.get_fixture_datasets()[0] @@ -2395,8 +2232,6 @@ def test_duplicate_existing_dataset(self): """ Dataset API: Test duplicate dataset with existing name """ - if backend() == "sqlite": - return dataset = self.get_fixture_virtual_datasets()[0] @@ -2507,9 +2342,10 @@ def test_get_or_create_dataset_creates_table(self): assert table.normalize_columns is False db.session.delete(table) + db.session.commit() + with examples_db.get_sqla_engine_with_context() as engine: engine.execute("DROP TABLE test_create_sqla_table_api") - db.session.commit() @pytest.mark.usefixtures( "load_energy_table_with_slice", "load_birth_names_dashboard_with_slices" diff --git a/tests/integration_tests/query_context_tests.py b/tests/integration_tests/query_context_tests.py index 8c2082d1c4b12..30cd160d7ee58 100644 --- a/tests/integration_tests/query_context_tests.py +++ b/tests/integration_tests/query_context_tests.py @@ -121,6 +121,7 @@ def test_cache(self): cached = cache_manager.cache.get(cache_key) assert cached is not None + assert "form_data" in cached["data"] rehydrated_qc = ChartDataQueryContextSchema().load(cached["data"]) rehydrated_qo = rehydrated_qc.queries[0] diff --git a/tests/integration_tests/result_set_tests.py b/tests/integration_tests/result_set_tests.py index a39e0ac0d45cf..3e2b3656c212c 100644 --- a/tests/integration_tests/result_set_tests.py +++ b/tests/integration_tests/result_set_tests.py @@ -21,6 +21,7 @@ from superset.dataframe import df_to_records from superset.db_engine_specs import BaseEngineSpec from superset.result_set import dedup, SupersetResultSet +from superset.utils.core import GenericDataType from .base_tests import SupersetTestCase @@ -48,9 +49,27 @@ def test_get_columns_basic(self): self.assertEqual( results.columns, [ - {"is_dttm": False, "type": "STRING", "column_name": "a", "name": "a"}, - {"is_dttm": False, "type": "STRING", "column_name": "b", "name": "b"}, - {"is_dttm": False, "type": "STRING", "column_name": "c", "name": "c"}, + { + "is_dttm": False, + "type": "STRING", + "type_generic": GenericDataType.STRING, + "column_name": "a", + "name": "a", + }, + { + "is_dttm": False, + "type": "STRING", + "type_generic": GenericDataType.STRING, + "column_name": "b", + "name": "b", + }, + { + "is_dttm": False, + "type": "STRING", + "type_generic": GenericDataType.STRING, + "column_name": "c", + "name": "c", + }, ], ) @@ -61,8 +80,20 @@ def test_get_columns_with_int(self): self.assertEqual( results.columns, [ - {"is_dttm": False, "type": "STRING", "column_name": "a", "name": "a"}, - {"is_dttm": False, "type": "INT", "column_name": "b", "name": "b"}, + { + "is_dttm": False, + "type": "STRING", + "type_generic": GenericDataType.STRING, + "column_name": "a", + "name": "a", + }, + { + "is_dttm": False, + "type": "INT", + "type_generic": GenericDataType.NUMERIC, + "column_name": "b", + "name": "b", + }, ], ) @@ -76,11 +107,41 @@ def test_get_columns_type_inference(self): self.assertEqual( results.columns, [ - {"is_dttm": False, "type": "FLOAT", "column_name": "a", "name": "a"}, - {"is_dttm": False, "type": "INT", "column_name": "b", "name": "b"}, - {"is_dttm": False, "type": "STRING", "column_name": "c", "name": "c"}, - {"is_dttm": True, "type": "DATETIME", "column_name": "d", "name": "d"}, - {"is_dttm": False, "type": "BOOL", "column_name": "e", "name": "e"}, + { + "is_dttm": False, + "type": "FLOAT", + "type_generic": GenericDataType.NUMERIC, + "column_name": "a", + "name": "a", + }, + { + "is_dttm": False, + "type": "INT", + "type_generic": GenericDataType.NUMERIC, + "column_name": "b", + "name": "b", + }, + { + "is_dttm": False, + "type": "STRING", + "type_generic": GenericDataType.STRING, + "column_name": "c", + "name": "c", + }, + { + "is_dttm": True, + "type": "DATETIME", + "type_generic": GenericDataType.TEMPORAL, + "column_name": "d", + "name": "d", + }, + { + "is_dttm": False, + "type": "BOOL", + "type_generic": GenericDataType.BOOLEAN, + "column_name": "e", + "name": "e", + }, ], ) @@ -108,6 +169,7 @@ def test_int64_with_missing_data(self): cursor_descr = [("user_id", "bigint", None, None, None, None, True)] results = SupersetResultSet(data, cursor_descr, BaseEngineSpec) self.assertEqual(results.columns[0]["type"], "BIGINT") + self.assertEqual(results.columns[0]["type_generic"], GenericDataType.NUMERIC) def test_data_as_list_of_lists(self): data = [[1, "a"], [2, "b"]] @@ -127,6 +189,7 @@ def test_nullable_bool(self): cursor_descr = [("is_test", "bool", None, None, None, None, True)] results = SupersetResultSet(data, cursor_descr, BaseEngineSpec) self.assertEqual(results.columns[0]["type"], "BOOL") + self.assertEqual(results.columns[0]["type_generic"], GenericDataType.BOOLEAN) df = results.to_pandas_df() self.assertEqual( df_to_records(df), @@ -158,9 +221,13 @@ def test_nested_types(self): cursor_descr = [("id",), ("dict_arr",), ("num_arr",), ("map_col",)] results = SupersetResultSet(data, cursor_descr, BaseEngineSpec) self.assertEqual(results.columns[0]["type"], "INT") + self.assertEqual(results.columns[0]["type_generic"], GenericDataType.NUMERIC) self.assertEqual(results.columns[1]["type"], "STRING") + self.assertEqual(results.columns[1]["type_generic"], GenericDataType.STRING) self.assertEqual(results.columns[2]["type"], "STRING") + self.assertEqual(results.columns[2]["type_generic"], GenericDataType.STRING) self.assertEqual(results.columns[3]["type"], "STRING") + self.assertEqual(results.columns[3]["type_generic"], GenericDataType.STRING) df = results.to_pandas_df() self.assertEqual( df_to_records(df), @@ -204,6 +271,7 @@ def test_single_column_multidim_nested_types(self): cursor_descr = [("metadata",)] results = SupersetResultSet(data, cursor_descr, BaseEngineSpec) self.assertEqual(results.columns[0]["type"], "STRING") + self.assertEqual(results.columns[0]["type_generic"], GenericDataType.STRING) df = results.to_pandas_df() self.assertEqual( df_to_records(df), @@ -219,6 +287,7 @@ def test_nested_list_types(self): cursor_descr = [("metadata",)] results = SupersetResultSet(data, cursor_descr, BaseEngineSpec) self.assertEqual(results.columns[0]["type"], "STRING") + self.assertEqual(results.columns[0]["type_generic"], GenericDataType.STRING) df = results.to_pandas_df() self.assertEqual( df_to_records(df), [{"metadata": '[{"TestKey": [123456, "foo"]}]'}] @@ -229,6 +298,7 @@ def test_empty_datetime(self): cursor_descr = [("ds", "timestamp", None, None, None, None, True)] results = SupersetResultSet(data, cursor_descr, BaseEngineSpec) self.assertEqual(results.columns[0]["type"], "TIMESTAMP") + self.assertEqual(results.columns[0]["type_generic"], GenericDataType.TEMPORAL) def test_no_type_coercion(self): data = [("a", 1), ("b", 2)] @@ -238,7 +308,9 @@ def test_no_type_coercion(self): ] results = SupersetResultSet(data, cursor_descr, BaseEngineSpec) self.assertEqual(results.columns[0]["type"], "VARCHAR") + self.assertEqual(results.columns[0]["type_generic"], GenericDataType.STRING) self.assertEqual(results.columns[1]["type"], "INT") + self.assertEqual(results.columns[1]["type_generic"], GenericDataType.NUMERIC) def test_empty_data(self): data = [] diff --git a/tests/unit_tests/async_events/async_query_manager_tests.py b/tests/unit_tests/async_events/async_query_manager_tests.py index b4ae06dfc3f6f..85ea1142019ee 100644 --- a/tests/unit_tests/async_events/async_query_manager_tests.py +++ b/tests/unit_tests/async_events/async_query_manager_tests.py @@ -14,12 +14,14 @@ # KIND, either express or implied. See the License for the # specific language governing permissions and limitations # under the License. +from unittest import mock +from unittest.mock import ANY, Mock -from unittest.mock import Mock - +from flask import g from jwt import encode from pytest import fixture, raises +from superset import security_manager from superset.async_events.async_query_manager import ( AsyncQueryManager, AsyncQueryTokenException, @@ -38,6 +40,12 @@ def async_query_manager(): return query_manager +def set_current_as_guest_user(): + g.user = security_manager.get_guest_user_from_token( + {"user": {}, "resources": [{"type": "dashboard", "id": "some-uuid"}]} + ) + + def test_parse_channel_id_from_request(async_query_manager): encoded_token = encode( {"channel": "test_channel_id"}, JWT_TOKEN_SECRET, algorithm="HS256" @@ -65,3 +73,70 @@ def test_parse_channel_id_from_request_bad_jwt(async_query_manager): with raises(AsyncQueryTokenException): async_query_manager.parse_channel_id_from_request(request) + + +@mock.patch("superset.is_feature_enabled") +def test_submit_chart_data_job_as_guest_user( + is_feature_enabled_mock, async_query_manager +): + is_feature_enabled_mock.return_value = True + set_current_as_guest_user() + job_mock = Mock() + async_query_manager._load_chart_data_into_cache_job = job_mock + job_meta = async_query_manager.submit_chart_data_job( + channel_id="test_channel_id", + form_data={}, + ) + + job_mock.delay.assert_called_once_with( + { + "channel_id": "test_channel_id", + "errors": [], + "guest_token": { + "resources": [{"id": "some-uuid", "type": "dashboard"}], + "user": {}, + }, + "job_id": ANY, + "result_url": None, + "status": "pending", + "user_id": None, + }, + {}, + ) + + assert "guest_token" not in job_meta + + +@mock.patch("superset.is_feature_enabled") +def test_submit_explore_json_job_as_guest_user( + is_feature_enabled_mock, async_query_manager +): + is_feature_enabled_mock.return_value = True + set_current_as_guest_user() + job_mock = Mock() + async_query_manager._load_explore_json_into_cache_job = job_mock + job_meta = async_query_manager.submit_explore_json_job( + channel_id="test_channel_id", + form_data={}, + response_type="json", + ) + + job_mock.delay.assert_called_once_with( + { + "channel_id": "test_channel_id", + "errors": [], + "guest_token": { + "resources": [{"id": "some-uuid", "type": "dashboard"}], + "user": {}, + }, + "job_id": ANY, + "result_url": None, + "status": "pending", + "user_id": None, + }, + {}, + "json", + False, + ) + + assert "guest_token" not in job_meta diff --git a/tox.ini b/tox.ini index be79ff4cf8dbe..1218fb79a305f 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,9 @@ setenv = SUPERSET_HOME = {envtmpdir} mysql: SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset?charset=utf8 postgres: SUPERSET__SQLALCHEMY_DATABASE_URI = postgresql+psycopg2://superset:superset@localhost/test - sqlite: SUPERSET__SQLALCHEMY_DATABASE_URI = sqlite:////{envtmpdir}/superset.db + sqlite: + SUPERSET__SQLALCHEMY_DATABASE_URI = sqlite:////{envtmpdir}/superset.db + SUPERSET__SQLALCHEMY_EXAMPLES_URI = sqlite:////{envtmpdir}/examples.db mysql-presto: SUPERSET__SQLALCHEMY_DATABASE_URI = mysql://mysqluser:mysqluserpassword@localhost/superset?charset=utf8 # docker run -p 8080:8080 --name presto starburstdata/presto mysql-presto: SUPERSET__SQLALCHEMY_EXAMPLES_URI = presto://localhost:8080/memory/default