From a439695248705ecaf0448d1678ed39f4e97879d3 Mon Sep 17 00:00:00 2001 From: Chaim Lev-Ari Date: Wed, 3 Apr 2024 17:38:32 +0300 Subject: [PATCH] refactor(users): migrate users table to react [EE-4708] (#10759) --- .../users-datatable/usersDatatable.html | 132 ------------------ .../users-datatable/usersDatatable.js | 18 --- .../usersDatatableController.js | 15 -- app/portainer/react/components/index.ts | 2 + app/portainer/react/components/users.ts | 13 ++ app/portainer/users/types.ts | 6 + app/portainer/views/users/users.html | 15 +- app/portainer/views/users/usersController.js | 20 +-- app/react/components/Widget/WidgetTitle.tsx | 2 +- app/react/components/datatables/TableRow.tsx | 2 +- app/react/portainer/settings/types.ts | 4 +- app/react/portainer/users/.keep | 0 .../UsersDatatable/UsersDatatable.tsx | 40 ++++++ .../UsersDatatable/columns/authentication.tsx | 5 + .../ListView/UsersDatatable/columns/helper.ts | 5 + .../ListView/UsersDatatable/columns/index.ts | 5 + .../ListView/UsersDatatable/columns/name.tsx | 32 +++++ .../ListView/UsersDatatable/columns/role.tsx | 29 ++++ .../users/ListView/UsersDatatable/types.ts | 6 + 19 files changed, 159 insertions(+), 192 deletions(-) delete mode 100644 app/portainer/components/datatables/users-datatable/usersDatatable.html delete mode 100644 app/portainer/components/datatables/users-datatable/usersDatatable.js delete mode 100644 app/portainer/components/datatables/users-datatable/usersDatatableController.js create mode 100644 app/portainer/react/components/users.ts delete mode 100644 app/react/portainer/users/.keep create mode 100644 app/react/portainer/users/ListView/UsersDatatable/UsersDatatable.tsx create mode 100644 app/react/portainer/users/ListView/UsersDatatable/columns/authentication.tsx create mode 100644 app/react/portainer/users/ListView/UsersDatatable/columns/helper.ts create mode 100644 app/react/portainer/users/ListView/UsersDatatable/columns/index.ts create mode 100644 app/react/portainer/users/ListView/UsersDatatable/columns/name.tsx create mode 100644 app/react/portainer/users/ListView/UsersDatatable/columns/role.tsx create mode 100644 app/react/portainer/users/ListView/UsersDatatable/types.ts 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 @@ -
- - -
-
-
- -
- {{ $ctrl.titleText }} -
- - - -
- -
-
- -
- - - - - - - - - - - - - - - - - - - - - -
-
- - - - - -
-
- - - -
- - - - - {{ item.Username }} - {{ item.Username }} - - - - - - {{ item.RoleName ? item.RoleName : '-' }} - - - Internal - LDAP - OAuth -
Loading...
No user available.
-
- -
-
-
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; +};