mirror of
https://github.com/apache/superset.git
synced 2026-05-07 08:54:23 +00:00
Add name and description
This commit is contained in:
@@ -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>
|
||||
</>
|
||||
),
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -24,6 +24,8 @@ export type RoleObject = {
|
||||
|
||||
export type DataAccessRuleObject = {
|
||||
id?: number;
|
||||
name?: string;
|
||||
description?: string;
|
||||
role_id: number;
|
||||
role?: RoleObject;
|
||||
rule: string;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
Reference in New Issue
Block a user