Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions src/authz-module/audit-user/CustomCells.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import ViewMoreLink from '@src/authz-module/components/ViewMoreLink';
import { Delete, ExpandMore } from '@openedx/paragon/icons';
import { IconButton } from '@openedx/paragon';
import { TableCellValue, UserRole } from 'types';
import messages from './messages';
import { getPermissionsCountByRole } from './utils';

interface ExpandableTableRow<T> extends TableCellValue<T> {
row: TableCellValue<T>['row'] & {
isExpanded: boolean;
toggleRowExpanded: () => void;
values: T;
};
}

type CellProps = ExpandableTableRow<UserRole>;

export const ViewAllPermissionsCell = ({ row }: CellProps) => {
const { formatMessage } = useIntl();
return (
<ViewMoreLink
label={formatMessage(
row.isExpanded
? messages['authz.user.table.view_all_permissions.link.text.close']
: messages['authz.user.table.view_all_permissions.link.text.open'],
)}
onClick={() => row.toggleRowExpanded()}
iconSrc={ExpandMore}
/>
);
};

export const ActionsCell = ({ row }: CellProps) => {
const { formatMessage } = useIntl();
const handleDelete = () => {
// TODO: Implement delete functionality
console.log('Delete clicked for row:', row);

Check warning on line 38 in src/authz-module/audit-user/CustomCells.tsx

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
};

return (
<IconButton variant="danger" onClick={handleDelete} alt={formatMessage(messages['authz.user.table.delete.action.alt'])} src={Delete} />
);
};

export const PermissionsCell = ({ row }: CellProps) => {
const { formatMessage } = useIntl();
// TODO handle permissions length per role
if (row.original.permissions.length === 1) {
return <span>{row.original.permissions[0]}</span>;
}
const count = getPermissionsCountByRole(row.original.role);
return (
<span>
{formatMessage(messages['authz.user.table.permissions.available.count'], { count })}
</span>
);
};
24 changes: 24 additions & 0 deletions src/authz-module/audit-user/RenderAdminRole.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import messages from './messages';

interface RenderAdminRoleProps {
role: string;
}

const RenderAdminRole = ({ role }: RenderAdminRoleProps) => {
const intl = useIntl();
// Determine which message to show based on role
const messageKey = role?.toLowerCase().includes('admin')
? 'authz.user.table.permissions.role.admin'
: 'authz.user.table.permissions.role.staff';

return (
<div className="mb-4">
<p className="mb-0 text-gray-700">
{intl.formatMessage(messages[messageKey])}
</p>
</div>
);
};

export default RenderAdminRole;
51 changes: 51 additions & 0 deletions src/authz-module/audit-user/RenderPermissionColumn.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Icon } from '@openedx/paragon';
import ResourceTooltip from 'authz-module/components/ResourceTooltip';
import { RolePermission } from 'types';

interface ExtendedRolePermission extends RolePermission {
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}

interface PermissionItem {
key: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
label: string;
description: string;
perms: ExtendedRolePermission[];
}

interface RenderPermissionColumnProps {
items: PermissionItem[];
}

const RenderPermissionColumn = ({ items }: RenderPermissionColumnProps) => items.map(({
key, icon, label, description, perms,
}) => (
<div key={key} className="mb-4">
<div className="d-flex align-items-center mb-2">
<Icon src={icon} className="mr-2 text-primary" size="xs" />
<h5 className="text-primary m-0">{label}</h5>
<ResourceTooltip
resourceGroup={{
key, label, description, permissions: perms,
}}
/>
</div>
<ul className="mb-0 list-unstyled d-flex">
{perms.map((perm, index) => (
<li
key={perm.key}
className={`d-flex align-items-center text-primary-400 ${index !== perms.length - 1 ? 'border-right pr-2' : ''
} ${index !== 0 ? 'pl-2' : ''}`}
>
<Icon src={perm.icon} className="mr-2" size="xs" />
<span className="text-primary small font-weight-light">
{perm.label}
</span>
</li>
))}
</ul>
</div>
));

export default RenderPermissionColumn;
56 changes: 56 additions & 0 deletions src/authz-module/audit-user/RenderPermissionInLine.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { Icon } from '@openedx/paragon';
import ResourceTooltip from 'authz-module/components/ResourceTooltip';
import { RolePermission } from 'types';

