Working on filters

This commit is contained in:
Beto Dealmeida
2026-04-17 15:10:13 -04:00
parent 71717818c3
commit c86ef4f044
3 changed files with 155 additions and 27 deletions

View File

@@ -190,13 +190,75 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
useState<ListViewFetchDataConfig | null>(null);
const [currentSourceFilter, setCurrentSourceFilter] = useState<string>('');
/**
* Fetches "Data connection" filter options — a combined list of databases
* and semantic layers.
*
* Semantic layer values are prefixed with "sl:" so that fetchData can tell
* them apart from integer database IDs and route to the correct API filter.
*/
const fetchConnectionOptions = useCallback(
async (filterValue = '', page: number, pageSize: number) => {
const showDatabases = currentSourceFilter !== 'semantic_layer';
const showSemanticLayers =
isFeatureEnabled(FeatureFlag.SemanticLayers) &&
currentSourceFilter !== 'database';
const [dbResult, slResult] = await Promise.all([
showDatabases
? createFetchRelated(
'dataset',
'database',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching %s: %s',
datasetsLabelLower(),
errMsg,
),
),
)(filterValue, page, pageSize)
: Promise.resolve({ data: [], totalCount: 0 }),
showSemanticLayers
? SupersetClient.get({
endpoint: `/api/v1/semantic_layer/?q=${rison.encode_uri({
...(filterValue
? { filters: [{ col: 'name', opr: 'ct', value: filterValue }] }
: {}),
page: 0,
page_size: 100,
})}`,
})
.then(({ json = {} }) => ({
data: (json?.result ?? []).map(
(layer: { uuid: string; name: string }) => ({
label: layer.name,
// "sl:" prefix distinguishes semantic layers from DB integer IDs
value: `sl:${layer.uuid}`,
}),
),
totalCount: json?.count ?? 0,
}))
.catch(() => ({ data: [], totalCount: 0 }))
: Promise.resolve({ data: [], totalCount: 0 }),
]);
return {
// Semantic layers first, then databases
data: [...slResult.data, ...dbResult.data],
totalCount: slResult.totalCount + dbResult.totalCount,
};
},
[currentSourceFilter],
);
const fetchData = useCallback((config: ListViewFetchDataConfig) => {
setLastFetchConfig(config);
setLoading(true);
const { pageIndex, pageSize, sortBy, filters: filterValues } = config;
// Separate source_type filter from other filters
// Separate source_type and database/connection filters for special handling
const sourceTypeFilter = filterValues.find(f => f.id === 'source_type');
const databaseFilter = filterValues.find(f => f.id === 'database');
// Track source filter for conditional Type filter visibility
const sourceVal =
@@ -204,8 +266,9 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
? (sourceTypeFilter.value as { value: string }).value
: ((sourceTypeFilter?.value as string) ?? '');
setCurrentSourceFilter(sourceVal);
const otherFilters = filterValues
.filter(f => f.id !== 'source_type')
.filter(f => f.id !== 'source_type' && f.id !== 'database')
.filter(
({ value }) => value !== '' && value !== null && value !== undefined,
)
@@ -231,6 +294,30 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
});
}
// Translate the "Data connection" filter: values prefixed with "sl:" are
// semantic layer UUIDs; plain values are database IDs.
if (databaseFilter?.value !== undefined && databaseFilter.value !== '') {
const raw =
databaseFilter.value &&
typeof databaseFilter.value === 'object' &&
'value' in databaseFilter.value
? (databaseFilter.value as { value: unknown }).value
: databaseFilter.value;
if (typeof raw === 'string' && raw.startsWith('sl:')) {
otherFilters.push({
col: 'semantic_layer_uuid',
opr: 'eq',
value: raw.slice(3),
});
} else if (raw !== null && raw !== undefined && raw !== '') {
otherFilters.push({
col: 'database',
opr: databaseFilter.operator,
value: raw,
});
}
}
const queryParams = rison.encode_uri({
order_column: sortBy[0].id,
order_direction: sortBy[0].desc ? 'desc' : 'asc',
@@ -744,7 +831,7 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
operator: FilterOperator.Equals,
unfilteredLabel: t('All'),
selects: [
{ label: databaseLabel(), value: 'database' },
{ label: t('Database'), value: 'database' },
{ label: t('Semantic Layer'), value: 'semantic_layer' },
],
},
@@ -797,20 +884,10 @@ const DatasetList: FunctionComponent<DatasetListProps> = ({
Header: databaseLabel(),
key: 'database',
id: 'database',
input: 'select',
input: 'select' as const,
operator: FilterOperator.RelationOneMany,
unfilteredLabel: 'All',
fetchSelects: createFetchRelated(
'dataset',
'database',
createErrorHandler(errMsg =>
t(
'An error occurred while fetching %s: %s',
datasetsLabelLower(),
errMsg,
),
),
),
fetchSelects: fetchConnectionOptions,
paginate: true,
dropdownStyle: { minWidth: WIDER_DROPDOWN_WIDTH },
},

View File

@@ -62,14 +62,32 @@ class GetCombinedDatasourceListCommand(BaseCommand):
order_direction = self._args.get("order_direction", "desc")
filters = self._args.get("filters", [])
source_type, name_filter, sql_filter, type_filter = self._parse_filters(filters)
(
source_type,
name_filter,
sql_filter,
type_filter,
database_id,
semantic_layer_uuid,
) = self._parse_filters(filters)
# A connection filter implicitly narrows the source type: selecting a
# database ID means "show only datasets", and selecting a semantic layer
# UUID means "show only semantic views". Only apply the implicit
# narrowing when the user hasn't already set an explicit source_type.
if source_type == "all":
if database_id is not None:
source_type = "database"
elif semantic_layer_uuid is not None:
source_type = "semantic_layer"
source_type = self._resolve_source_type(source_type, sql_filter, type_filter)
if source_type == "empty":
return {"count": 0, "result": []}
ds_q = DatasourceDAO.build_dataset_query(name_filter, sql_filter)
sv_q = DatasourceDAO.build_semantic_view_query(name_filter)
ds_q = DatasourceDAO.build_dataset_query(name_filter, sql_filter, database_id)
sv_q = DatasourceDAO.build_semantic_view_query(name_filter, semantic_layer_uuid)
if source_type == "database":
combined = ds_q.subquery()
@@ -126,10 +144,15 @@ class GetCombinedDatasourceListCommand(BaseCommand):
return "database"
if not self._can_read_datasets:
return "semantic_layer"
# An explicit source_type selection ("database" or "semantic_layer") always
# wins. This prevents e.g. Type="Semantic View" from overriding an explicit
# Source="Database" filter and showing inconsistent results.
if source_type in ("database", "semantic_layer"):
return source_type
# sql_filter (physical/virtual toggle) only applies to datasets
if sql_filter is not None:
return "database"
# Explicit semantic-view type filter
# Explicit semantic-view type filter (only reached when source_type="all")
if type_filter == "semantic_view":
return "semantic_layer"
return source_type
@@ -137,20 +160,24 @@ class GetCombinedDatasourceListCommand(BaseCommand):
@staticmethod
def _parse_filters(
filters: list[dict[str, Any]],
) -> tuple[str, str | None, bool | None, str | None]:
) -> tuple[str, str | None, bool | None, str | None, int | None, str | None]:
"""
Translate raw rison filter dicts into typed query parameters.
Returns:
source_type: "all" | "database" | "semantic_layer"
name_filter: substring to match against name/table_name
sql_filter: True → physical only, False → virtual only, None → both
type_filter: "semantic_view" when the caller wants only semantic views
source_type: "all" | "database" | "semantic_layer"
name_filter: substring to match against name/table_name
sql_filter: True → physical only, False → virtual only, None → both
type_filter: "semantic_view" when the caller wants only semantic views
database_id: filter datasets to a specific database ID
semantic_layer_uuid: filter semantic views to a specific semantic layer UUID
"""
source_type = "all"
name_filter: str | None = None
sql_filter: bool | None = None
type_filter: str | None = None
database_id: int | None = None
semantic_layer_uuid: str | None = None
for f in filters:
col = f.get("col")
@@ -165,5 +192,19 @@ class GetCombinedDatasourceListCommand(BaseCommand):
type_filter = "semantic_view"
else:
sql_filter = value
elif col == "database" and value is not None:
try:
database_id = int(value)
except (TypeError, ValueError):
pass
elif col == "semantic_layer_uuid" and value is not None:
semantic_layer_uuid = str(value)
return source_type, name_filter, sql_filter, type_filter
return (
source_type,
name_filter,
sql_filter,
type_filter,
database_id,
semantic_layer_uuid,
)

View File

@@ -91,6 +91,7 @@ class DatasourceDAO(BaseDAO[Datasource]):
def build_dataset_query(
name_filter: str | None,
sql_filter: bool | None,
database_id: int | None = None,
) -> Select:
"""Build a SELECT for datasets, applying access and content filters."""
ds_q = select(
@@ -116,11 +117,17 @@ class DatasourceDAO(BaseDAO[Datasource]):
else:
ds_q = ds_q.where(and_(SqlaTable.sql.isnot(None), SqlaTable.sql != ""))
if database_id is not None:
ds_q = ds_q.where(SqlaTable.database_id == database_id)
return ds_q
@staticmethod
def build_semantic_view_query(name_filter: str | None) -> Select:
"""Build a SELECT for semantic views, applying name filter."""
def build_semantic_view_query(
name_filter: str | None,
semantic_layer_uuid: str | None = None,
) -> Select:
"""Build a SELECT for semantic views, applying name and layer filters."""
sv_q = select(
SemanticView.id.label("item_id"),
literal("semantic_layer").label("source_type"),
@@ -131,6 +138,9 @@ class DatasourceDAO(BaseDAO[Datasource]):
if name_filter:
sv_q = sv_q.where(SemanticView.name.ilike(f"%{name_filter}%"))
if semantic_layer_uuid is not None:
sv_q = sv_q.where(SemanticView.semantic_layer_uuid == semantic_layer_uuid)
return sv_q
@staticmethod