diff --git a/app/portainer/components/datatables/users-datatable/usersDatatable.html b/app/portainer/components/datatables/users-datatable/usersDatatable.html
deleted file mode 100644
index c29461894..000000000
--- a/app/portainer/components/datatables/users-datatable/usersDatatable.html
+++ /dev/null
@@ -1,132 +0,0 @@
-
diff --git a/app/portainer/components/datatables/users-datatable/usersDatatable.js b/app/portainer/components/datatables/users-datatable/usersDatatable.js
deleted file mode 100644
index fbad8cb28..000000000
--- a/app/portainer/components/datatables/users-datatable/usersDatatable.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import angular from 'angular';
-import UsersDatatableController from './usersDatatableController';
-
-angular.module('portainer.app').component('usersDatatable', {
- templateUrl: './usersDatatable.html',
- controller: UsersDatatableController,
- bindings: {
- titleText: '@',
- titleIcon: '@',
- dataset: '<',
- tableKey: '@',
- orderBy: '@',
- reverseOrder: '<',
- removeAction: '<',
- authenticationMethod: '<',
- isAdmin: '<',
- },
-});
diff --git a/app/portainer/components/datatables/users-datatable/usersDatatableController.js b/app/portainer/components/datatables/users-datatable/usersDatatableController.js
deleted file mode 100644
index 8b8a60605..000000000
--- a/app/portainer/components/datatables/users-datatable/usersDatatableController.js
+++ /dev/null
@@ -1,15 +0,0 @@
-export default class UsersDatatableController {
- /* @ngInject*/
- constructor($controller, $scope) {
- const allowSelection = this.allowSelection;
- angular.extend(this, $controller('GenericDatatableController', { $scope }));
- this.allowSelection = allowSelection.bind(this);
- }
-
- /**
- * Override this method to allow/deny selection
- */
- allowSelection(item) {
- return item.Id !== 1;
- }
-}
diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts
index 7f1262010..61269e8ed 100644
--- a/app/portainer/react/components/index.ts
+++ b/app/portainer/react/components/index.ts
@@ -47,6 +47,7 @@ import { accessControlModule } from './access-control';
import { environmentsModule } from './environments';
import { registriesModule } from './registries';
import { accountModule } from './account';
+import { usersModule } from './users';
export const ngModule = angular
.module('portainer.app.react.components', [
@@ -57,6 +58,7 @@ export const ngModule = angular
registriesModule,
settingsModule,
accountModule,
+ usersModule,
])
.component(
'tagSelector',
diff --git a/app/portainer/react/components/users.ts b/app/portainer/react/components/users.ts
new file mode 100644
index 000000000..74c378c62
--- /dev/null
+++ b/app/portainer/react/components/users.ts
@@ -0,0 +1,13 @@
+import angular from 'angular';
+
+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';
+
+export const usersModule = angular
+ .module('portainer.app.react.components.users', [])
+ .component(
+ 'usersDatatable',
+ r2a(withUIRouter(withCurrentUser(UsersDatatable)), ['dataset', 'onRemove'])
+ ).name;
diff --git a/app/portainer/users/types.ts b/app/portainer/users/types.ts
index fb686813c..2cb44c00d 100644
--- a/app/portainer/users/types.ts
+++ b/app/portainer/users/types.ts
@@ -10,6 +10,12 @@ export enum Role {
EdgeAdmin,
}
+export const RoleNames: { [key in Role]: string } = {
+ [Role.Admin]: 'administrator',
+ [Role.Standard]: 'user',
+ [Role.EdgeAdmin]: 'edge administrator',
+};
+
interface AuthorizationMap {
[authorization: string]: boolean;
}
diff --git a/app/portainer/views/users/users.html b/app/portainer/views/users/users.html
index 7043b51a9..4b59705b0 100644
--- a/app/portainer/views/users/users.html
+++ b/app/portainer/views/users/users.html
@@ -166,17 +166,4 @@
-
+
diff --git a/app/portainer/views/users/usersController.js b/app/portainer/views/users/usersController.js
index c898ea732..09e48609c 100644
--- a/app/portainer/views/users/usersController.js
+++ b/app/portainer/views/users/usersController.js
@@ -1,5 +1,5 @@
import _ from 'lodash-es';
-import { confirmDelete } from '@@/modals/confirm';
+import { AuthenticationMethod } from '@/react/portainer/settings/types';
angular.module('portainer.app').controller('UsersController', [
'$q',
@@ -91,12 +91,7 @@ angular.module('portainer.app').controller('UsersController', [
}
$scope.removeAction = function (selectedItems) {
- confirmDelete('Do you want to remove the selected users? They will not be able to login into Portainer anymore.').then((confirmed) => {
- if (!confirmed) {
- return;
- }
- deleteSelectedUsers(selectedItems);
- });
+ return deleteSelectedUsers(selectedItems);
};
function assignTeamLeaders(users, memberships) {
@@ -107,7 +102,6 @@ angular.module('portainer.app').controller('UsersController', [
var membership = memberships[j];
if (user.Id === membership.UserId && membership.Role === 1) {
user.isTeamLeader = true;
- user.RoleName = 'team leader';
break;
}
}
@@ -125,11 +119,12 @@ angular.module('portainer.app').controller('UsersController', [
settings: SettingsService.publicSettings(),
})
.then(function success(data) {
+ $scope.AuthenticationMethod = data.settings.AuthenticationMethod;
var users = data.users;
assignTeamLeaders(users, data.memberships);
+ users = assignAuthMethod(users, $scope.AuthenticationMethod);
$scope.users = users;
$scope.teams = _.orderBy(data.teams, 'Name', 'asc');
- $scope.AuthenticationMethod = data.settings.AuthenticationMethod;
$scope.requiredPasswordLength = data.settings.RequiredPasswordLength;
$scope.teamSync = data.settings.TeamSync;
})
@@ -143,3 +138,10 @@ angular.module('portainer.app').controller('UsersController', [
initView();
},
]);
+
+function assignAuthMethod(users, authMethod) {
+ return users.map((u) => ({
+ ...u,
+ authMethod: AuthenticationMethod[u.Id === 1 ? AuthenticationMethod.Internal : authMethod],
+ }));
+}
diff --git a/app/react/components/Widget/WidgetTitle.tsx b/app/react/components/Widget/WidgetTitle.tsx
index 15c7bfdd4..2623c374c 100644
--- a/app/react/components/Widget/WidgetTitle.tsx
+++ b/app/react/components/Widget/WidgetTitle.tsx
@@ -26,7 +26,7 @@ export function WidgetTitle({
- {title}
+ {title}
{children}
diff --git a/app/react/components/datatables/TableRow.tsx b/app/react/components/datatables/TableRow.tsx
index de7426448..7a6aa681e 100644
--- a/app/react/components/datatables/TableRow.tsx
+++ b/app/react/components/datatables/TableRow.tsx
@@ -20,7 +20,7 @@ export function TableRow({
onClick={onClick}
>
{cells.map((cell) => (
-
+ |
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
))}
diff --git a/app/react/portainer/settings/types.ts b/app/react/portainer/settings/types.ts
index 6f5126855..f105e0b87 100644
--- a/app/react/portainer/settings/types.ts
+++ b/app/react/portainer/settings/types.ts
@@ -72,11 +72,11 @@ export interface OAuthSettings {
KubeSecretKey: string;
}
-enum AuthenticationMethod {
+export enum AuthenticationMethod {
/**
* Internal represents the internal authentication method (authentication against Portainer API)
*/
- Internal,
+ Internal = 1,
/**
* LDAP represents the LDAP authentication method (authentication against a LDAP server)
*/
diff --git a/app/react/portainer/users/.keep b/app/react/portainer/users/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/react/portainer/users/ListView/UsersDatatable/UsersDatatable.tsx b/app/react/portainer/users/ListView/UsersDatatable/UsersDatatable.tsx
new file mode 100644
index 000000000..64504602d
--- /dev/null
+++ b/app/react/portainer/users/ListView/UsersDatatable/UsersDatatable.tsx
@@ -0,0 +1,40 @@
+import { User as UserIcon } from 'lucide-react';
+
+import { Datatable } from '@@/datatables';
+import { useTableState } from '@@/datatables/useTableState';
+import { createPersistedStore } from '@@/datatables/types';
+import { DeleteButton } from '@@/buttons/DeleteButton';
+
+import { columns } from './columns';
+import { DecoratedUser } from './types';
+
+const store = createPersistedStore('users');
+
+export function UsersDatatable({
+ dataset,
+ onRemove,
+}: {
+ dataset?: Array;
+ onRemove: (selectedItems: Array) => void;
+}) {
+ const tableState = useTableState(store, 'users');
+
+ return (
+ row.original.Id !== 1}
+ renderTableActions={(selectedItems) => (
+ onRemove(selectedItems)}
+ />
+ )}
+ />
+ );
+}
diff --git a/app/react/portainer/users/ListView/UsersDatatable/columns/authentication.tsx b/app/react/portainer/users/ListView/UsersDatatable/columns/authentication.tsx
new file mode 100644
index 000000000..35fbf0056
--- /dev/null
+++ b/app/react/portainer/users/ListView/UsersDatatable/columns/authentication.tsx
@@ -0,0 +1,5 @@
+import { helper } from './helper';
+
+export const authentication = helper.accessor('authMethod', {
+ header: 'Authentication',
+});
diff --git a/app/react/portainer/users/ListView/UsersDatatable/columns/helper.ts b/app/react/portainer/users/ListView/UsersDatatable/columns/helper.ts
new file mode 100644
index 000000000..4cf192571
--- /dev/null
+++ b/app/react/portainer/users/ListView/UsersDatatable/columns/helper.ts
@@ -0,0 +1,5 @@
+import { createColumnHelper } from '@tanstack/react-table';
+
+import { DecoratedUser } from '../types';
+
+export const helper = createColumnHelper();
diff --git a/app/react/portainer/users/ListView/UsersDatatable/columns/index.ts b/app/react/portainer/users/ListView/UsersDatatable/columns/index.ts
new file mode 100644
index 000000000..28d4308ca
--- /dev/null
+++ b/app/react/portainer/users/ListView/UsersDatatable/columns/index.ts
@@ -0,0 +1,5 @@
+import { authentication } from './authentication';
+import { name } from './name';
+import { role } from './role';
+
+export const columns = [name, role, authentication];
diff --git a/app/react/portainer/users/ListView/UsersDatatable/columns/name.tsx b/app/react/portainer/users/ListView/UsersDatatable/columns/name.tsx
new file mode 100644
index 000000000..775b0f1fd
--- /dev/null
+++ b/app/react/portainer/users/ListView/UsersDatatable/columns/name.tsx
@@ -0,0 +1,32 @@
+import { CellContext } from '@tanstack/react-table';
+
+import { useCurrentUser } from '@/react/hooks/useUser';
+
+import { Link } from '@@/Link';
+
+import { DecoratedUser } from '../types';
+
+import { helper } from './helper';
+
+export const name = helper.accessor('Username', {
+ header: 'Name',
+ cell: Cell,
+});
+
+function Cell({
+ getValue,
+ row: { original: item },
+}: CellContext) {
+ const { isPureAdmin } = useCurrentUser();
+ const name = getValue();
+
+ if (!isPureAdmin) {
+ return <>{name}>;
+ }
+
+ return (
+
+ {name}
+
+ );
+}
diff --git a/app/react/portainer/users/ListView/UsersDatatable/columns/role.tsx b/app/react/portainer/users/ListView/UsersDatatable/columns/role.tsx
new file mode 100644
index 000000000..252993a09
--- /dev/null
+++ b/app/react/portainer/users/ListView/UsersDatatable/columns/role.tsx
@@ -0,0 +1,29 @@
+import { User, UserPlus } from 'lucide-react';
+
+import { isEdgeAdmin } from '@/portainer/users/user.helpers';
+import { RoleNames } from '@/portainer/users/types';
+
+import { Icon } from '@@/Icon';
+
+import { helper } from './helper';
+
+export const role = helper.accessor(
+ (item) =>
+ `${RoleNames[item.Role]} ${
+ item.isTeamLeader ? ' - team leader' : ''
+ }`.trim(),
+ {
+ header: 'Role',
+ cell: ({ getValue, row: { original: item } }) => {
+ const icon =
+ isEdgeAdmin({ Role: item.Role }) || item.isTeamLeader ? User : UserPlus;
+
+ return (
+
+
+ {getValue() || '-'}
+
+ );
+ },
+ }
+);
diff --git a/app/react/portainer/users/ListView/UsersDatatable/types.ts b/app/react/portainer/users/ListView/UsersDatatable/types.ts
new file mode 100644
index 000000000..e85191ea3
--- /dev/null
+++ b/app/react/portainer/users/ListView/UsersDatatable/types.ts
@@ -0,0 +1,6 @@
+import { type User } from '@/portainer/users/types';
+
+export type DecoratedUser = User & {
+ isTeamLeader?: boolean;
+ authMethod: string;
+};