interface ExtendedRolePermission extends RolePermission {
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
}

interface PermissionItem {
key: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
label: string;
description: string;
perms: ExtendedRolePermission[];
}

interface RenderPermissionInLineProps {
items: PermissionItem[];
}

const RenderPermissionInLine = ({ items }: RenderPermissionInLineProps) => (
<div className="d-flex align-items-start w-100 no-scroll">
{items.map(({
key, icon, label, description, perms,
}, index) => (
<div
key={key}
className={`d-flex flex-column ${index !== items.length - 1 ? 'pr-4 mr-4 border-right' : ''}`}
>
<div className="d-flex align-items-center mb-2">
<Icon src={icon} className="mr-2 text-primary" size="xs" />
<h5 className="text-primary m-0">{label}</h5>
<ResourceTooltip
resourceGroup={{
key, label, description, permissions: perms,
}}
/>
</div>
<div className="d-flex">
{perms.map((perm, i) => (
<div
key={perm.key}
className={`d-flex align-items-center ${i !== perms.length - 1 ? 'pr-2 mr-2 border-right' : ''}`}
>
<Icon src={perm.icon} className="mr-2" size="xs" />
<span className="text-primary small font-weight-light">
{perm.label}
</span>
</div>
))}
</div>
</div>
))}
</div>
);
export default RenderPermissionInLine;
90 changes: 90 additions & 0 deletions src/authz-module/audit-user/UserPermissions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {
courseResourceTypes,
coursePermissions,
rolesObject,
} from '../courses/constant';
import {
libraryResourceTypes,
libraryPermissions,
rolesLibraryObject,
} from '../libraries/constants';
import RenderPermissionColumn from './RenderPermissionColumn';
import RenderPermissionInLine from './RenderPermissionInLine';
import RenderAdminRole from './RenderAdminRole';

interface UserPermissionsProps {
row: {
original: {
role: string;
};
};
}

const UserPermissions = ({ row }: UserPermissionsProps) => {
let roleKey = row?.original?.role;
if (!roleKey) { return null; }

// validation to show django roles
const normalizedRole = roleKey.trim().toLowerCase();
if (!normalizedRole.includes('library') && !normalizedRole.includes('course')
&& (normalizedRole.includes('admin') || normalizedRole.includes('staff'))) {
return (
<div className="d-flex flex-wrap bg-white px-4 py-4 border border-light">
<RenderAdminRole role={roleKey} />
</div>
);
}

// Normalize role string to match keys in constants (e.g. "Course Admin" -> "course_admin")
roleKey = roleKey.trim().toLowerCase().replace(/[-\s]+/g, '_');
const isLibraryRole = roleKey.includes('library');
const config = isLibraryRole
? {
resourceTypes: libraryResourceTypes,
permissions: libraryPermissions,
roles: rolesLibraryObject,
}
: {
resourceTypes: courseResourceTypes,
permissions: coursePermissions,
roles: rolesObject,
};

const roleObj = config.roles.find(r => r.role === roleKey);
if (!roleObj) { return null; }

const rolePerms = new Set(roleObj.permissions.map(String));
// Build resource list with permissions (only once)
const resources = config.resourceTypes
.map(resource => {
const perms = config.permissions.filter(
p => p.resource === resource.key && rolePerms.has(String(p.key)),
);
return perms.length ? { ...resource, perms } : null;
})
.filter(Boolean);

const isSingleRow = resources.length <= 3;
const mid = Math.ceil(resources.length / 2);
const columns = isSingleRow
? [resources]
: [resources.slice(0, mid), resources.slice(mid)];
return (
<div className="d-flex flex-wrap bg-white px-4 py-4 border border-light-200">
{isSingleRow
? <RenderPermissionInLine items={resources} />
: columns.map((col, index) => (
<div
key={`column-${index === 0 ? 'left' : 'right'}`}
className={`w-100 w-md-50 py-3 ${
index === 0 ? 'pr-md-3 border-right' : 'pl-md-4'
}`}
>
<RenderPermissionColumn items={col} />
</div>
))}
</div>
);
};

export default UserPermissions;
Loading
Loading