blob: abef1afb36423d74d5adfc2175818e45adbe46b3 [file] [log] [blame]
/**
* 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 { TIME_FILTER_MAP } from 'src/explore/constants';
import { getChartIdsInFilterScope } from 'src/dashboard/util/activeDashboardFilters';
import { NativeFiltersState } from 'src/dashboard/reducers/types';
import { Layout } from '../../types';
import { getTreeCheckedItems } from '../nativeFilters/FiltersConfigModal/FiltersConfigForm/FilterScope/utils';
export enum IndicatorStatus {
Unset = 'UNSET',
Applied = 'APPLIED',
Incompatible = 'INCOMPATIBLE',
}
const TIME_GRANULARITY_FIELDS = new Set(Object.values(TIME_FILTER_MAP));
// As of 2020-09-28, the DatasourceMeta type in superset-ui is incorrect.
// Should patch it here until the DatasourceMeta type is updated.
type Datasource = {
time_grain_sqla?: [string, string][];
granularity?: [string, string][];
};
type Filter = {
chartId: number;
columns: { [key: string]: string | string[] };
scopes: { [key: string]: any };
labels: { [key: string]: string };
isDateFilter: boolean;
directPathToFilter: string[];
datasourceId: string;
};
const selectIndicatorValue = (
columnKey: string,
filter: Filter,
datasource: Datasource,
): string[] => {
const values = filter.columns[columnKey];
const arrValues = Array.isArray(values) ? values : [values];
if (
values == null ||
(filter.isDateFilter && values === 'No filter') ||
arrValues.length === 0
) {
return [];
}
if (filter.isDateFilter && TIME_GRANULARITY_FIELDS.has(columnKey)) {
const timeGranularityMap = (
(columnKey === TIME_FILTER_MAP.time_grain_sqla
? datasource.time_grain_sqla
: datasource.granularity) || []
).reduce(
(map, [key, value]) => ({
...map,
[key]: value,
}),
{},
);
return arrValues.map(value => timeGranularityMap[value] || value);
}
return arrValues;
};
const selectIndicatorsForChartFromFilter = (
chartId: number,
filter: Filter,
filterDataSource: Datasource,
appliedColumns: Set<string>,
rejectedColumns: Set<string>,
): Indicator[] => {
// filters can be applied (if the filter is compatible with the datasource)
// or rejected (if the filter is incompatible)
// or the status can be unknown (if the filter has calculated parameters that we can't analyze)
const getStatus = (column: string, filter: Filter) => {
if (appliedColumns.has(column) && filter.columns[column])
return IndicatorStatus.Applied;
if (rejectedColumns.has(column)) return IndicatorStatus.Incompatible;
return IndicatorStatus.Unset;
};
return Object.keys(filter.columns)
.filter(column =>
getChartIdsInFilterScope({
filterScope: filter.scopes[column],
}).includes(chartId),
)
.map(column => ({
column,
name: filter.labels[column] || column,
value: selectIndicatorValue(column, filter, filterDataSource),
status: getStatus(column, filter),
path: filter.directPathToFilter,
}));
};
const getAppliedColumns = (chart: any): Set<string> =>
new Set(
(chart?.queriesResponse?.[0]?.applied_filters || []).map(
(filter: any) => filter.column,
),
);
const getRejectedColumns = (chart: any): Set<string> =>
new Set(
(chart?.queriesResponse?.[0]?.rejected_filters || []).map(
(filter: any) => filter.column,
),
);
export type Indicator = {
column?: string;
name: string;
value: string[];
status: IndicatorStatus;
path: string[];
};
// inspects redux state to find what the filter indicators should be shown for a given chart
export const selectIndicatorsForChart = (
chartId: number,
filters: { [key: number]: Filter },
datasources: { [key: string]: Datasource },
charts: any,
): Indicator[] => {
const chart = charts[chartId];
// no indicators if chart is loading
if (chart.chartStatus === 'loading') return [];
// for now we only need to know which columns are compatible/incompatible,
// so grab the columns from the applied/rejected filters
const appliedColumns = getAppliedColumns(chart);
const rejectedColumns = getRejectedColumns(chart);
const indicators = Object.values(filters)
.filter(filter => filter.chartId !== chartId)
.reduce(
(acc, filter) =>
acc.concat(
selectIndicatorsForChartFromFilter(
chartId,
filter,
datasources[filter.datasourceId] || {},
appliedColumns,
rejectedColumns,
),
),
[] as Indicator[],
);
indicators.sort((a, b) => a.name.localeCompare(b.name));
return indicators;
};
export const selectNativeIndicatorsForChart = (
nativeFilters: NativeFiltersState,
chartId: number,
charts: any,
dashboardLayout: Layout,
): Indicator[] => {
const chart = charts[chartId];
const appliedColumns = getAppliedColumns(chart);
const rejectedColumns = getRejectedColumns(chart);
const getStatus = (
value: string[],
isAffectedByScope: boolean,
column?: string,
): IndicatorStatus => {
if (!isAffectedByScope) {
return IndicatorStatus.Unset;
}
if (!column) {
// Filter without datasource
return IndicatorStatus.Applied;
}
if (column && rejectedColumns.has(column))
return IndicatorStatus.Incompatible;
if (column && appliedColumns.has(column) && value.length > 0) {
return IndicatorStatus.Applied;
}
return IndicatorStatus.Unset;
};
const indicators = Object.values(nativeFilters.filters).map(nativeFilter => {
const isAffectedByScope = getTreeCheckedItems(
nativeFilter.scope,
dashboardLayout,
).some(
layoutItem => dashboardLayout[layoutItem]?.meta?.chartId === chartId,
);
const column = nativeFilter.targets[0]?.column?.name;
const filterState =
nativeFilters.filtersState.nativeFilters?.[nativeFilter.id];
let value = filterState?.currentState?.value ?? [];
if (!Array.isArray(value)) {
value = [value];
}
return {
column,
name: nativeFilter.name,
path: [nativeFilter.id],
status: getStatus(value, isAffectedByScope, column),
value,
};
});
return indicators;
};