From 3ad694e5a96bd7e2e8370ca2d8e62dab00c6ce7e Mon Sep 17 00:00:00 2001 From: Beto Dealmeida Date: Wed, 17 Dec 2025 21:52:32 -0500 Subject: [PATCH] Add name and description --- .../dataAccessRules/DataAccessRuleModal.tsx | 68 +++++++++++-------- .../dataAccessRules/PermissionsTree/index.tsx | 18 +++-- .../src/features/dataAccessRules/types.ts | 2 + .../src/pages/DataAccessRulesList/index.tsx | 64 ++++++++++------- superset/data_access_rules/api.py | 9 +++ superset/data_access_rules/models.py | 6 +- superset/data_access_rules/schemas.py | 24 +++++++ ...d_name_description_to_data_access_rules.py | 43 ++++++++++++ 8 files changed, 173 insertions(+), 61 deletions(-) create mode 100644 superset/migrations/versions/2025-12-17_12-00_b463d8709290_add_name_description_to_data_access_rules.py diff --git a/superset-frontend/src/features/dataAccessRules/DataAccessRuleModal.tsx b/superset-frontend/src/features/dataAccessRules/DataAccessRuleModal.tsx index ee87e011cfa..97f23db312e 100644 --- a/superset-frontend/src/features/dataAccessRules/DataAccessRuleModal.tsx +++ b/superset-frontend/src/features/dataAccessRules/DataAccessRuleModal.tsx @@ -122,6 +122,8 @@ export interface DataAccessRuleModalProps { } const DEFAULT_RULE: DataAccessRuleObject = { + name: '', + description: '', role_id: 0, rule: JSON.stringify( { @@ -133,19 +135,6 @@ const DEFAULT_RULE: DataAccessRuleObject = { ), }; -const RULE_EXAMPLE = `{ - "allowed": [ - {"database": "sales", "schema": "orders"}, - {"database": "sales", "schema": "orders", "table": "prices", - "rls": {"predicate": "org_id = 123", "group_key": "org"}}, - {"database": "sales", "schema": "users", "table": "info", - "cls": {"email": "mask", "ssn": "hide"}} - ], - "denied": [ - {"database": "sales", "schema": "internal"} - ] -}`; - type SelectValue = { value: number; label: string; @@ -318,6 +307,8 @@ function DataAccessRuleModal(props: DataAccessRuleModalProps) { const onSave = () => { const data = { + name: currentRule.name || null, + description: currentRule.description || null, role_id: selectedRole?.value, rule: currentRule.rule, }; @@ -403,6 +394,43 @@ function DataAccessRuleModal(props: DataAccessRuleModalProps) { + +
+ {t('Name')} + +
+
+ updateRuleState('name', e.target.value)} + placeholder={t('e.g., Sales team access')} + data-test="rule-name" + /> +
+
+ + +
+ {t('Description')} + +
+
+ updateRuleState('description', e.target.value)} + placeholder={t('Describe the purpose of this rule...')} + rows={2} + data-test="rule-description" + /> +
+
+
{t('Table Permissions')} @@ -454,20 +482,6 @@ function DataAccessRuleModal(props: DataAccessRuleModalProps) { )} - -
{t('Example')}
-
-                        {RULE_EXAMPLE}
-                      
-
), }, diff --git a/superset-frontend/src/features/dataAccessRules/PermissionsTree/index.tsx b/superset-frontend/src/features/dataAccessRules/PermissionsTree/index.tsx index 4853977da96..e3bdf4e2a95 100644 --- a/superset-frontend/src/features/dataAccessRules/PermissionsTree/index.tsx +++ b/superset-frontend/src/features/dataAccessRules/PermissionsTree/index.tsx @@ -164,6 +164,16 @@ const StyledContainer = styled.div` max-width: 250px; font-size: 12px; } + + .count-allowed { + color: ${theme.colorSuccess}; + font-weight: ${theme.fontWeightSemiBold}; + } + + .count-denied { + color: ${theme.colorError}; + font-weight: ${theme.fontWeightSemiBold}; + } `} `; @@ -684,13 +694,9 @@ function PermissionsTree({ {hasCustomRules && ( ( - - {counts.allowed} - + {counts.allowed} {' / '} - - {counts.denied} - + {counts.denied} ) )} diff --git a/superset-frontend/src/features/dataAccessRules/types.ts b/superset-frontend/src/features/dataAccessRules/types.ts index 1c4e804442d..9b4cfa194d8 100644 --- a/superset-frontend/src/features/dataAccessRules/types.ts +++ b/superset-frontend/src/features/dataAccessRules/types.ts @@ -24,6 +24,8 @@ export type RoleObject = { export type DataAccessRuleObject = { id?: number; + name?: string; + description?: string; role_id: number; role?: RoleObject; rule: string; diff --git a/superset-frontend/src/pages/DataAccessRulesList/index.tsx b/superset-frontend/src/pages/DataAccessRulesList/index.tsx index e2f29e0a50d..721a1443e9a 100644 --- a/superset-frontend/src/pages/DataAccessRulesList/index.tsx +++ b/superset-frontend/src/pages/DataAccessRulesList/index.tsx @@ -126,6 +126,44 @@ function DataAccessRulesList(props: DataAccessRulesListProps) { const columns = useMemo( () => [ + { + Cell: ({ + row: { + original: { name }, + }, + }: { + row: { original: DataAccessRuleObject }; + }) => name || '-', + accessor: 'name', + Header: t('Name'), + size: 'lg', + id: 'name', + }, + { + Cell: ({ + row: { + original: { description }, + }, + }: { + row: { original: DataAccessRuleObject }; + }) => { + if (!description) return '-'; + const truncated = + description.length > 100 + ? `${description.substring(0, 100)}...` + : description; + return ( + + {truncated} + + ); + }, + accessor: 'description', + Header: t('Description'), + size: 'xl', + id: 'description', + disableSortBy: true, + }, { Cell: ({ row: { @@ -140,32 +178,6 @@ function DataAccessRulesList(props: DataAccessRulesListProps) { id: 'role', disableSortBy: true, }, - { - Cell: ({ - row: { - original: { rule }, - }, - }: { - row: { original: DataAccessRuleObject }; - }) => { - const displayRule = - typeof rule === 'string' ? rule : JSON.stringify(rule); - const truncated = - displayRule.length > 100 - ? `${displayRule.substring(0, 100)}...` - : displayRule; - return ( - - {truncated} - - ); - }, - accessor: 'rule', - Header: t('Rule (JSON)'), - size: 'xxl', - id: 'rule', - disableSortBy: true, - }, { Cell: ({ row: { diff --git a/superset/data_access_rules/api.py b/superset/data_access_rules/api.py index fcd54474fbf..639b900377d 100644 --- a/superset/data_access_rules/api.py +++ b/superset/data_access_rules/api.py @@ -63,6 +63,8 @@ class DataAccessRulesRestApi(BaseSupersetModelRestApi): list_columns = [ "id", + "name", + "description", "role_id", "role.id", "role.name", @@ -74,19 +76,26 @@ class DataAccessRulesRestApi(BaseSupersetModelRestApi): ] order_columns = [ "id", + "name", "role_id", "changed_on_delta_humanized", ] add_columns = [ + "name", + "description", "role_id", "rule", ] edit_columns = [ + "name", + "description", "role_id", "rule", ] show_columns = [ "id", + "name", + "description", "role_id", "role.name", "role.id", diff --git a/superset/data_access_rules/models.py b/superset/data_access_rules/models.py index 1aa166f7c2a..f87e03da96d 100644 --- a/superset/data_access_rules/models.py +++ b/superset/data_access_rules/models.py @@ -68,7 +68,7 @@ from __future__ import annotations from typing import Any from flask_appbuilder import Model -from sqlalchemy import Column, ForeignKey, Integer, Text +from sqlalchemy import Column, ForeignKey, Integer, String, Text from sqlalchemy.orm import relationship from superset import security_manager @@ -87,6 +87,8 @@ class DataAccessRule(Model, AuditMixinNullable): __tablename__ = "data_access_rules" id = Column(Integer, primary_key=True) + name = Column(String(250), nullable=True) + description = Column(Text, nullable=True) role_id = Column(Integer, ForeignKey("ab_role.id"), nullable=False) rule = Column(Text, nullable=False) @@ -97,7 +99,7 @@ class DataAccessRule(Model, AuditMixinNullable): ) def __repr__(self) -> str: - return f"" + return f"" @property def rule_dict(self) -> dict[str, Any]: diff --git a/superset/data_access_rules/schemas.py b/superset/data_access_rules/schemas.py index a1f7f1137a5..cc344d5019b 100644 --- a/superset/data_access_rules/schemas.py +++ b/superset/data_access_rules/schemas.py @@ -66,6 +66,8 @@ class DataAccessRuleListSchema(Schema): """Schema for listing data access rules.""" id = fields.Integer(metadata={"description": "Unique ID of the rule"}) + name = fields.String(metadata={"description": "Name of the rule"}) + description = fields.String(metadata={"description": "Description of the rule"}) role_id = fields.Integer(metadata={"description": "ID of the associated role"}) role = fields.Nested(RoleSchema) rule = fields.String(metadata={"description": rule_description}) @@ -80,6 +82,8 @@ class DataAccessRuleShowSchema(Schema): """Schema for showing a single data access rule.""" id = fields.Integer(metadata={"description": "Unique ID of the rule"}) + name = fields.String(metadata={"description": "Name of the rule"}) + description = fields.String(metadata={"description": "Description of the rule"}) role_id = fields.Integer(metadata={"description": "ID of the associated role"}) role = fields.Nested(RoleSchema) rule = fields.String(metadata={"description": rule_description}) @@ -92,6 +96,16 @@ class DataAccessRuleShowSchema(Schema): class DataAccessRulePostSchema(Schema): """Schema for creating a data access rule.""" + name = fields.String( + metadata={"description": "Name for this rule (optional)"}, + required=False, + allow_none=True, + ) + description = fields.String( + metadata={"description": "Description of the rule (optional)"}, + required=False, + allow_none=True, + ) role_id = fields.Integer( metadata={"description": "ID of the role this rule applies to"}, required=True, @@ -159,6 +173,16 @@ class DataAccessRulePostSchema(Schema): class DataAccessRulePutSchema(Schema): """Schema for updating a data access rule.""" + name = fields.String( + metadata={"description": "Name for this rule (optional)"}, + required=False, + allow_none=True, + ) + description = fields.String( + metadata={"description": "Description of the rule (optional)"}, + required=False, + allow_none=True, + ) role_id = fields.Integer( metadata={"description": "ID of the role this rule applies to"}, required=False, diff --git a/superset/migrations/versions/2025-12-17_12-00_b463d8709290_add_name_description_to_data_access_rules.py b/superset/migrations/versions/2025-12-17_12-00_b463d8709290_add_name_description_to_data_access_rules.py new file mode 100644 index 00000000000..1068e9bbcb5 --- /dev/null +++ b/superset/migrations/versions/2025-12-17_12-00_b463d8709290_add_name_description_to_data_access_rules.py @@ -0,0 +1,43 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +"""add name and description to data_access_rules + +Revision ID: b463d8709290 +Revises: a352d7609189 +Create Date: 2025-12-17 12:00:00.000000 + +""" + +import sqlalchemy as sa + +from superset.migrations.shared.utils import add_columns, drop_columns + +# revision identifiers, used by Alembic. +revision = "b463d8709290" +down_revision = "a352d7609189" + + +def upgrade(): + add_columns( + "data_access_rules", + sa.Column("name", sa.String(250), nullable=True), + sa.Column("description", sa.Text(), nullable=True), + ) + + +def downgrade(): + drop_columns("data_access_rules", "name", "description")