refactor(activity-logs): migrate auth logs table to react [EE-4715] (#10890)

pull/10792/head
Chaim Lev-Ari 2024-04-09 08:10:25 +03:00 committed by GitHub
parent bd271ec5a1
commit 7e53d01d0f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 181 additions and 197 deletions

View File

@ -0,0 +1,24 @@
import angular from 'angular';
import { r2a } from '@/react-tools/react2angular';
import { withUIRouter } from '@/react-tools/withUIRouter';
import { withReactQuery } from '@/react-tools/withReactQuery';
import { AuthenticationLogsTable } from '@/react/portainer/logs/AuthenticationLogsView/AuthenticationLogsTable';
export const activityLogsModule = angular
.module('portainer.app.react.components.activity-logs', [])
.component(
'authenticationLogsTable',
r2a(withUIRouter(withReactQuery(AuthenticationLogsTable)), [
'currentPage',
'dataset',
'keyword',
'limit',
'totalItems',
'sort',
'onChangeSort',
'onChangePage',
'onChangeLimit',
'onChangeKeyword',
])
).name;

View File

@ -48,6 +48,7 @@ import { environmentsModule } from './environments';
import { registriesModule } from './registries';
import { accountModule } from './account';
import { usersModule } from './users';
import { activityLogsModule } from './activity-logs';
export const ngModule = angular
.module('portainer.app.react.components', [
@ -59,6 +60,7 @@ export const ngModule = angular
settingsModule,
accountModule,
usersModule,
activityLogsModule,
])
.component(
'tagSelector',

View File

@ -1,68 +0,0 @@
import { authenticationMethodTypesMap, authenticationMethodTypesLabels } from '@/portainer/settings/authentication/auth-method-constants';
import { authenticationActivityTypesMap, authenticationActivityTypesLabels } from '@/portainer/settings/authentication/auth-type-constants';
class ActivityLogsDatatableController {
/* @ngInject */
constructor($controller, $scope, PaginationService) {
this.PaginationService = PaginationService;
this.tableKey = 'authLogs';
this.contextFilterLabels = Object.values(authenticationMethodTypesMap).map((value) => ({ value, label: authenticationMethodTypesLabels[value] }));
this.typeFilterLabels = Object.values(authenticationActivityTypesMap).map((value) => ({ value, label: authenticationActivityTypesLabels[value] }));
const $onInit = this.$onInit;
angular.extend(this, $controller('GenericDatatableController', { $scope }));
this.$onInit = $onInit.bind(this);
this.changeSort = this.changeSort.bind(this);
this.handleChangeLimit = this.handleChangeLimit.bind(this);
}
changeSort(key) {
let desc = false;
if (key === this.sort.key) {
desc = !this.sort.desc;
}
this.onChangeSort({ key, desc });
}
contextType(context) {
if (!(context in authenticationMethodTypesLabels)) {
return '';
}
return authenticationMethodTypesLabels[context];
}
activityType(type) {
if (!(type in authenticationActivityTypesLabels)) {
return '';
}
return authenticationActivityTypesLabels[type];
}
isAuthSuccess(type) {
return type === authenticationActivityTypesMap.AuthSuccess;
}
isAuthFailure(type) {
return type === authenticationActivityTypesMap.AuthFailure;
}
handleChangeLimit(limit) {
this.PaginationService.setPaginationLimit(this.tableKey, limit);
this.onChangeLimit(limit);
}
$onInit() {
this.$onInitGeneric();
const limit = this.PaginationService.getPaginationLimit(this.tableKey);
if (limit) {
this.handleChangeLimit(+limit);
}
}
}
export default ActivityLogsDatatableController;

View File

@ -1,99 +0,0 @@
<div class="datatable datatable-empty">
<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="'history'"></pr-icon>
</div>
Authentication Events
</div>
<div class="vertical-center">
<datatable-searchbar on-change="($ctrl.onChangeKeyword)"></datatable-searchbar>
</div>
</div>
<div class="table-responsive">
<table class="table-hover nowrap-cells table">
<thead>
<tr>
<th>
<div class="vertical-center">
<table-column-header
col-title="'Time'"
can-sort="true"
is-sorted="$ctrl.sort.key === 'Timestamp'"
is-sorted-desc="$ctrl.sort.key === 'Timestamp' && $ctrl.sort.desc"
ng-click="$ctrl.changeSort('Timestamp')"
>
</table-column-header>
</div>
</th>
<th>
<div class="vertical-center">
<table-column-header
col-title="'Origin'"
can-sort="true"
is-sorted="$ctrl.sort.key === 'Origin'"
is-sorted-desc="$ctrl.sort.key === 'Origin' && $ctrl.sort.desc"
ng-click="$ctrl.changeSort('Origin')"
>
</table-column-header>
</div>
</th>
<th>
<div class="vertical-center">
<table-column-header
col-title="'Context'"
can-sort="true"
is-sorted="$ctrl.sort.key === 'Context'"
is-sorted-desc="$ctrl.sort.key === 'Context' && $ctrl.sort.desc"
ng-click="$ctrl.changeSort('Context')"
>
</table-column-header>
<datatable-filter labels="$ctrl.contextFilterLabels" filter-key="context" state="$ctrl.contextFilter" on-change="($ctrl.onChangeContextFilter)">
</datatable-filter>
</div>
</th>
<th>
<div class="vertical-center">
<table-column-header
col-title="'User'"
can-sort="true"
is-sorted="$ctrl.sort.key === 'Username'"
is-sorted-desc="$ctrl.sort.key === 'Username' && $ctrl.sort.desc"
ng-click="$ctrl.changeSort('Username')"
>
</table-column-header>
</div>
</th>
<th>
<div class="vertical-center">
<table-column-header col-title="'Result'" can-sort="false"> </table-column-header>
<datatable-filter labels="$ctrl.typeFilterLabels" filter-key="type" state="$ctrl.typeFilter" on-change="($ctrl.onChangeTypeFilter)"> </datatable-filter>
</div>
</th>
</tr>
</thead>
<tbody>
<tr dir-paginate="item in $ctrl.logs | itemsPerPage: $ctrl.limit" total-items="$ctrl.totalItems" current-page="$ctrl.currentPage">
<td></td>
<td></td>
<td></td>
<td></td>
<td></td>
</tr>
<tr ng-if="!$ctrl.logs">
<td class="text-muted text-center" colspan="5">Loading...</td>
</tr>
<tr ng-if="$ctrl.state.logs.length === 0">
<td class="text-muted text-center" colspan="8"> No logs available. </td>
</tr>
</tbody>
</table>
</div>
<div class="footer" ng-if="$ctrl.logs">
<datatable-pagination limit="$ctrl.limit" on-change-limit="($ctrl.handleChangeLimit)" on-change-page="($ctrl.onChangePage)"></datatable-pagination>
</div>
</rd-widget-body>
</rd-widget>
</div>

View File

@ -1,25 +0,0 @@
import controller from './auth-logs-datatable.controller';
export const authLogsDatatable = {
templateUrl: './auth-logs-datatable.html',
controller,
bindings: {
logs: '<',
keyword: '<',
sort: '<',
limit: '<',
totalItems: '<',
currentPage: '<',
contextFilter: '<',
typeFilter: '<',
feature: '@',
onChangeContextFilter: '<',
onChangeTypeFilter: '<',
onChangeKeyword: '<',
onChangeSort: '<',
onChangeLimit: '<',
onChangePage: '<',
},
};

View File

@ -26,8 +26,8 @@
</rd-widget-body>
</rd-widget>
<div class="row mt-5">
<auth-logs-datatable
logs="$ctrl.state.logs"
<authentication-logs-table
dataset="$ctrl.state.logs"
keyword="$ctrl.state.keyword"
sort="$ctrl.state.sort"
limit="$ctrl.state.limit"
@ -42,7 +42,7 @@
on-change-sort="($ctrl.onChangeSort)"
on-change-limit="($ctrl.onChangeLimit)"
on-change-page="($ctrl.onChangePage)"
></auth-logs-datatable>
></authentication-logs-table>
</div>
</div>
</div>

View File

@ -1,6 +1,5 @@
import angular from 'angular';
import { authLogsView } from './auth-logs-view';
import { authLogsDatatable } from './auth-logs-datatable';
export default angular.module('portainer.app.user-activity.auth-logs-view', []).component('authLogsView', authLogsView).component('authLogsDatatable', authLogsDatatable).name;
export default angular.module('portainer.app.user-activity.auth-logs-view', []).component('authLogsView', authLogsView).name;

View File

@ -0,0 +1,57 @@
import { History } from 'lucide-react';
import { Datatable } from '@@/datatables';
import { AuthLog } from './types';
import { columns } from './columns';
export function AuthenticationLogsTable({
dataset,
currentPage,
keyword,
limit,
onChangeKeyword,
onChangeLimit,
onChangePage,
onChangeSort,
sort,
totalItems,
}: {
keyword: string;
onChangeKeyword(keyword: string): void;
sort: { key: string; desc: boolean };
onChangeSort(sort: { key: string; desc: boolean }): void;
limit: number;
onChangeLimit(limit: number): void;
currentPage: number;
onChangePage(page: number): void;
totalItems: number;
dataset?: Array<AuthLog>;
}) {
return (
<Datatable<AuthLog>
title="Authentication Events"
titleIcon={History}
columns={columns}
dataset={dataset || []}
isLoading={!dataset}
settingsManager={{
pageSize: limit,
search: keyword,
setPageSize: onChangeLimit,
setSearch: onChangeKeyword,
setSortBy: (key, desc) =>
onChangeSort({ key: key || 'timestamp', desc }),
sortBy: {
id: sort.key,
desc: sort.desc,
},
}}
page={currentPage}
onPageChange={onChangePage}
isServerSidePagination
totalCount={totalItems}
disableSelect
/>
);
}

View File

@ -0,0 +1,74 @@
import { createColumnHelper } from '@tanstack/react-table';
import { Check, X } from 'lucide-react';
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
import { multiple } from '@@/datatables/filter-types';
import { filterHOC } from '@@/datatables/Filter';
import { Icon } from '@@/Icon';
import { ActivityType, AuthLog, AuthMethodType } from './types';
const activityTypesProps = {
[ActivityType.AuthSuccess]: {
label: 'Authentication success',
icon: Check,
mode: 'success',
},
[ActivityType.AuthFailure]: {
label: 'Authentication failure',
icon: X,
mode: 'danger',
},
[ActivityType.Logout]: { label: 'Logout', icon: undefined, mode: undefined },
} as const;
const columnHelper = createColumnHelper<AuthLog>();
export const columns = [
columnHelper.accessor('timestamp', {
header: 'Time',
cell: ({ getValue }) => {
const value = getValue();
return value ? isoDateFromTimestamp(value) : '';
},
}),
columnHelper.accessor('origin', {
header: 'Origin',
}),
columnHelper.accessor(({ context }) => AuthMethodType[context] || '', {
header: 'Context',
enableColumnFilter: true,
filterFn: multiple,
meta: {
filter: filterHOC('Filter'),
},
}),
columnHelper.accessor('username', {
header: 'User',
}),
columnHelper.accessor((item) => activityTypesProps[item.type].label, {
header: 'Result',
enableColumnFilter: true,
filterFn: multiple,
meta: {
filter: filterHOC('Filter'),
},
cell({ row: { original: item } }) {
const props = activityTypesProps[item.type];
if (!props) {
return null;
}
const { label, icon, mode } = props;
return (
<span className="flex gap-1 items-center">
{label}
{icon && mode && <Icon icon={icon} mode={mode} />}
</span>
);
},
}),
];

View File

@ -0,0 +1,20 @@
export enum AuthMethodType {
Internal = 1,
LDAP,
OAuth,
}
export enum ActivityType {
AuthSuccess = 1,
AuthFailure,
Logout,
}
export interface AuthLog {
timestamp: number;
context: AuthMethodType;
id: number;
username: string;
type: ActivityType;
origin: string;
}