refactor(users): migrate users table to react [EE-4708] (#10759)

pull/11495/head
Chaim Lev-Ari 8 months ago committed by GitHub
parent 86f1b8df6e
commit a439695248
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,132 +0,0 @@
<div class="datatable">
<rd-widget>
<rd-widget-body classes="no-padding">
<div class="toolBar vertical-center flex-wrap !gap-x-5 !gap-y-1">
<div class="toolBarTitle vertical-center">
<div class="widget-icon space-right">
<pr-icon icon="$ctrl.titleIcon"></pr-icon>
</div>
{{ $ctrl.titleText }}
</div>
<div class="searchBar vertical-center">
<pr-icon icon="'search'"></pr-icon>
<input
type="text"
class="searchInput"
ng-model="$ctrl.state.textFilter"
ng-change="$ctrl.onTextFilterChange()"
placeholder="Search..."
ng-model-options="{ debounce: 300 }"
/>
</div>
<div class="actionBar !gap-3" ng-show="$ctrl.isAdmin">
<button
type="button"
class="btn btn-sm btn-dangerlight vertical-center !ml-0 h-fit"
ng-disabled="$ctrl.state.selectedItemCount === 0"
ng-click="$ctrl.removeAction($ctrl.state.selectedItems)"
data-cy="user-removeUserButton"
>
<pr-icon icon="'trash-2'"></pr-icon>Remove
</button>
</div>
</div>
<div class="table-responsive">
<table class="table-hover nowrap-cells table" data-cy="user-userTable">
<thead>
<tr>
<th>
<div class="vertical-center">
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
<input id="select_all" type="checkbox" ng-model="$ctrl.state.selectAll" ng-change="$ctrl.selectAll()" />
<label for="select_all"></label>
</span>
<table-column-header
col-title="'Name'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'Username'"
is-sorted-desc="$ctrl.state.orderBy === 'Username' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('Username')"
></table-column-header>
</div>
</th>
<th>
<table-column-header
col-title="'Role'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'RoleName'"
is-sorted-desc="$ctrl.state.orderBy === 'RoleName' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('RoleName')"
></table-column-header>
</th>
<th>
<table-column-header
col-title="'Authentication'"
can-sort="true"
is-sorted="$ctrl.state.orderBy === 'AuthenticationMethod'"
is-sorted-desc="$ctrl.state.orderBy === 'AuthenticationMethod' && $ctrl.state.reverseOrder"
ng-click="$ctrl.changeOrderBy('AuthenticationMethod')"
></table-column-header>
</th>
</tr>
</thead>
<tbody>
<tr
dir-paginate="item in ($ctrl.state.filteredDataSet = ($ctrl.dataset | filter:$ctrl.state.textFilter | orderBy:$ctrl.state.orderBy:$ctrl.state.reverseOrder | itemsPerPage: $ctrl.state.paginatedItemLimit))"
ng-class="{ active: item.Checked }"
>
<td>
<span class="md-checkbox" ng-show="$ctrl.isAdmin">
<input id="select_{{ $index }}" type="checkbox" ng-model="item.Checked" ng-click="$ctrl.selectItem(item, $event)" ng-disabled="!$ctrl.allowSelection(item)" />
<label for="select_{{ $index }}"></label>
</span>
<a ui-sref="portainer.users.user({id: item.Id})" ng-show="$ctrl.isAdmin">{{ item.Username }}</a>
<span ng-show="!$ctrl.isAdmin">{{ item.Username }}</span>
</td>
<td>
<span class="vertical-center">
<pr-icon icon="'user'" ng-if="item.Role === 1 && !item.isTeamLeader"></pr-icon>
<pr-icon icon="'user'" ng-if="item.Role !== 1 && item.isTeamLeader"></pr-icon>
<pr-icon icon="'user-plus'" ng-if="item.Role !== 1 && !item.isTeamLeader"></pr-icon>
{{ item.RoleName ? item.RoleName : '-' }}
</span>
</td>
<td>
<span ng-if="item.Id === 1 || ($ctrl.authenticationMethod !== 2 && $ctrl.authenticationMethod !== 3)">Internal</span>
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 2">LDAP</span>
<span ng-if="item.Id !== 1 && $ctrl.authenticationMethod === 3">OAuth</span>
</td>
</tr>
<tr ng-if="!$ctrl.dataset">
<td colspan="3" class="text-muted text-center">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.filteredDataSet.length === 0">
<td colspan="3" class="text-muted text-center">No user available.</td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.dataset">
<div class="infoBar" ng-if="$ctrl.state.selectedItemCount !== 0"> {{ $ctrl.state.selectedItemCount }} item(s) selected </div>
<div class="paginationControls">
<form class="form-inline">
<span class="limitSelector">
<span style="margin-right: 5px"> Items per page </span>
<select class="form-control" ng-model="$ctrl.state.paginatedItemLimit" ng-change="$ctrl.changePaginationLimit()" data-cy="component-paginationSelect">
<option value="0">All</option>
<option value="10">10</option>
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
</select>
</span>
<dir-pagination-controls max-size="5"></dir-pagination-controls>
</form>
</div>
</div>
</rd-widget-body>
</rd-widget>
</div>

@ -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: '<',
},
});

