From ba15a6315856479ad91a130892926e4b3bde100e Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Tue, 7 Jul 2026 03:28:42 +0530 Subject: [PATCH 1/9] feat(permissions): integrate unified permission API in core components Signed-off-by: Rishi Raj --- src/base/Button/Button.tsx | 38 +++- src/base/IconButton/IconButton.tsx | 31 ++- src/base/ListItem/ListItem.tsx | 29 ++- src/base/ListItemButton/ListItemButton.tsx | 30 ++- src/base/MenuItem/MenuItem.tsx | 30 ++- src/custom/permissions.tsx | 245 +++++++++++++++++++-- src/index.tsx | 9 + 7 files changed, 385 insertions(+), 27 deletions(-) diff --git a/src/base/Button/Button.tsx b/src/base/Button/Button.tsx index 534ae2de0..642bc14cc 100644 --- a/src/base/Button/Button.tsx +++ b/src/base/Button/Button.tsx @@ -1,11 +1,47 @@ import { Button as MuiButton, type ButtonProps as MuiButtonProps } from '@mui/material'; +import React from 'react'; +import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; export interface ButtonProps extends MuiButtonProps { label?: string; children?: React.ReactNode; + permissionKey?: Key; + permissionAction?: PermissionAction; } -export function Button({ label, children, ...props }: ButtonProps): JSX.Element { +export function Button({ + label, + children, + permissionKey, + permissionAction = 'disable', + ...props +}: ButtonProps): JSX.Element | null { + const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); + + if (!hasPermission) { + switch (action) { + case 'hide': + return null; + case 'showShield': + return ( + + + {label} + {children} + + + ); + case 'disable': + default: + return ( + + {label} + {children} + + ); + } + } + return ( {label} diff --git a/src/base/IconButton/IconButton.tsx b/src/base/IconButton/IconButton.tsx index f21709a23..09bec4f4c 100644 --- a/src/base/IconButton/IconButton.tsx +++ b/src/base/IconButton/IconButton.tsx @@ -3,12 +3,35 @@ import { type IconButtonProps as MuiIconButtonProps } from '@mui/material'; import React from 'react'; +import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; -export type IconButtonProps = MuiIconButtonProps; +export interface IconButtonProps extends MuiIconButtonProps { + permissionKey?: Key; + permissionAction?: PermissionAction; +} -export const IconButton = React.forwardRef((props, ref) => ( - -)); +export const IconButton = React.forwardRef((props, ref) => { + const { permissionKey, permissionAction = 'disable', ...rest } = props; + const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); + + if (!hasPermission) { + switch (action) { + case 'hide': + return null; + case 'showShield': + return ( + + + + ); + case 'disable': + default: + return ; + } + } + + return ; +}); IconButton.displayName = 'IconButton'; diff --git a/src/base/ListItem/ListItem.tsx b/src/base/ListItem/ListItem.tsx index bcb609aec..c3cf573f4 100644 --- a/src/base/ListItem/ListItem.tsx +++ b/src/base/ListItem/ListItem.tsx @@ -1,8 +1,33 @@ import { ListItem as MuiListItem, ListItemProps as MuiListItemProps } from '@mui/material'; import React from 'react'; +import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; -const ListItem = React.forwardRef((props, ref) => { - return ; +export interface ListItemProps extends MuiListItemProps { + permissionKey?: Key; + permissionAction?: PermissionAction; +} + +const ListItem = React.forwardRef((props, ref) => { + const { permissionKey, permissionAction = 'disable', ...rest } = props; + const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); + + if (!hasPermission) { + switch (action) { + case 'hide': + return null; + case 'showShield': + return ( + + + + ); + case 'disable': + default: + return ; + } + } + + return ; }); export default ListItem; diff --git a/src/base/ListItemButton/ListItemButton.tsx b/src/base/ListItemButton/ListItemButton.tsx index f19e4f3e9..88ec61666 100644 --- a/src/base/ListItemButton/ListItemButton.tsx +++ b/src/base/ListItemButton/ListItemButton.tsx @@ -3,9 +3,35 @@ import { ListItemButtonProps as MuiListItemButtonProps } from '@mui/material'; import React from 'react'; +import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; -const ListItemButton = React.forwardRef((props, ref) => { - return ; +export interface ListItemButtonProps extends MuiListItemButtonProps { + permissionKey?: Key; + permissionAction?: PermissionAction; +} + +const ListItemButton = React.forwardRef((props, ref) => { + const { permissionKey, permissionAction = 'disable', ...rest } = props; + const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); + + if (!hasPermission) { + switch (action) { + case 'hide': + return null; + case 'showShield': + return ( + + + + ); + case 'disable': + default: + return ; + } + } + + return ; }); export { ListItemButton }; +export default ListItemButton; diff --git a/src/base/MenuItem/MenuItem.tsx b/src/base/MenuItem/MenuItem.tsx index 397ef39a9..968d3b4d6 100644 --- a/src/base/MenuItem/MenuItem.tsx +++ b/src/base/MenuItem/MenuItem.tsx @@ -1,7 +1,33 @@ import { MenuItem as MuiMenuItem, MenuItemProps as MuiMenuItemProps } from '@mui/material'; +import React from 'react'; +import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; -export function MenuItem(props: MuiMenuItemProps): JSX.Element { - return ; +export interface MenuItemProps extends MuiMenuItemProps { + permissionKey?: Key; + permissionAction?: PermissionAction; +} + +export function MenuItem(props: MenuItemProps): JSX.Element | null { + const { permissionKey, permissionAction = 'disable', ...rest } = props; + const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); + + if (!hasPermission) { + switch (action) { + case 'hide': + return null; + case 'showShield': + return ( + + + + ); + case 'disable': + default: + return ; + } + } + + return ; } export default MenuItem; diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx index 9799dd19c..a0e614734 100644 --- a/src/custom/permissions.tsx +++ b/src/custom/permissions.tsx @@ -1,10 +1,20 @@ -// import { CAN, getCapabilitiesRegistry, getMesheryEventBus } from '@/globals/mesherySdk'; -import React from 'react'; +import React, { createContext, useContext } from 'react'; +import { Box, Typography } from '@mui/material'; import { EventBus } from '../actors/eventBus'; +import CustomTooltip from './CustomTooltip/customTooltip'; +import ShieldIcon from '@mui/icons-material/Shield'; export interface Key { - subject: string; - action: string; + // Backwards compatibility for old CanShow + subject?: string; + action?: string; + + // New Schemas key structure + id?: string; + category?: string; + subcategory?: string; + function?: string; + description?: string; } export type InvertAction = 'disable' | 'hide'; @@ -27,12 +37,214 @@ export type ReasonEvent = MissingPermissionReason | MissingCapabilityReason; export interface HasKeyProps { Key?: Key; - predicate?: (capabilitiesRegistry: unknown) => [boolean, ReasonEvent]; // returns a boolean and an event if the user does not have the permission + predicate?: (capabilitiesRegistry: unknown) => [boolean, ReasonEvent]; children: React.ReactNode; notifyOnclick?: boolean; invert_action?: InvertAction[]; } +export type CANFunction = (action: string, subject: string) => boolean; + +export interface PermissionContextType { + CAN?: CANFunction; +} + +export const PermissionContext = createContext({}); + +export const PermissionProvider: React.FC<{ + CAN: CANFunction; + children: React.ReactNode; +}> = ({ CAN, children }) => { + return ( + + {children} + + ); +}; + +export const usePermissionContext = () => useContext(PermissionContext); + +export type PermissionAction = 'disable' | 'hide' | 'showShield' | 'grayOut'; + +export interface UsePermissionProps { + permissionKey?: Key; + permissionAction?: PermissionAction; +} + +/** + * usePermission Hook + * Evaluates whether the user has the specified permission key using the context CAN function. + */ +export const usePermission = (props?: UsePermissionProps) => { + const { CAN } = usePermissionContext(); + const permissionKey = props?.permissionKey; + const permissionAction = props?.permissionAction || 'disable'; + + if (!permissionKey || !CAN) { + return { + hasPermission: true, + action: permissionAction, + }; + } + + // Support both new Key (id, function) and old Key (action, subject) + const actionString = permissionKey.id || permissionKey.action || ''; + const subjectString = permissionKey.function || permissionKey.subject || ''; + + if (!actionString || !subjectString) { + return { + hasPermission: true, + action: permissionAction, + }; + } + + const hasPermission = CAN(actionString, subjectString); + + return { + hasPermission, + action: permissionAction, + }; +}; + +export interface PermissionShieldProps { + permissionKey: Key; + children: React.ReactNode; + variant?: 'inline' | 'badge'; +} + +/** + * PermissionShield Wrapper Component + * Grays out its children and attaches a hoverable/clickable shield (lock) icon showing permission metadata. + */ +export const PermissionShield: React.FC = ({ + permissionKey, + children, + variant = 'inline', +}) => { + const tooltipTitle = ( + + + You don't currently have permission to perform this action. + + {permissionKey.category && ( + + + Category + + {permissionKey.category} + + )} + {permissionKey.subcategory && ( + + + Subcategory + + {permissionKey.subcategory} + + )} + {(permissionKey.description || permissionKey.function) && ( + + + Description + + + {permissionKey.description || permissionKey.function} + + + )} + + ); + + const isBadge = variant === 'badge'; + + return ( + + + {children} + + + + + + + + + ); +}; + // returns the children if the user has the permission to view the component or if a key is not provided // if the user does not have the permission to view the component, it will return null or a disabled version of the component specified by the invert_action prop export const createCanShow = ( @@ -45,13 +257,16 @@ export const createCanShow = ( children, notifyOnclick = true, predicate, - invert_action = ['disable'] + invert_action = ['disable'], }: HasKeyProps) => { if (!children) { return null; } - const hasKey = Key?.subject ? CAN(Key?.action, Key?.subject) : true; + const actionString = Key?.id || Key?.action || ''; + const subjectString = Key?.function || Key?.subject || ''; + + const hasKey = subjectString ? CAN(actionString, subjectString) : true; const predicateRes = predicate && predicate(getCapabilitiesRegistry()); const can = predicateRes ? predicateRes[0] && hasKey : hasKey; @@ -59,8 +274,8 @@ export const createCanShow = ( const reason = predicateRes?.[1] || { type: 'MISSING_PERMISSION', data: { - keyId: Key?.action as string - } + keyId: actionString, + }, }; if (can) { @@ -89,22 +304,20 @@ export const createCanShow = ( style={{ cursor: 'pointer', pointerEvents, - opacity: opacity + opacity: opacity, }} onClick={onClick} > - {React.cloneElement(children as React.ReactElement, { + {React.cloneElement(children as React.ReactElement, { style: { - ...((children as React.ReactElement).props.style as React.CSSProperties), + ...((children as React.ReactElement).props.style as React.CSSProperties), cursor: 'pointer', pointerEvents, - opacity: opacity + opacity: opacity, }, - onClick: onClick + onClick: onClick, })} ); - - // return null; }; }; diff --git a/src/index.tsx b/src/index.tsx index 6a1180d1e..e583d2f69 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -45,3 +45,12 @@ export { type QuickDateRangeOption, type UniversalFilterProps } from './custom/UniversalFilter'; + +export { + PermissionProvider, + PermissionContext, + usePermission, + PermissionShield, + type Key, + type PermissionAction +} from './custom/permissions'; From c59cc666f4afb1bdedd6a2737a52720d987c71fa Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Wed, 8 Jul 2026 17:19:58 +0530 Subject: [PATCH 2/9] [Permissions] Simplify permission shield components to be visual-only Signed-off-by: Rishi Raj --- src/base/Button/Button.tsx | 43 +++++-------- src/base/IconButton/IconButton.tsx | 29 +++------ src/base/ListItem/ListItem.tsx | 28 +++------ src/base/ListItemButton/ListItemButton.tsx | 29 +++------ src/base/MenuItem/MenuItem.tsx | 31 ++++----- src/custom/permissions.tsx | 73 +++------------------- src/index.tsx | 5 +- 7 files changed, 64 insertions(+), 174 deletions(-) diff --git a/src/base/Button/Button.tsx b/src/base/Button/Button.tsx index 642bc14cc..9eefa74a1 100644 --- a/src/base/Button/Button.tsx +++ b/src/base/Button/Button.tsx @@ -1,49 +1,34 @@ import { Button as MuiButton, type ButtonProps as MuiButtonProps } from '@mui/material'; import React from 'react'; -import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; +import { Key, PermissionShield } from '../../custom/permissions'; export interface ButtonProps extends MuiButtonProps { label?: string; children?: React.ReactNode; permissionKey?: Key; - permissionAction?: PermissionAction; } export function Button({ label, children, permissionKey, - permissionAction = 'disable', + disabled, ...props -}: ButtonProps): JSX.Element | null { - const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); - - if (!hasPermission) { - switch (action) { - case 'hide': - return null; - case 'showShield': - return ( - - - {label} - {children} - - - ); - case 'disable': - default: - return ( - - {label} - {children} - - ); - } +}: ButtonProps): JSX.Element { + // When disabled AND permissionKey is provided, show the shield overlay + if (disabled && permissionKey) { + return ( + + + {label} + {children} + + + ); } return ( - + {label} {children} diff --git a/src/base/IconButton/IconButton.tsx b/src/base/IconButton/IconButton.tsx index 09bec4f4c..778158ee2 100644 --- a/src/base/IconButton/IconButton.tsx +++ b/src/base/IconButton/IconButton.tsx @@ -3,34 +3,25 @@ import { type IconButtonProps as MuiIconButtonProps } from '@mui/material'; import React from 'react'; -import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; +import { Key, PermissionShield } from '../../custom/permissions'; export interface IconButtonProps extends MuiIconButtonProps { permissionKey?: Key; - permissionAction?: PermissionAction; } export const IconButton = React.forwardRef((props, ref) => { - const { permissionKey, permissionAction = 'disable', ...rest } = props; - const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); + const { permissionKey, disabled, ...rest } = props; - if (!hasPermission) { - switch (action) { - case 'hide': - return null; - case 'showShield': - return ( - - - - ); - case 'disable': - default: - return ; - } + // When disabled AND permissionKey is provided, show the shield overlay + if (disabled && permissionKey) { + return ( + + + + ); } - return ; + return ; }); IconButton.displayName = 'IconButton'; diff --git a/src/base/ListItem/ListItem.tsx b/src/base/ListItem/ListItem.tsx index c3cf573f4..9ccbdeca4 100644 --- a/src/base/ListItem/ListItem.tsx +++ b/src/base/ListItem/ListItem.tsx @@ -1,30 +1,22 @@ import { ListItem as MuiListItem, ListItemProps as MuiListItemProps } from '@mui/material'; import React from 'react'; -import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; +import { Key, PermissionShield } from '../../custom/permissions'; export interface ListItemProps extends MuiListItemProps { permissionKey?: Key; - permissionAction?: PermissionAction; } const ListItem = React.forwardRef((props, ref) => { - const { permissionKey, permissionAction = 'disable', ...rest } = props; - const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); + const { permissionKey, ...rest } = props; - if (!hasPermission) { - switch (action) { - case 'hide': - return null; - case 'showShield': - return ( - - - - ); - case 'disable': - default: - return ; - } + // ListItem doesn't have a native `disabled` prop, so check via sx/style or a custom flag + // For now, if permissionKey is provided, show the shield (consumer controls when to pass it) + if (permissionKey) { + return ( + + + + ); } return ; diff --git a/src/base/ListItemButton/ListItemButton.tsx b/src/base/ListItemButton/ListItemButton.tsx index 88ec61666..d96c54fa5 100644 --- a/src/base/ListItemButton/ListItemButton.tsx +++ b/src/base/ListItemButton/ListItemButton.tsx @@ -3,34 +3,25 @@ import { ListItemButtonProps as MuiListItemButtonProps } from '@mui/material'; import React from 'react'; -import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; +import { Key, PermissionShield } from '../../custom/permissions'; export interface ListItemButtonProps extends MuiListItemButtonProps { permissionKey?: Key; - permissionAction?: PermissionAction; } const ListItemButton = React.forwardRef((props, ref) => { - const { permissionKey, permissionAction = 'disable', ...rest } = props; - const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); + const { permissionKey, disabled, ...rest } = props; - if (!hasPermission) { - switch (action) { - case 'hide': - return null; - case 'showShield': - return ( - - - - ); - case 'disable': - default: - return ; - } + // When disabled AND permissionKey is provided, show the shield overlay + if (disabled && permissionKey) { + return ( + + + + ); } - return ; + return ; }); export { ListItemButton }; diff --git a/src/base/MenuItem/MenuItem.tsx b/src/base/MenuItem/MenuItem.tsx index 968d3b4d6..d46d2ee7a 100644 --- a/src/base/MenuItem/MenuItem.tsx +++ b/src/base/MenuItem/MenuItem.tsx @@ -1,33 +1,24 @@ import { MenuItem as MuiMenuItem, MenuItemProps as MuiMenuItemProps } from '@mui/material'; import React from 'react'; -import { Key, PermissionAction, usePermission, PermissionShield } from '../../custom/permissions'; +import { Key, PermissionShield } from '../../custom/permissions'; export interface MenuItemProps extends MuiMenuItemProps { permissionKey?: Key; - permissionAction?: PermissionAction; } -export function MenuItem(props: MenuItemProps): JSX.Element | null { - const { permissionKey, permissionAction = 'disable', ...rest } = props; - const { hasPermission, action } = usePermission({ permissionKey, permissionAction }); +export function MenuItem(props: MenuItemProps): JSX.Element { + const { permissionKey, disabled, ...rest } = props; - if (!hasPermission) { - switch (action) { - case 'hide': - return null; - case 'showShield': - return ( - - - - ); - case 'disable': - default: - return ; - } + // When disabled AND permissionKey is provided, show the shield overlay + if (disabled && permissionKey) { + return ( + + + + ); } - return ; + return ; } export default MenuItem; diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx index a0e614734..ab2744824 100644 --- a/src/custom/permissions.tsx +++ b/src/custom/permissions.tsx @@ -1,4 +1,4 @@ -import React, { createContext, useContext } from 'react'; +import React from 'react'; import { Box, Typography } from '@mui/material'; import { EventBus } from '../actors/eventBus'; import CustomTooltip from './CustomTooltip/customTooltip'; @@ -43,69 +43,6 @@ export interface HasKeyProps { invert_action?: InvertAction[]; } -export type CANFunction = (action: string, subject: string) => boolean; - -export interface PermissionContextType { - CAN?: CANFunction; -} - -export const PermissionContext = createContext({}); - -export const PermissionProvider: React.FC<{ - CAN: CANFunction; - children: React.ReactNode; -}> = ({ CAN, children }) => { - return ( - - {children} - - ); -}; - -export const usePermissionContext = () => useContext(PermissionContext); - -export type PermissionAction = 'disable' | 'hide' | 'showShield' | 'grayOut'; - -export interface UsePermissionProps { - permissionKey?: Key; - permissionAction?: PermissionAction; -} - -/** - * usePermission Hook - * Evaluates whether the user has the specified permission key using the context CAN function. - */ -export const usePermission = (props?: UsePermissionProps) => { - const { CAN } = usePermissionContext(); - const permissionKey = props?.permissionKey; - const permissionAction = props?.permissionAction || 'disable'; - - if (!permissionKey || !CAN) { - return { - hasPermission: true, - action: permissionAction, - }; - } - - // Support both new Key (id, function) and old Key (action, subject) - const actionString = permissionKey.id || permissionKey.action || ''; - const subjectString = permissionKey.function || permissionKey.subject || ''; - - if (!actionString || !subjectString) { - return { - hasPermission: true, - action: permissionAction, - }; - } - - const hasPermission = CAN(actionString, subjectString); - - return { - hasPermission, - action: permissionAction, - }; -}; - export interface PermissionShieldProps { permissionKey: Key; children: React.ReactNode; @@ -114,7 +51,13 @@ export interface PermissionShieldProps { /** * PermissionShield Wrapper Component - * Grays out its children and attaches a hoverable/clickable shield (lock) icon showing permission metadata. + * + * Renders children with a shield icon overlay showing permission metadata. + * This is a pure visual component — it does NOT check permissions itself. + * The consumer is responsible for determining disabled state (e.g. via CAN()). + * + * Usage in base components: when `disabled` is true AND `permissionKey` is provided, + * the component automatically wraps itself in PermissionShield. */ export const PermissionShield: React.FC = ({ permissionKey, diff --git a/src/index.tsx b/src/index.tsx index e583d2f69..1129de512 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -47,10 +47,7 @@ export { } from './custom/UniversalFilter'; export { - PermissionProvider, - PermissionContext, - usePermission, PermissionShield, type Key, - type PermissionAction + type PermissionShieldProps } from './custom/permissions'; From 933c886681bcb6dad45ce3bf9f9764ecd6b9d05b Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Wed, 8 Jul 2026 17:23:47 +0530 Subject: [PATCH 3/9] [Permissions] Fix explicit any type lint errors in permissions.tsx Signed-off-by: Rishi Raj --- src/custom/permissions.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx index ab2744824..d6bb75922 100644 --- a/src/custom/permissions.tsx +++ b/src/custom/permissions.tsx @@ -251,9 +251,9 @@ export const createCanShow = ( }} onClick={onClick} > - {React.cloneElement(children as React.ReactElement, { + {React.cloneElement(children as React.ReactElement<{ style?: React.CSSProperties; onClick?: React.MouseEventHandler }>, { style: { - ...((children as React.ReactElement).props.style as React.CSSProperties), + ...((children as React.ReactElement<{ style?: React.CSSProperties; onClick?: React.MouseEventHandler }>).props.style || {}), cursor: 'pointer', pointerEvents, opacity: opacity, From 8d0d2e1502c6c677aad2195479dc00e716de2621 Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Wed, 8 Jul 2026 17:29:26 +0530 Subject: [PATCH 4/9] [Permissions] Fix transitive dependency pollution by using base Tooltip in permissions.tsx Signed-off-by: Rishi Raj --- src/custom/permissions.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx index d6bb75922..c9c192326 100644 --- a/src/custom/permissions.tsx +++ b/src/custom/permissions.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Box, Typography } from '@mui/material'; import { EventBus } from '../actors/eventBus'; -import CustomTooltip from './CustomTooltip/customTooltip'; +import Tooltip from '../base/Tooltip/Tooltip'; import ShieldIcon from '@mui/icons-material/Shield'; export interface Key { @@ -137,7 +137,7 @@ export const PermissionShield: React.FC = ({ {children} - + = ({ > - + ); }; From 33253e98f7244ac0bf519d2314ffc5c83d994e90 Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Wed, 8 Jul 2026 18:03:24 +0530 Subject: [PATCH 5/9] [Permissions] Format tooltip as a structured card and coordinate active instances Signed-off-by: Rishi Raj --- src/custom/permissions.tsx | 389 ++++++++++++++++++++++++++++--------- 1 file changed, 294 insertions(+), 95 deletions(-) diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx index c9c192326..b0ac260f2 100644 --- a/src/custom/permissions.tsx +++ b/src/custom/permissions.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { Box, Typography } from '@mui/material'; +import { Box, Typography, ClickAwayListener, Chip } from '@mui/material'; import { EventBus } from '../actors/eventBus'; import Tooltip from '../base/Tooltip/Tooltip'; import ShieldIcon from '@mui/icons-material/Shield'; @@ -64,60 +64,230 @@ export const PermissionShield: React.FC = ({ children, variant = 'inline', }) => { + const [open, setOpen] = React.useState(false); + const uniqueId = React.useId(); + + const handleClose = () => { + setOpen(false); + }; + + const handleToggle = (e: React.MouseEvent) => { + e.stopPropagation(); + setOpen((prev) => { + const next = !prev; + if (next) { + window.dispatchEvent(new CustomEvent('permission-shield-opened', { detail: { id: uniqueId } })); + } + return next; + }); + }; + + React.useEffect(() => { + const handleOtherOpen = (e: Event) => { + const customEvent = e as CustomEvent; + if (customEvent.detail?.id !== uniqueId) { + setOpen(false); + } + }; + window.addEventListener('permission-shield-opened', handleOtherOpen); + return () => { + window.removeEventListener('permission-shield-opened', handleOtherOpen); + }; + }, [uniqueId]); + const tooltipTitle = ( - - - You don't currently have permission to perform this action. - + + {/* Header Row */} + + + Access Restricted + + + {/* Status dot chip */} + + } + sx={{ + background: 'rgba(235, 192, 36, 0.12)', + color: '#EBC024', + fontWeight: 600, + fontSize: '0.7rem', + height: '20px', + border: '1px solid rgba(235, 192, 36, 0.25)', + '& .MuiChip-label': { + paddingLeft: '4px', + paddingRight: '6px', + }, + }} + /> + + + {/* Meta info chips row */} {permissionKey.category && ( - - + - Category - - {permissionKey.category} + /> + {permissionKey.subcategory && ( + + )} )} - {permissionKey.subcategory && ( - + + {/* Divider */} + + + {/* Capabilities Blocked Section */} + + Blocked Action + + + + + {permissionKey.function || permissionKey.subject || 'Action'} + + + Locked + + + + {/* Divider */} + + + {/* Description Section */} + + What this permission allows + + + + {permissionKey.description || `Allows you to perform the ${permissionKey.function || permissionKey.subject || 'selected'} operation.`} + + + + {/* Resource Details / Key Info */} + {permissionKey.id && ( + <> + - Subcategory + Resource ID - {permissionKey.subcategory} - - )} - {(permissionKey.description || permissionKey.function) && ( - - Description - - - {permissionKey.description || permissionKey.function} + {permissionKey.id} - + )} ); @@ -125,66 +295,95 @@ export const PermissionShield: React.FC = ({ const isBadge = variant === 'badge'; return ( - - - {children} - + + + + {children} + - - - - - - + + + + + + ); }; From c49d10797c5cbb85891898b45939127f66c22c04 Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Wed, 8 Jul 2026 21:44:09 +0530 Subject: [PATCH 6/9] fixing-missing-names Signed-off-by: Rishi Raj --- src/base/ListItem/ListItem.tsx | 10 ++-- src/base/ListItemButton/ListItemButton.tsx | 2 + src/custom/permissions.tsx | 61 ++++++++-------------- 3 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/base/ListItem/ListItem.tsx b/src/base/ListItem/ListItem.tsx index 9ccbdeca4..58298ac14 100644 --- a/src/base/ListItem/ListItem.tsx +++ b/src/base/ListItem/ListItem.tsx @@ -4,14 +4,14 @@ import { Key, PermissionShield } from '../../custom/permissions'; export interface ListItemProps extends MuiListItemProps { permissionKey?: Key; + disabled?: boolean; } const ListItem = React.forwardRef((props, ref) => { - const { permissionKey, ...rest } = props; + const { permissionKey, disabled, ...rest } = props; - // ListItem doesn't have a native `disabled` prop, so check via sx/style or a custom flag - // For now, if permissionKey is provided, show the shield (consumer controls when to pass it) - if (permissionKey) { + // When disabled AND permissionKey is provided, show the shield overlay + if (disabled && permissionKey) { return ( @@ -22,4 +22,6 @@ const ListItem = React.forwardRef((props, ref) => return ; }); +ListItem.displayName = 'ListItem'; + export default ListItem; diff --git a/src/base/ListItemButton/ListItemButton.tsx b/src/base/ListItemButton/ListItemButton.tsx index d96c54fa5..d5125aa76 100644 --- a/src/base/ListItemButton/ListItemButton.tsx +++ b/src/base/ListItemButton/ListItemButton.tsx @@ -24,5 +24,7 @@ const ListItemButton = React.forwardRef((pr return ; }); +ListItemButton.displayName = 'ListItemButton'; + export { ListItemButton }; export default ListItemButton; diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx index b0ac260f2..b907ea424 100644 --- a/src/custom/permissions.tsx +++ b/src/custom/permissions.tsx @@ -4,6 +4,21 @@ import { EventBus } from '../actors/eventBus'; import Tooltip from '../base/Tooltip/Tooltip'; import ShieldIcon from '@mui/icons-material/Shield'; +const SECTION_HEADING_SX = { + fontSize: '0.7rem', + fontWeight: 700, + letterSpacing: '0.06em', + textTransform: 'uppercase' as const, + color: 'rgba(255, 255, 255, 0.7)', + mb: 0.75, +}; + +const DIVIDER_SX = { + height: '1px', + background: 'rgba(255, 255, 255, 0.1)', + my: 1.25, +}; + export interface Key { // Backwards compatibility for old CanShow subject?: string; @@ -177,21 +192,10 @@ export const PermissionShield: React.FC = ({ )} {/* Divider */} - + {/* Capabilities Blocked Section */} - - Blocked Action - + Blocked Action = ({ {/* Divider */} - + {/* Description Section */} - - What this permission allows - + What this permission allows = ({ {/* Resource Details / Key Info */} {permissionKey.id && ( <> - - - Resource ID - + + Resource ID = ({ Date: Wed, 8 Jul 2026 21:56:51 +0530 Subject: [PATCH 7/9] applying-gemini-suggestions Signed-off-by: Rishi Raj --- src/base/ListItem/ListItem.tsx | 4 ++-- src/custom/permissions.tsx | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/base/ListItem/ListItem.tsx b/src/base/ListItem/ListItem.tsx index 58298ac14..febdc8977 100644 --- a/src/base/ListItem/ListItem.tsx +++ b/src/base/ListItem/ListItem.tsx @@ -14,12 +14,12 @@ const ListItem = React.forwardRef((props, ref) => if (disabled && permissionKey) { return ( - + ); } - return ; + return ; }); ListItem.displayName = 'ListItem'; diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx index b907ea424..8ef2449be 100644 --- a/src/custom/permissions.tsx +++ b/src/custom/permissions.tsx @@ -110,6 +110,10 @@ export const PermissionShield: React.FC = ({ }; }, [uniqueId]); + if (!permissionKey) { + return <>{children}; + } + const tooltipTitle = ( {/* Header Row */} @@ -431,7 +435,7 @@ export const createCanShow = ( }} onClick={onClick} > - {React.cloneElement(children as React.ReactElement<{ style?: React.CSSProperties; onClick?: React.MouseEventHandler }>, { + {React.isValidElement(children) ? React.cloneElement(children as React.ReactElement<{ style?: React.CSSProperties; onClick?: React.MouseEventHandler }>, { style: { ...((children as React.ReactElement<{ style?: React.CSSProperties; onClick?: React.MouseEventHandler }>).props.style || {}), cursor: 'pointer', @@ -439,7 +443,7 @@ export const createCanShow = ( opacity: opacity, }, onClick: onClick, - })} + }) : children} ); }; From 78a26e84d6bd95b0753adfeee94c0cc374efeb16 Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Wed, 8 Jul 2026 22:01:45 +0530 Subject: [PATCH 8/9] fix-build Signed-off-by: Rishi Raj --- src/base/ListItem/ListItem.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/base/ListItem/ListItem.tsx b/src/base/ListItem/ListItem.tsx index febdc8977..eb3ec7981 100644 --- a/src/base/ListItem/ListItem.tsx +++ b/src/base/ListItem/ListItem.tsx @@ -11,15 +11,17 @@ const ListItem = React.forwardRef((props, ref) => const { permissionKey, disabled, ...rest } = props; // When disabled AND permissionKey is provided, show the shield overlay + // Note: MUI ListItem doesn't have a native `disabled` prop, so we only use + // it as a custom guard for the PermissionShield wrapper if (disabled && permissionKey) { return ( - + ); } - return ; + return ; }); ListItem.displayName = 'ListItem'; From 464f35a1b3fcb081c78c566ca0bd0764a0ffcded Mon Sep 17 00:00:00 2001 From: Rishi Raj Date: Thu, 9 Jul 2026 01:49:57 +0530 Subject: [PATCH 9/9] update-the-tooltip Signed-off-by: Rishi Raj --- src/custom/permissions.tsx | 43 ++------------------------------------ 1 file changed, 2 insertions(+), 41 deletions(-) diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx index 8ef2449be..ff78c5672 100644 --- a/src/custom/permissions.tsx +++ b/src/custom/permissions.tsx @@ -134,7 +134,7 @@ export const PermissionShield: React.FC = ({ color: '#FFFFFF', }} > - Access Restricted + {permissionKey.function || permissionKey.subject || 'Access Restricted'} {/* Status dot chip */} @@ -149,6 +149,7 @@ export const PermissionShield: React.FC = ({ borderRadius: '50%', background: '#EBC024', marginLeft: 6, + marginRight: 0, }} /> } @@ -198,46 +199,6 @@ export const PermissionShield: React.FC = ({ {/* Divider */} - {/* Capabilities Blocked Section */} - Blocked Action - - - - {permissionKey.function || permissionKey.subject || 'Action'} - - - Locked - - - - {/* Divider */} - - {/* Description Section */} What this permission allows