diff --git a/app/portainer/react/components/activity-logs.ts b/app/portainer/react/components/activity-logs.ts
new file mode 100644
index 000000000..4c837255d
--- /dev/null
+++ b/app/portainer/react/components/activity-logs.ts
@@ -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;
diff --git a/app/portainer/react/components/index.ts b/app/portainer/react/components/index.ts
index 61269e8ed..468b40d4d 100644
--- a/app/portainer/react/components/index.ts
+++ b/app/portainer/react/components/index.ts
@@ -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',
diff --git a/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/auth-logs-datatable.controller.js b/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/auth-logs-datatable.controller.js
deleted file mode 100644
index cd053f5c7..000000000
--- a/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/auth-logs-datatable.controller.js
+++ /dev/null
@@ -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;
diff --git a/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/auth-logs-datatable.html b/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/auth-logs-datatable.html
deleted file mode 100644
index a72179f6d..000000000
--- a/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/auth-logs-datatable.html
+++ /dev/null
@@ -1,99 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
- |
-
-
-
-
- |
- |
- |
- |
- |
-
-
- Loading... |
-
-
- No logs available. |
-
-
-
-
-
-
-
-
diff --git a/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/index.js b/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/index.js
deleted file mode 100644
index 6488f3549..000000000
--- a/app/portainer/user-activity/auth-logs-view/auth-logs-datatable/index.js
+++ /dev/null
@@ -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: '<',
- },
-};
diff --git a/app/portainer/user-activity/auth-logs-view/auth-logs-view.html b/app/portainer/user-activity/auth-logs-view/auth-logs-view.html
index 622d1bff3..86c1de947 100644
--- a/app/portainer/user-activity/auth-logs-view/auth-logs-view.html
+++ b/app/portainer/user-activity/auth-logs-view/auth-logs-view.html
@@ -26,8 +26,8 @@
diff --git a/app/portainer/user-activity/auth-logs-view/index.js b/app/portainer/user-activity/auth-logs-view/index.js
index 6aa7f7e85..5b17122ae 100644
--- a/app/portainer/user-activity/auth-logs-view/index.js
+++ b/app/portainer/user-activity/auth-logs-view/index.js
@@ -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;
diff --git a/app/react/portainer/logs/AuthenticationLogsView/.keep b/app/react/portainer/logs/AuthenticationLogsView/.keep
deleted file mode 100644
index e69de29bb..000000000
diff --git a/app/react/portainer/logs/AuthenticationLogsView/AuthenticationLogsTable.tsx b/app/react/portainer/logs/AuthenticationLogsView/AuthenticationLogsTable.tsx
new file mode 100644
index 000000000..da7122ec8
--- /dev/null
+++ b/app/react/portainer/logs/AuthenticationLogsView/AuthenticationLogsTable.tsx
@@ -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;
+}) {
+ return (
+
+ 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
+ />
+ );
+}
diff --git a/app/react/portainer/logs/AuthenticationLogsView/columns.tsx b/app/react/portainer/logs/AuthenticationLogsView/columns.tsx
new file mode 100644
index 000000000..839bbfe7a
--- /dev/null
+++ b/app/react/portainer/logs/AuthenticationLogsView/columns.tsx
@@ -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();
+
+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 (
+
+ {label}
+ {icon && mode && }
+
+ );
+ },
+ }),
+];
diff --git a/app/react/portainer/logs/AuthenticationLogsView/types.ts b/app/react/portainer/logs/AuthenticationLogsView/types.ts
new file mode 100644
index 000000000..65686fc0c
--- /dev/null
+++ b/app/react/portainer/logs/AuthenticationLogsView/types.ts
@@ -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;
+}