mirror of https://github.com/portainer/portainer
refactor(activity-logs): migrate auth logs table to react [EE-4715] (#10890)
parent
bd271ec5a1
commit
7e53d01d0f
|
@ -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;
|
|
@ -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',
|
||||
|
|
|
@ -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;
|
|
@ -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>
|
|
@ -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: '<',
|
||||
},
|
||||
};
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue