mirror of
https://github.com/portainer/portainer.git
synced 2025-08-07 14:55:27 +02:00
refactor(activity-logs): migrate activity logs table to react [EE-4714] (#10891)
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run
Some checks are pending
ci / build_images (map[arch:amd64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
ci / build_images (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
ci / build_images (map[arch:arm platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:arm64 platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:ppc64le platform:linux version:]) (push) Waiting to run
ci / build_images (map[arch:s390x platform:linux version:]) (push) Waiting to run
ci / build_manifests (push) Blocked by required conditions
/ triage (push) Waiting to run
Lint / Run linters (push) Waiting to run
Test / test-client (push) Waiting to run
Test / test-server (map[arch:amd64 platform:linux]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:1809]) (push) Waiting to run
Test / test-server (map[arch:amd64 platform:windows version:ltsc2022]) (push) Waiting to run
Test / test-server (map[arch:arm64 platform:linux]) (push) Waiting to run
This commit is contained in:
parent
960d18998f
commit
c22d280491
29 changed files with 659 additions and 429 deletions
13
app/portainer/react/views/activity-logs.ts
Normal file
13
app/portainer/react/views/activity-logs.ts
Normal file
|
@ -0,0 +1,13 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { r2a } from '@/react-tools/react2angular';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { ActivityLogsView } from '@/react/portainer/logs/ActivityLogsView/ActivityLogsView';
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
|
||||
export const activityLogsModule = angular
|
||||
.module('portainer.app.react.views.activity-logs', [])
|
||||
.component(
|
||||
'activityLogsView',
|
||||
r2a(withUIRouter(withCurrentUser(ActivityLogsView)), [])
|
||||
).name;
|
|
@ -18,6 +18,7 @@ import { teamsModule } from './teams';
|
|||
import { updateSchedulesModule } from './update-schedules';
|
||||
import { environmentGroupModule } from './env-groups';
|
||||
import { registriesModule } from './registries';
|
||||
import { activityLogsModule } from './activity-logs';
|
||||
|
||||
export const viewsModule = angular
|
||||
.module('portainer.app.react.views', [
|
||||
|
@ -26,6 +27,7 @@ export const viewsModule = angular
|
|||
updateSchedulesModule,
|
||||
environmentGroupModule,
|
||||
registriesModule,
|
||||
activityLogsModule,
|
||||
])
|
||||
.component(
|
||||
'homeView',
|
||||
|
|
|
@ -1,38 +0,0 @@
|
|||
export default class ActivityLogsDatatableController {
|
||||
/* @ngInject */
|
||||
constructor($controller, $scope, PaginationService) {
|
||||
this.PaginationService = PaginationService;
|
||||
|
||||
this.tableKey = 'authLogs';
|
||||
|
||||
const $onInit = this.$onInit;
|
||||
angular.extend(this, $controller('GenericDatatableController', { $scope }));
|
||||
|
||||
this.changeSort = this.changeSort.bind(this);
|
||||
this.handleChangeLimit = this.handleChangeLimit.bind(this);
|
||||
this.$onInit = $onInit.bind(this);
|
||||
}
|
||||
|
||||
changeSort(key) {
|
||||
let desc = false;
|
||||
if (key === this.sort.key) {
|
||||
desc = !this.sort.desc;
|
||||
}
|
||||
|
||||
this.onChangeSort({ key, desc });
|
||||
}
|
||||
|
||||
handleChangeLimit(limit) {
|
||||
this.PaginationService.setPaginationLimit(this.tableKey, limit);
|
||||
this.onChangeLimit(limit);
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
this.$onInitGeneric();
|
||||
|
||||
const limit = this.PaginationService.getPaginationLimit(this.tableKey);
|
||||
if (limit) {
|
||||
this.onChangeLimit(+limit);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.activity-logs-datatable .small-column {
|
||||
width: 150px;
|
||||
}
|
|
@ -1,95 +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>
|
||||
Activity Logs
|
||||
</div>
|
||||
<div class="vertical-center">
|
||||
<datatable-searchbar on-change="($ctrl.onChangeKeyword)" value="$ctrl.keyword"></datatable-searchbar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-responsive">
|
||||
<table class="table-hover table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="small-column">
|
||||
<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 class="small-column">
|
||||
<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 class="small-column">
|
||||
<div class="vertical-center">
|
||||
<table-column-header
|
||||
col-title="'Environment'"
|
||||
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>
|
||||
</div>
|
||||
</th>
|
||||
|
||||
<th>
|
||||
<div class="vertical-center">
|
||||
<table-column-header col-title="'Action'" can-sort="false"> </table-column-header>
|
||||
</div>
|
||||
</th>
|
||||
<th>
|
||||
<div class="vertical-center">
|
||||
<table-column-header col-title="'Payload'" can-sort="false"> </table-column-header>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate-start="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 dir-paginate-end ng-show="item.Expanded">
|
||||
<td colspan="5">
|
||||
<json-tree object="item.payload" root-name="containerInfo.Id" start-expanded="true"></json-tree>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="!$ctrl.logs">
|
||||
<td class="text-muted text-center" colspan="5">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="$ctrl.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,24 +0,0 @@
|
|||
import './activity-logs-datatable.css';
|
||||
|
||||
import controller from './activity-logs-datatable.controller.js';
|
||||
|
||||
export const activityLogsDatatable = {
|
||||
templateUrl: './activity-logs-datatable.html',
|
||||
controller,
|
||||
bindings: {
|
||||
logs: '<',
|
||||
keyword: '<',
|
||||
sort: '<',
|
||||
limit: '<',
|
||||
totalItems: '<',
|
||||
currentPage: '<',
|
||||
feature: '@',
|
||||
|
||||
onChangeContextFilter: '<',
|
||||
onChangeKeyword: '<',
|
||||
onChangeSort: '<',
|
||||
|
||||
onChangeLimit: '<',
|
||||
onChangePage: '<',
|
||||
},
|
||||
};
|
|
@ -1,89 +0,0 @@
|
|||
import moment from 'moment';
|
||||
|
||||
import { FeatureId } from '@/react/portainer/feature-flags/enums';
|
||||
export default class ActivityLogsViewController {
|
||||
/* @ngInject */
|
||||
constructor($async, $scope, Notifications) {
|
||||
this.$async = $async;
|
||||
this.$scope = $scope;
|
||||
this.Notifications = Notifications;
|
||||
|
||||
this.limitedFeature = FeatureId.ACTIVITY_AUDIT;
|
||||
|
||||
this.state = {
|
||||
keyword: '',
|
||||
date: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
sort: {
|
||||
key: 'Timestamp',
|
||||
desc: true,
|
||||
},
|
||||
page: 1,
|
||||
limit: 10,
|
||||
totalItems: 0,
|
||||
logs: null,
|
||||
};
|
||||
|
||||
this.today = moment().endOf('day');
|
||||
this.minValidDate = moment().subtract(7, 'd').startOf('day');
|
||||
|
||||
this.onChangeDate = this.onChangeDate.bind(this);
|
||||
this.onChangeKeyword = this.onChangeKeyword.bind(this);
|
||||
this.onChangeSort = this.onChangeSort.bind(this);
|
||||
this.loadLogs = this.loadLogs.bind(this);
|
||||
this.onChangePage = this.onChangePage.bind(this);
|
||||
this.onChangeLimit = this.onChangeLimit.bind(this);
|
||||
}
|
||||
|
||||
onChangePage(page) {
|
||||
this.state.page = page;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
onChangeLimit(limit) {
|
||||
this.state.page = 1;
|
||||
this.state.limit = limit;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
onChangeSort(sort) {
|
||||
this.state.page = 1;
|
||||
this.state.sort = sort;
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
onChangeKeyword(keyword) {
|
||||
return this.$scope.$evalAsync(() => {
|
||||
this.state.page = 1;
|
||||
this.state.keyword = keyword;
|
||||
this.loadLogs();
|
||||
});
|
||||
}
|
||||
|
||||
onChangeDate({ startDate, endDate }) {
|
||||
this.state.page = 1;
|
||||
this.state.date = { to: endDate, from: startDate };
|
||||
this.loadLogs();
|
||||
}
|
||||
|
||||
async loadLogs() {
|
||||
return this.$async(async () => {
|
||||
this.state.logs = null;
|
||||
try {
|
||||
const { logs, totalCount } = { logs: [{}, {}, {}, {}, {}], totalCount: 5 };
|
||||
this.state.logs = logs;
|
||||
this.state.totalItems = totalCount;
|
||||
} catch (err) {
|
||||
this.Notifications.error('Failure', err, 'Failed loading user activity logs');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
return this.$async(async () => {
|
||||
this.loadLogs();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
<page-header title="'User Activity'" breadcrumbs="['Activity Logs']" reload="true"> </page-header>
|
||||
|
||||
<div class="be-indicator-container limited-be mx-4">
|
||||
<div>
|
||||
<div class="limited-be-link vertical-center"><be-feature-indicator feature="$ctrl.limitedFeature"></be-feature-indicator></div>
|
||||
<div class="limited-be-content">
|
||||
<rd-widget>
|
||||
<rd-widget-body>
|
||||
<div class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<label for="dateRangeInput" class="col-sm-2 control-label text-left">Date Range</label>
|
||||
<div class="col-sm-6">
|
||||
<input type="text" class="form-control" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted small vertical-center">
|
||||
<pr-icon icon="'info'" class-name="'icon icon-sm icon-primary'"></pr-icon>
|
||||
Portainer user activity logs have a maximum retention of 7 days.
|
||||
</p>
|
||||
<div>
|
||||
<button type="button" class="btn btn-sm btn-primary" limited-feature-dir="{{::$ctrl.limitedFeature}}" limited-feature-class="limited-be" limited-feature-disabled>
|
||||
<pr-icon icon="'download'" class-name="'icon icon-sm'"></pr-icon>
|
||||
Export as CSV
|
||||
</button>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
<div class="row mt-5">
|
||||
<activity-logs-datatable
|
||||
logs="$ctrl.state.logs"
|
||||
keyword="$ctrl.state.keyword"
|
||||
sort="$ctrl.state.sort"
|
||||
limit="$ctrl.state.limit"
|
||||
context-filter="$ctrl.state.contextFilter"
|
||||
total-items="$ctrl.state.totalItems"
|
||||
current-page="$ctrl.state.currentPage"
|
||||
feature="{{:: $ctrl.limitedFeature}}"
|
||||
on-change-keyword="($ctrl.onChangeKeyword)"
|
||||
on-change-sort="($ctrl.onChangeSort)"
|
||||
on-change-limit="($ctrl.onChangeLimit)"
|
||||
on-change-page="($ctrl.onChangePage)"
|
||||
></activity-logs-datatable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
|
@ -1,6 +0,0 @@
|
|||
import controller from './activity-logs-view.controller.js';
|
||||
|
||||
export const activityLogsView = {
|
||||
templateUrl: './activity-logs-view.html',
|
||||
controller,
|
||||
};
|
|
@ -1,9 +0,0 @@
|
|||
import angular from 'angular';
|
||||
|
||||
import { activityLogsView } from './activity-logs-view';
|
||||
import { activityLogsDatatable } from './activity-logs-datatable';
|
||||
|
||||
export default angular
|
||||
.module('portainer.app.user-activity.activity-logs-view', [])
|
||||
.component('activityLogsDatatable', activityLogsDatatable)
|
||||
.component('activityLogsView', activityLogsView).name;
|
|
@ -3,9 +3,15 @@ import angular from 'angular';
|
|||
import { NotificationsViewAngular } from '@/react/portainer/notifications/NotificationsView';
|
||||
import { AccessHeaders } from '../authorization-guard';
|
||||
import authLogsViewModule from './auth-logs-view';
|
||||
import activityLogsViewModule from './activity-logs-view';
|
||||
import { UserActivityService } from './user-activity.service';
|
||||
import { UserActivity } from './user-activity.rest';
|
||||
|
||||
export default angular.module('portainer.app.user-activity', [authLogsViewModule, activityLogsViewModule]).component('notifications', NotificationsViewAngular).config(config).name;
|
||||
export default angular
|
||||
.module('portainer.app.user-activity', [authLogsViewModule])
|
||||
.service('UserActivity', UserActivity)
|
||||
.service('UserActivityService', UserActivityService)
|
||||
.component('notifications', NotificationsViewAngular)
|
||||
.config(config).name;
|
||||
|
||||
/* @ngInject */
|
||||
function config($stateRegistryProvider) {
|
||||
|
|
28
app/portainer/user-activity/user-activity.rest.js
Normal file
28
app/portainer/user-activity/user-activity.rest.js
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { baseHref } from '@/portainer/helpers/pathHelper';
|
||||
|
||||
/* @ngInject */
|
||||
export function UserActivity($resource, $http) {
|
||||
const BASE_URL = baseHref() + 'api/useractivity';
|
||||
|
||||
const resource = $resource(
|
||||
`${BASE_URL}/:action`,
|
||||
{},
|
||||
{
|
||||
authLogs: { method: 'GET', params: { action: 'authlogs' } },
|
||||
}
|
||||
);
|
||||
|
||||
return { authLogsAsCSV, ...resource };
|
||||
|
||||
async function authLogsAsCSV(params) {
|
||||
return $http({
|
||||
method: 'GET',
|
||||
url: `${BASE_URL}/authlogs.csv`,
|
||||
params,
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-type': 'text/csv',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
13
app/portainer/user-activity/user-activity.service.js
Normal file
13
app/portainer/user-activity/user-activity.service.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
/* @ngInject */
|
||||
export function UserActivityService(FileSaver, UserActivity) {
|
||||
return { authLogs, saveAuthLogsAsCSV };
|
||||
|
||||
function authLogs(offset, limit, sort, keyword, date, contexts, types) {
|
||||
return UserActivity.authLogs({ offset, limit, keyword, before: date.to, after: date.from, sortBy: sort.key, sortDesc: sort.desc, contexts, types }).$promise;
|
||||
}
|
||||
|
||||
async function saveAuthLogsAsCSV(sort, keyword, date, contexts, types) {
|
||||
const response = await UserActivity.authLogsAsCSV({ keyword, before: date.to, after: date.from, sortBy: sort.key, sortDesc: sort.desc, limit: 2000, contexts, types });
|
||||
return FileSaver.saveAs(response.data, 'logs.csv');
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue