diff --git a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.css b/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.css
deleted file mode 100644
index 1d8291e6e..000000000
--- a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.css
+++ /dev/null
@@ -1,15 +0,0 @@
-.datatable.access-viewer-datatable .toolBar {
- font-size: inherit;
-}
-
-.datatable.access-viewer-datatable .toolBar .small {
- font-weight: normal;
-}
-
-.datatable.access-viewer-datatable .toolBar.pl-0 {
- padding-left: 0;
-}
-
-.datatable.access-viewer-datatable .toolBar.pr-0 {
- padding-right: 0;
-}
diff --git a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.html b/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.html
deleted file mode 100644
index 1eec17532..000000000
--- a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/access-viewer-datatable.html
+++ /dev/null
@@ -1,89 +0,0 @@
-
-
-
-
-
-
-
-
- |
-
-
- |
- Access origin |
-
-
-
-
- {{ item.EndpointName }} |
- {{ item.RoleName }} |
- {{ item.TeamName ? 'Team' : 'User' }} {{ item.TeamName }} access defined on {{ item.AccessLocation }}
- {{ item.GroupName }}
- Manage access
- Manage access
- |
-
-
- Select a user to show associated access and role |
-
-
- The selected user does not have access to any environment(s) |
-
-
-
-
-
-
diff --git a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/index.js b/app/portainer/rbac/components/access-viewer/access-viewer-datatable/index.js
deleted file mode 100644
index cfcc9bd17..000000000
--- a/app/portainer/rbac/components/access-viewer/access-viewer-datatable/index.js
+++ /dev/null
@@ -1,14 +0,0 @@
-import './access-viewer-datatable.css';
-
-export const accessViewerDatatable = {
- templateUrl: './access-viewer-datatable.html',
- controller: 'GenericDatatableController',
- bindings: {
- titleText: '@',
- titleIcon: '@',
- tableKey: '@',
- orderBy: '@',
- dataset: '<',
- isAdmin: '<',
- },
-};
diff --git a/app/portainer/rbac/components/access-viewer/access-viewer.controller.js b/app/portainer/rbac/components/access-viewer/access-viewer.controller.js
index d2d33e8d0..b231d6afb 100644
--- a/app/portainer/rbac/components/access-viewer/access-viewer.controller.js
+++ b/app/portainer/rbac/components/access-viewer/access-viewer.controller.js
@@ -2,7 +2,7 @@ import _ from 'lodash-es';
import { isLimitedToBE } from '@/react/portainer/feature-flags/feature-flags.service';
import { getEnvironments } from '@/react/portainer/environments/environment.service';
-import AccessViewerPolicyModel from '../../models/access';
+import { AccessViewerPolicyModel } from '@/react/portainer/users/RolesView/AccessViewer/model';
export default class AccessViewerController {
/* @ngInject */
diff --git a/app/portainer/rbac/components/access-viewer/access-viewer.html b/app/portainer/rbac/components/access-viewer/access-viewer.html
index 1da86123f..8be5ebb0b 100644
--- a/app/portainer/rbac/components/access-viewer/access-viewer.html
+++ b/app/portainer/rbac/components/access-viewer/access-viewer.html
@@ -17,7 +17,8 @@
-
+
+
diff --git a/app/portainer/rbac/index.js b/app/portainer/rbac/index.js
index 0448c55d7..21563de83 100644
--- a/app/portainer/rbac/index.js
+++ b/app/portainer/rbac/index.js
@@ -1,7 +1,6 @@
import { AccessHeaders } from '../authorization-guard';
import { rolesView } from './views/roles';
import { accessViewer } from './components/access-viewer';
-import { accessViewerDatatable } from './components/access-viewer/access-viewer-datatable';
import { rolesDatatable } from './components/roles-datatable';
import { RoleService } from './services/role.service';
@@ -11,7 +10,6 @@ angular
.module('portainer.rbac', ['ngResource'])
.constant('API_ENDPOINT_ROLES', 'api/roles')
.component('accessViewer', accessViewer)
- .component('accessViewerDatatable', accessViewerDatatable)
.component('rolesDatatable', rolesDatatable)
.component('rolesView', rolesView)
.factory('RoleService', RoleService)
diff --git a/app/portainer/rbac/models/access.js b/app/portainer/rbac/models/access.js
deleted file mode 100644
index 18b7269fe..000000000
--- a/app/portainer/rbac/models/access.js
+++ /dev/null
@@ -1,16 +0,0 @@
-export default function AccessViewerPolicyModel(policy, endpoint, roles, group, team) {
- this.EndpointId = endpoint.Id;
- this.EndpointName = endpoint.Name;
- this.RoleId = policy.RoleId;
- this.RoleName = roles[policy.RoleId].Name;
- this.RolePriority = roles[policy.RoleId].Priority;
- if (group) {
- this.GroupId = group.Id;
- this.GroupName = group.Name;
- }
- if (team) {
- this.TeamId = team.Id;
- this.TeamName = team.Name;
- }
- this.AccessLocation = group ? 'environment group' : 'environment';
-}
diff --git a/app/portainer/react/components/users.ts b/app/portainer/react/components/users.ts
index 74c378c62..c8d9f4117 100644
--- a/app/portainer/react/components/users.ts
+++ b/app/portainer/react/components/users.ts
@@ -4,10 +4,17 @@ import { r2a } from '@/react-tools/react2angular';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { UsersDatatable } from '@/react/portainer/users/ListView/UsersDatatable/UsersDatatable';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
+import { EffectiveAccessViewerDatatable } from '@/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable';
export const usersModule = angular
.module('portainer.app.react.components.users', [])
.component(
'usersDatatable',
r2a(withUIRouter(withCurrentUser(UsersDatatable)), ['dataset', 'onRemove'])
+ )
+ .component(
+ 'effectiveAccessViewerDatatable',
+ r2a(withUIRouter(withCurrentUser(EffectiveAccessViewerDatatable)), [
+ 'dataset',
+ ])
).name;
diff --git a/app/portainer/users/types.ts b/app/portainer/users/types.ts
index 2cb44c00d..b62fb75f5 100644
--- a/app/portainer/users/types.ts
+++ b/app/portainer/users/types.ts
@@ -1,4 +1,5 @@
import { EnvironmentId } from '@/react/portainer/environments/types';
+import { AuthorizationMap } from '@/react/portainer/users/RolesView/types';
import { type UserId } from './types/user-id';
@@ -16,10 +17,6 @@ export const RoleNames: { [key in Role]: string } = {
[Role.EdgeAdmin]: 'edge administrator',
};
-interface AuthorizationMap {
- [authorization: string]: boolean;
-}
-
export type User = {
Id: UserId;
Username: string;
diff --git a/app/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable.tsx b/app/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable.tsx
new file mode 100644
index 000000000..530bad8ce
--- /dev/null
+++ b/app/react/portainer/users/RolesView/AccessViewer/EffectiveAccessViewerDatatable.tsx
@@ -0,0 +1,36 @@
+import { TextTip } from '@@/Tip/TextTip';
+import { Datatable } from '@@/datatables';
+import { useTableStateWithStorage } from '@@/datatables/useTableState';
+
+import { AccessViewerPolicyModel } from './model';
+import { columns } from './columns';
+
+export function EffectiveAccessViewerDatatable({
+ dataset,
+}: {
+ dataset?: Array;
+}) {
+ const tableState = useTableStateWithStorage('access-viewer', 'Environment');
+
+ return (
+
+ Effective role for each environment will be displayed for the selected
+ user
+
+ }
+ emptyContentLabel={
+ dataset
+ ? 'The selected user does not have access to any environment(s)'
+ : 'Select a user to show associated access and role'
+ }
+ disableSelect
+ />
+ );
+}
diff --git a/app/react/portainer/users/RolesView/AccessViewer/columns.tsx b/app/react/portainer/users/RolesView/AccessViewer/columns.tsx
new file mode 100644
index 000000000..41cbfe2c8
--- /dev/null
+++ b/app/react/portainer/users/RolesView/AccessViewer/columns.tsx
@@ -0,0 +1,81 @@
+import { createColumnHelper, CellContext } from '@tanstack/react-table';
+import { Users } from 'lucide-react';
+
+import { useCurrentUser } from '@/react/hooks/useUser';
+
+import { Icon } from '@@/Icon';
+import { Link } from '@@/Link';
+
+import { AccessViewerPolicyModel } from './model';
+
+const helper = createColumnHelper();
+
+export const columns = [
+ helper.accessor('EndpointName', {
+ header: 'Environment',
+ id: 'Environment',
+ }),
+ helper.accessor('RoleName', {
+ header: 'Role',
+ id: 'Role',
+ }),
+ helper.display({
+ header: 'Access Origin',
+ cell: AccessCell,
+ }),
+];
+
+function AccessCell({
+ row: { original: item },
+}: CellContext) {
+ const { isPureAdmin } = useCurrentUser();
+
+ if (item.RoleId === 0) {
+ return (
+ <>
+ User access all environments
+
+ Manage access
+
+ >
+ );
+ }
+
+ return (
+ <>
+ {prefix(item.TeamName)} access defined on {item.AccessLocation}{' '}
+ {!!item.GroupName && {item.GroupName}
}{' '}
+ {manageAccess(item, isPureAdmin)}
+ >
+ );
+}
+
+function prefix(teamName: string | undefined) {
+ if (!teamName) {
+ return 'User';
+ }
+ return (
+ <>
+ Team {teamName}
+ >
+ );
+}
+
+function manageAccess(item: AccessViewerPolicyModel, isPureAdmin: boolean) {
+ if (!isPureAdmin) {
+ return null;
+ }
+
+ return item.GroupName ? (
+
+ Manage access
+
+ ) : (
+
+ Manage access
+
+ );
+}
diff --git a/app/react/portainer/users/RolesView/AccessViewer/model.ts b/app/react/portainer/users/RolesView/AccessViewer/model.ts
new file mode 100644
index 000000000..264c01ef7
--- /dev/null
+++ b/app/react/portainer/users/RolesView/AccessViewer/model.ts
@@ -0,0 +1,53 @@
+import {
+ Environment,
+ EnvironmentId,
+} from '@/react/portainer/environments/types';
+import { EnvironmentGroup } from '@/react/portainer/environments/environment-groups/types';
+
+import { RbacRole } from '../types';
+import { Team, TeamId } from '../../teams/types';
+
+export class AccessViewerPolicyModel {
+ EndpointId: EnvironmentId;
+
+ EndpointName: string;
+
+ RoleId: RbacRole['Id'];
+
+ RoleName: RbacRole['Name'];
+
+ RolePriority: RbacRole['Priority'];
+
+ GroupId?: EnvironmentGroup['Id'];
+
+ GroupName?: EnvironmentGroup['Name'];
+
+ TeamId?: TeamId;
+
+ TeamName?: Team['Name'];
+
+ AccessLocation: string;
+
+ constructor(
+ policy: { RoleId: RbacRole['Id'] },
+ endpoint: Environment,
+ roles: Record,
+ group?: EnvironmentGroup,
+ team?: Team
+ ) {
+ this.EndpointId = endpoint.Id;
+ this.EndpointName = endpoint.Name;
+ this.RoleId = policy.RoleId;
+ this.RoleName = roles[policy.RoleId].Name;
+ this.RolePriority = roles[policy.RoleId].Priority;
+ if (group) {
+ this.GroupId = group.Id;
+ this.GroupName = group.Name;
+ }
+ if (team) {
+ this.TeamId = team.Id;
+ this.TeamName = team.Name;
+ }
+ this.AccessLocation = group ? 'environment group' : 'environment';
+ }
+}
diff --git a/app/react/portainer/users/RolesView/types.ts b/app/react/portainer/users/RolesView/types.ts
new file mode 100644
index 000000000..ca4e6d5bc
--- /dev/null
+++ b/app/react/portainer/users/RolesView/types.ts
@@ -0,0 +1,11 @@
+export interface AuthorizationMap {
+ [authorization: string]: boolean;
+}
+
+export interface RbacRole {
+ Id: number;
+ Name: string;
+ Description: string;
+ Authorizations: AuthorizationMap;
+ Priority: number;
+}