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 { registriesModule } from './registries';
|
||||||
import { accountModule } from './account';
|
import { accountModule } from './account';
|
||||||
import { usersModule } from './users';
|
import { usersModule } from './users';
|
||||||
|
import { activityLogsModule } from './activity-logs';
|
||||||
|
|
||||||
export const ngModule = angular
|
export const ngModule = angular
|
||||||
.module('portainer.app.react.components', [
|
.module('portainer.app.react.components', [
|
||||||
|
@ -59,6 +60,7 @@ export const ngModule = angular
|
||||||
settingsModule,
|
settingsModule,
|
||||||
accountModule,
|
accountModule,
|
||||||
usersModule,
|
usersModule,
|
||||||
|
activityLogsModule,
|
||||||
])
|
])
|
||||||
.component(
|
.component(
|
||||||
'tagSelector',
|
'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-body>
|
||||||
</rd-widget>
|
</rd-widget>
|
||||||
<div class="row mt-5">
|
<div class="row mt-5">
|
||||||
<auth-logs-datatable
|
<authentication-logs-table
|
||||||
logs="$ctrl.state.logs"
|
dataset="$ctrl.state.logs"
|
||||||
keyword="$ctrl.state.keyword"
|
keyword="$ctrl.state.keyword"
|
||||||
sort="$ctrl.state.sort"
|
sort="$ctrl.state.sort"
|
||||||
limit="$ctrl.state.limit"
|
limit="$ctrl.state.limit"
|
||||||
|
@ -42,7 +42,7 @@
|
||||||
on-change-sort="($ctrl.onChangeSort)"
|
on-change-sort="($ctrl.onChangeSort)"
|
||||||
on-change-limit="($ctrl.onChangeLimit)"
|
on-change-limit="($ctrl.onChangeLimit)"
|
||||||
on-change-page="($ctrl.onChangePage)"
|
on-change-page="($ctrl.onChangePage)"
|
||||||
></auth-logs-datatable>
|
></authentication-logs-table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import { authLogsView } from './auth-logs-view';
|
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