diff --git a/src/base/Button/Button.tsx b/src/base/Button/Button.tsx
index 534ae2de0..9eefa74a1 100644
--- a/src/base/Button/Button.tsx
+++ b/src/base/Button/Button.tsx
@@ -1,13 +1,34 @@
import { Button as MuiButton, type ButtonProps as MuiButtonProps } from '@mui/material';
+import React from 'react';
+import { Key, PermissionShield } from '../../custom/permissions';
export interface ButtonProps extends MuiButtonProps {
label?: string;
children?: React.ReactNode;
+ permissionKey?: Key;
}
-export function Button({ label, children, ...props }: ButtonProps): JSX.Element {
+export function Button({
+ label,
+ children,
+ permissionKey,
+ disabled,
+ ...props
+}: 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 f21709a23..778158ee2 100644
--- a/src/base/IconButton/IconButton.tsx
+++ b/src/base/IconButton/IconButton.tsx
@@ -3,12 +3,26 @@ import {
type IconButtonProps as MuiIconButtonProps
} from '@mui/material';
import React from 'react';
+import { Key, PermissionShield } from '../../custom/permissions';
-export type IconButtonProps = MuiIconButtonProps;
+export interface IconButtonProps extends MuiIconButtonProps {
+ permissionKey?: Key;
+}
-export const IconButton = React.forwardRef((props, ref) => (
-
-));
+export const IconButton = React.forwardRef((props, ref) => {
+ const { permissionKey, disabled, ...rest } = props;
+
+ // When disabled AND permissionKey is provided, show the shield overlay
+ if (disabled && permissionKey) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
+});
IconButton.displayName = 'IconButton';
diff --git a/src/base/ListItem/ListItem.tsx b/src/base/ListItem/ListItem.tsx
index bcb609aec..eb3ec7981 100644
--- a/src/base/ListItem/ListItem.tsx
+++ b/src/base/ListItem/ListItem.tsx
@@ -1,8 +1,29 @@
import { ListItem as MuiListItem, ListItemProps as MuiListItemProps } from '@mui/material';
import React from 'react';
+import { Key, PermissionShield } from '../../custom/permissions';
-const ListItem = React.forwardRef((props, ref) => {
- return ;
+export interface ListItemProps extends MuiListItemProps {
+ permissionKey?: Key;
+ disabled?: boolean;
+}
+
+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 ;
});
+ListItem.displayName = 'ListItem';
+
export default ListItem;
diff --git a/src/base/ListItemButton/ListItemButton.tsx b/src/base/ListItemButton/ListItemButton.tsx
index f19e4f3e9..d5125aa76 100644
--- a/src/base/ListItemButton/ListItemButton.tsx
+++ b/src/base/ListItemButton/ListItemButton.tsx
@@ -3,9 +3,28 @@ import {
ListItemButtonProps as MuiListItemButtonProps
} from '@mui/material';
import React from 'react';
+import { Key, PermissionShield } from '../../custom/permissions';
-const ListItemButton = React.forwardRef((props, ref) => {
- return ;
+export interface ListItemButtonProps extends MuiListItemButtonProps {
+ permissionKey?: Key;
+}
+
+const ListItemButton = React.forwardRef((props, ref) => {
+ const { permissionKey, disabled, ...rest } = props;
+
+ // When disabled AND permissionKey is provided, show the shield overlay
+ if (disabled && permissionKey) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
});
+ListItemButton.displayName = 'ListItemButton';
+
export { ListItemButton };
+export default ListItemButton;
diff --git a/src/base/MenuItem/MenuItem.tsx b/src/base/MenuItem/MenuItem.tsx
index 397ef39a9..d46d2ee7a 100644
--- a/src/base/MenuItem/MenuItem.tsx
+++ b/src/base/MenuItem/MenuItem.tsx
@@ -1,7 +1,24 @@
import { MenuItem as MuiMenuItem, MenuItemProps as MuiMenuItemProps } from '@mui/material';
+import React from 'react';
+import { Key, PermissionShield } from '../../custom/permissions';
-export function MenuItem(props: MuiMenuItemProps): JSX.Element {
- return ;
+export interface MenuItemProps extends MuiMenuItemProps {
+ permissionKey?: Key;
+}
+
+export function MenuItem(props: MenuItemProps): JSX.Element {
+ const { permissionKey, disabled, ...rest } = props;
+
+ // When disabled AND permissionKey is provided, show the shield overlay
+ if (disabled && permissionKey) {
+ return (
+
+
+
+ );
+ }
+
+ return ;
}
export default MenuItem;
diff --git a/src/custom/permissions.tsx b/src/custom/permissions.tsx
index 9799dd19c..ff78c5672 100644
--- a/src/custom/permissions.tsx
+++ b/src/custom/permissions.tsx
@@ -1,10 +1,35 @@
-// import { CAN, getCapabilitiesRegistry, getMesheryEventBus } from '@/globals/mesherySdk';
import React from 'react';
+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';
+
+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 {
- 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 +52,287 @@ 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 interface PermissionShieldProps {
+ permissionKey: Key;
+ children: React.ReactNode;
+ variant?: 'inline' | 'badge';
+}
+
+/**
+ * PermissionShield Wrapper Component
+ *
+ * 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,
+ 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]);
+
+ if (!permissionKey) {
+ return <>{children}>;
+ }
+
+ const tooltipTitle = (
+
+ {/* Header Row */}
+
+
+ {permissionKey.function || permissionKey.subject || '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 && (
+
+
+ {permissionKey.subcategory && (
+
+ )}
+
+ )}
+
+ {/* 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 && (
+ <>
+
+ Resource ID
+
+ {permissionKey.id}
+
+ >
+ )}
+
+ );
+
+ 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 +345,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 +362,8 @@ export const createCanShow = (
const reason = predicateRes?.[1] || {
type: 'MISSING_PERMISSION',
data: {
- keyId: Key?.action as string
- }
+ keyId: actionString,
+ },
};
if (can) {
@@ -89,22 +392,20 @@ export const createCanShow = (
style={{
cursor: 'pointer',
pointerEvents,
- opacity: opacity
+ opacity: opacity,
}}
onClick={onClick}
>
- {React.cloneElement(children as React.ReactElement, {
+ {React.isValidElement(children) ? 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
+ opacity: opacity,
},
- onClick: onClick
- })}
+ onClick: onClick,
+ }) : children}
);
-
- // return null;
};
};
diff --git a/src/index.tsx b/src/index.tsx
index 6a1180d1e..1129de512 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -45,3 +45,9 @@ export {
type QuickDateRangeOption,
type UniversalFilterProps
} from './custom/UniversalFilter';
+
+export {
+ PermissionShield,
+ type Key,
+ type PermissionShieldProps
+} from './custom/permissions';