Add name and description

This commit is contained in:
Beto Dealmeida
2025-12-17 21:52:32 -05:00
parent b66729ad08
commit 3ad694e5a9
8 changed files with 173 additions and 61 deletions

View File

@@ -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) {
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">
{t('Name')}
<InfoTooltip
tooltip={t('Optional name to help identify this rule.')}
/>
</div>
<div className="input-container">
<Input
name="name"
value={currentRule.name || ''}
onChange={e => updateRuleState('name', e.target.value)}
placeholder={t('e.g., Sales team access')}
data-test="rule-name"
/>
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">
{t('Description')}
<InfoTooltip
tooltip={t('Optional description of what this rule grants or restricts.')}
/>
</div>
<div className="input-container">
<Input.TextArea
name="description"
value={currentRule.description || ''}
onChange={e => updateRuleState('description', e.target.value)}
placeholder={t('Describe the purpose of this rule...')}
rows={2}
data-test="rule-description"
/>
</div>
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">
{t('Table Permissions')}
@@ -454,20 +482,6 @@ function DataAccessRuleModal(props: DataAccessRuleModalProps) {
)}
</StyledInputContainer>
<StyledInputContainer>
<div className="control-label">{t('Example')}</div>
<pre
style={{
background: '#f5f5f5',
padding: '12px',
borderRadius: '4px',
fontSize: '12px',
overflow: 'auto',
}}
>
{RULE_EXAMPLE}
</pre>
</StyledInputContainer>
</>
),
},

View File

@@ -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 && (
<span className="node-count">
(
<span style={{ color: '#52c41a', fontWeight: 'bold' }}>
{counts.allowed}
</span>
<span className="count-allowed">{counts.allowed}</span>
{' / '}
<span style={{ color: '#f5222d', fontWeight: 'bold' }}>
{counts.denied}
</span>
<span className="count-denied">{counts.denied}</span>
)
</span>
)}

View File

@@ -24,6 +24,8 @@ export type RoleObject = {
export type DataAccessRuleObject = {
id?: number;
name?: string;
description?: string;
role_id: number;
role?: RoleObject;
rule: string;

View File

@@ -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 (
<Tooltip id="desc-tooltip" title={description} placement="top">
<span>{truncated}</span>
</Tooltip>
);
},
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 (
<Tooltip id="rule-tooltip" title={displayRule} placement="top">
<code style={{ fontSize: '11px' }}>{truncated}</code>
</Tooltip>
);
},
accessor: 'rule',
Header: t('Rule (JSON)'),
size: 'xxl',
id: 'rule',
disableSortBy: true,
},
{
Cell: ({
row: {

View File

@@ -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",

View File

@@ -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"<DataAccessRule(id={self.id}, role_id={self.role_id})>"
return f"<DataAccessRule(id={self.id}, name={self.name!r}, role_id={self.role_id})>"
@property
def rule_dict(self) -> dict[str, Any]:

View File

@@ -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,

View File

@@ -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")