@ -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;
}
}

@ -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',

@ -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;

@ -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;
}

@ -166,17 +166,4 @@
</div>
</div>
<div class="row">
<div class="col-sm-12">
<users-datatable
title-text="Users"
title-icon="user"
dataset="users"
table-key="users"
order-by="Username"
authentication-method="AuthenticationMethod"
remove-action="removeAction"
is-admin="isAdmin"
></users-datatable>
</div>
</div>
<users-datatable dataset="users" on-remove="(removeAction)"></users-datatable>

@ -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],
}));
}

@ -26,7 +26,7 @@ export function WidgetTitle({
<div className="widget-icon">
<Icon icon={icon} className="space-right" />
</div>
<span>{title}</span>
<h2 className="text-base m-0">{title}</h2>
</span>
<span className={clsx('pull-right', className)}>{children}</span>
</div>

@ -20,7 +20,7 @@ export function TableRow<D extends DefaultType = DefaultType>({
onClick={onClick}
>
{cells.map((cell) => (
<td key={cell.id}>
<td key={cell.id} className={cell.column.columnDef.meta?.className}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}

@ -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)
*/

@ -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<DecoratedUser>;
onRemove: (selectedItems: Array<DecoratedUser>) => void;
}) {
const tableState = useTableState(store, 'users');
return (
<Datatable
columns={columns}
dataset={dataset || []}
isLoading={!dataset}
title="Users"
titleIcon={UserIcon}
settingsManager={tableState}
isRowSelectable={(row) => row.original.Id !== 1}
renderTableActions={(selectedItems) => (
<DeleteButton
disabled={selectedItems.length === 0}
confirmMessage="Do you want to remove the selected users? They will not be able to login into Portainer anymore."
onConfirmed={() => onRemove(selectedItems)}
/>
)}
/>
);
}

@ -0,0 +1,5 @@
import { helper } from './helper';
export const authentication = helper.accessor('authMethod', {
header: 'Authentication',
});

@ -0,0 +1,5 @@
import { createColumnHelper } from '@tanstack/react-table';
import { DecoratedUser } from '../types';
export const helper = createColumnHelper<DecoratedUser>();

@ -0,0 +1,5 @@
import { authentication } from './authentication';
import { name } from './name';
import { role } from './role';
export const columns = [name, role, authentication];

@ -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<DecoratedUser, 'string'>) {
const { isPureAdmin } = useCurrentUser();
const name = getValue();
if (!isPureAdmin) {
return <>{name}</>;
}
return (
<Link to=".user" params={{ id: item.Id }}>
{name}
</Link>
);
}

@ -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 (
<span className="vertical-center">
<Icon icon={icon} />
{getValue() || '-'}
</span>
);
},
}
);

@ -0,0 +1,6 @@
import { type User } from '@/portainer/users/types';
export type DecoratedUser = User & {
isTeamLeader?: boolean;
authMethod: string;
};
Loading…
Cancel
Save