mirror of https://github.com/portainer/portainer
refactor(activity-logs): migrate activity logs table to react [EE-4714] (#10891)
parent
960d18998f
commit
c22d280491
|
@ -615,24 +615,6 @@ input[type='checkbox'] {
|
|||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* json-tree override */
|
||||
json-tree {
|
||||
font-size: 13px;
|
||||
color: var(--blue-5);
|
||||
}
|
||||
|
||||
json-tree .key {
|
||||
color: var(--blue-3);
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
json-tree .branch-preview {
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
/* !json-tree override */
|
||||
|
||||
/* uib-progressbar override */
|
||||
.progress-bar {
|
||||
color: var(--text-progress-bar-color);
|
||||
|
|
|
@ -168,17 +168,6 @@ pre {
|
|||
background-color: var(--bg-pre-color);
|
||||
color: var(--text-pre-color);
|
||||
}
|
||||
json-tree .key {
|
||||
color: var(--text-json-tree-color);
|
||||
}
|
||||
|
||||
json-tree .leaf-value {
|
||||
color: var(--text-json-tree-leaf-color);
|
||||
}
|
||||
|
||||
json-tree .branch-preview {
|
||||
color: var(--text-json-tree-branch-preview-color);
|
||||
}
|
||||
|
||||
.progress {
|
||||
background-color: var(--bg-progress-color);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
/* json-tree override */
|
||||
.json-tree,
|
||||
json-tree {
|
||||
font-size: 13px;
|
||||
color: var(--blue-5);
|
||||
}
|
||||
|
||||
.json-tree .key,
|
||||
json-tree .key {
|
||||
color: var(--text-json-tree-color);
|
||||
}
|
||||
|
||||
json-tree .key {
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.json-tree .branch-preview,
|
||||
json-tree .branch-preview {
|
||||
color: var(--text-json-tree-branch-preview-color);
|
||||
font-style: normal;
|
||||
font-size: 11px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.json-tree .leaf-value,
|
||||
json-tree .leaf-value {
|
||||
color: var(--text-json-tree-leaf-color);
|
||||
}
|
||||
|
||||
/* !json-tree override */
|
|
@ -0,0 +1,41 @@
|
|||
import { ComponentProps } from 'react';
|
||||
import { JsonView, defaultStyles } from 'react-json-view-lite';
|
||||
import 'react-json-view-lite/dist/index.css';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import './JsonTree.css';
|
||||
|
||||
export function JsonTree({ style, ...props }: ComponentProps<typeof JsonView>) {
|
||||
const currentStyle = getCurrentStyle(style);
|
||||
return (
|
||||
<JsonView
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...props}
|
||||
style={currentStyle}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
type StyleProps = ComponentProps<typeof JsonView>['style'];
|
||||
|
||||
function getCurrentStyle(style: StyleProps | undefined): StyleProps {
|
||||
if (style) {
|
||||
return style;
|
||||
}
|
||||
|
||||
return {
|
||||
...defaultStyles,
|
||||
container: 'json-tree',
|
||||
booleanValue: 'leaf-value',
|
||||
nullValue: 'leaf-value',
|
||||
otherValue: 'leaf-value',
|
||||
numberValue: 'leaf-value',
|
||||
stringValue: 'leaf-value',
|
||||
undefinedValue: 'leaf-value',
|
||||
label: 'key',
|
||||
punctuation: 'leaf-value',
|
||||
collapseIcon: clsx(defaultStyles.collapseIcon, 'key'),
|
||||
expandIcon: clsx(defaultStyles.expandIcon, 'key'),
|
||||
collapsedContent: clsx(defaultStyles.collapsedContent, 'branch-preview'),
|
||||
};
|
||||
}
|
|
@ -16,6 +16,9 @@ import { TextTip } from '@@/Tip/TextTip';
|
|||
|
||||
import { FormValues } from './types';
|
||||
|
||||
import 'react-datetime-picker/dist/DateTimePicker.css';
|
||||
import 'react-calendar/dist/Calendar.css';
|
||||
|
||||
interface Props {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
import { createColumnHelper } from '@tanstack/react-table';
|
||||
import { History, Search } from 'lucide-react';
|
||||
|
||||
import { isoDateFromTimestamp } from '@/portainer/filters/filters';
|
||||
|
||||
import { ExpandableDatatable } from '@@/datatables/ExpandableDatatable';
|
||||
import { Button } from '@@/buttons';
|
||||
import { JsonTree } from '@@/JsonTree';
|
||||
|
||||
import { ActivityLog } from './types';
|
||||
import { getSortType } from './useActivityLogs';
|
||||
|
||||
const columnHelper = createColumnHelper<ActivityLog>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.accessor('timestamp', {
|
||||
id: 'Timestamp',
|
||||
header: 'Time',
|
||||
cell: ({ getValue }) => {
|
||||
const value = getValue();
|
||||
return value ? isoDateFromTimestamp(value) : '';
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('username', {
|
||||
id: 'Username',
|
||||
header: 'User',
|
||||
}),
|
||||
columnHelper.accessor('context', {
|
||||
id: 'Context',
|
||||
header: 'Environment',
|
||||
}),
|
||||
columnHelper.accessor('action', {
|
||||
id: 'Action',
|
||||
header: 'Action',
|
||||
}),
|
||||
columnHelper.accessor('payload', {
|
||||
header: 'Payload',
|
||||
enableSorting: false,
|
||||
cell: ({ row, getValue }) =>
|
||||
getValue() ? (
|
||||
<Button color="link" onClick={() => row.toggleExpanded()} icon={Search}>
|
||||
inspect
|
||||
</Button>
|
||||
) : null,
|
||||
}),
|
||||
];
|
||||
|
||||
export function ActivityLogsTable({
|
||||
dataset,
|
||||
currentPage,
|
||||
keyword,
|
||||
limit,
|
||||
onChangeKeyword,
|
||||
onChangeLimit,
|
||||
onChangePage,
|
||||
onChangeSort,
|
||||
sort,
|
||||
totalItems,
|
||||
}: {
|
||||
keyword: string;
|
||||
onChangeKeyword(keyword: string): void;
|
||||
sort: { id: string; desc: boolean } | undefined;
|
||||
onChangeSort(sort: { id: string; desc: boolean } | undefined): void;
|
||||
limit: number;
|
||||
onChangeLimit(limit: number): void;
|
||||
currentPage: number;
|
||||
onChangePage(page: number): void;
|
||||
totalItems: number;
|
||||
dataset?: Array<ActivityLog>;
|
||||
}) {
|
||||
return (
|
||||
<ExpandableDatatable<ActivityLog>
|
||||
title="Activity Logs"
|
||||
titleIcon={History}
|
||||
columns={columns}
|
||||
dataset={dataset || []}
|
||||
isLoading={!dataset}
|
||||
settingsManager={{
|
||||
pageSize: limit,
|
||||
search: keyword,
|
||||
setPageSize: onChangeLimit,
|
||||
setSearch: onChangeKeyword,
|
||||
setSortBy: (id, desc) =>
|
||||
onChangeSort({ id: getSortType(id) || 'Timestamp', desc }),
|
||||
sortBy: sort
|
||||
? {
|
||||
id: sort.id,
|
||||
desc: sort.desc,
|
||||
}
|
||||
: undefined,
|
||||
}}
|
||||
page={currentPage}
|
||||
onPageChange={onChangePage}
|
||||
isServerSidePagination
|
||||
totalCount={totalItems}
|
||||
disableSelect
|
||||
renderSubRow={(row) => <SubRow item={row.original} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function SubRow({ item }: { item: ActivityLog }) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={Number.MAX_SAFE_INTEGER}>
|
||||
<JsonTree data={item.payload} />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
import { useState } from 'react';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { useTableStateWithoutStorage } from '@@/datatables/useTableState';
|
||||
import { BEOverlay } from '@@/BEFeatureIndicator/BEOverlay';
|
||||
|
||||
import { FeatureId } from '../../feature-flags/enums';
|
||||
|
||||
import { ActivityLogsTable } from './ActivityLogsTable';
|
||||
import { useActivityLogs, getSortType } from './useActivityLogs';
|
||||
import { useExportMutation } from './useExportMutation';
|
||||
import { FilterBar } from './FilterBar';
|
||||
|
||||
export function ActivityLogsView() {
|
||||
const exportMutation = useExportMutation();
|
||||
const [range, setRange] = useState<
|
||||
{ start: Date; end: Date | null } | undefined
|
||||
>(undefined);
|
||||
const [page, setPage] = useState(0);
|
||||
const tableState = useTableStateWithoutStorage('Timestamp');
|
||||
const offset = page * tableState.pageSize;
|
||||
|
||||
const query = {
|
||||
offset,
|
||||
limit: tableState.pageSize,
|
||||
sortBy: getSortType(tableState.sortBy?.id),
|
||||
desc: tableState.sortBy?.desc,
|
||||
search: tableState.search,
|
||||
...(range
|
||||
? {
|
||||
after: seconds(range?.start?.valueOf()),
|
||||
before: seconds(range?.end?.valueOf()),
|
||||
}
|
||||
: undefined),
|
||||
};
|
||||
|
||||
const logsQuery = useActivityLogs(query);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="User Activity" breadcrumbs="Activity Logs" reload />
|
||||
|
||||
<div className="mx-4">
|
||||
<BEOverlay featureId={FeatureId.ACTIVITY_AUDIT}>
|
||||
<FilterBar
|
||||
value={range}
|
||||
onChange={setRange}
|
||||
onExport={handleExport}
|
||||
/>
|
||||
|
||||
<div className="-mx-[15px] mt-4">
|
||||
<ActivityLogsTable
|
||||
sort={tableState.sortBy}
|
||||
onChangeSort={(value) =>
|
||||
tableState.setSortBy(value?.id, value?.desc || false)
|
||||
}
|
||||
limit={tableState.pageSize}
|
||||
onChangeLimit={tableState.setPageSize}
|
||||
keyword={tableState.search}
|
||||
onChangeKeyword={tableState.setSearch}
|
||||
currentPage={page}
|
||||
onChangePage={setPage}
|
||||
totalItems={logsQuery.data?.totalCount || 0}
|
||||
dataset={logsQuery.data?.logs}
|
||||
/>
|
||||
</div>
|
||||
</BEOverlay>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
function handleExport() {
|
||||
exportMutation.mutate(query);
|
||||
}
|
||||
}
|
||||
|
||||
function seconds(ms?: number) {
|
||||
if (!ms) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return Math.floor(ms / 1000);
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
import { DownloadIcon } from 'lucide-react';
|
||||
|
||||
import { Widget } from '@@/Widget';
|
||||
import { TextTip } from '@@/Tip/TextTip';
|
||||
import { Button } from '@@/buttons';
|
||||
import { BEFeatureIndicator } from '@@/BEFeatureIndicator';
|
||||
|
||||
import { FeatureId } from '../../feature-flags/enums';
|
||||
import { DateRangePicker } from '../components/DateRangePicker';
|
||||
|
||||
export function FilterBar({
|
||||
value,
|
||||
onChange,
|
||||
onExport,
|
||||
}: {
|
||||
value: { start: Date; end: Date | null } | undefined;
|
||||
onChange: (value?: { start: Date; end: Date | null }) => void;
|
||||
onExport: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Widget>
|
||||
<Widget.Body>
|
||||
<form className="form-horizontal">
|
||||
<DateRangePicker value={value} onChange={onChange} />
|
||||
|
||||
<TextTip color="blue">
|
||||
Portainer user activity logs have a maximum retention of 7 days.
|
||||
</TextTip>
|
||||
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
color="primary"
|
||||
icon={DownloadIcon}
|
||||
onClick={onExport}
|
||||
className="!ml-0"
|
||||
>
|
||||
Export as CSV
|
||||
</Button>
|
||||
<BEFeatureIndicator featureId={FeatureId.ACTIVITY_AUDIT} />
|
||||
</div>
|
||||
</form>
|
||||
</Widget.Body>
|
||||
</Widget>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
export interface ActivityLog {
|
||||
timestamp: number;
|
||||
action: string;
|
||||
context: string;
|
||||
id: number;
|
||||
payload: object;
|
||||
username: string;
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
import { useQuery } from 'react-query';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { isBE } from '../../feature-flags/feature-flags.service';
|
||||
|
||||
import { ActivityLog } from './types';
|
||||
|
||||
export const sortKeys = ['Context', 'Action', 'Timestamp', 'Username'] as const;
|
||||
export type SortKey = (typeof sortKeys)[number];
|
||||
export function isSortKey(value?: string): value is SortKey {
|
||||
return !!value && sortKeys.includes(value as SortKey);
|
||||
}
|
||||
export function getSortType(value?: string): SortKey | undefined {
|
||||
return isSortKey(value) ? value : undefined;
|
||||
}
|
||||
|
||||
export interface Query {
|
||||
offset: number;
|
||||
limit: number;
|
||||
sortBy?: SortKey;
|
||||
desc?: boolean;
|
||||
search: string;
|
||||
after?: number;
|
||||
before?: number;
|
||||
}
|
||||
|
||||
export function useActivityLogs(query: Query) {
|
||||
return useQuery({
|
||||
queryKey: ['activityLogs', query] as const,
|
||||
queryFn: () => fetchActivityLogs(query),
|
||||
keepPreviousData: true,
|
||||
});
|
||||
}
|
||||
|
||||
interface ActivityLogsResponse {
|
||||
logs: Array<ActivityLog>;
|
||||
totalCount: number;
|
||||
}
|
||||
|
||||
async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> {
|
||||
try {
|
||||
if (!isBE) {
|
||||
return {
|
||||
logs: [{}, {}, {}, {}, {}] as Array<ActivityLog>,
|
||||
totalCount: 5,
|
||||
};
|
||||
}
|
||||
|
||||
const { data } = await axios.get<ActivityLogsResponse>(
|
||||
'/useractivity/logs',
|
||||
{ params: query }
|
||||
);
|
||||
return data;
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err, 'Failed loading user activity logs csv');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import { useMutation } from 'react-query';
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import axios, { parseAxiosError } from '@/portainer/services/axios';
|
||||
|
||||
import { Query } from './useActivityLogs';
|
||||
|
||||
export function useExportMutation() {
|
||||
return useMutation({
|
||||
mutationFn: exportActivityLogs,
|
||||
});
|
||||
}
|
||||
|
||||
async function exportActivityLogs(query: Omit<Query, 'limit'>) {
|
||||
try {
|
||||
const { data, headers } = await axios.get<Blob>('/useractivity/logs.csv', {
|
||||
params: { ...query, limit: 2000 },
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-type': 'text/csv',
|
||||
},
|
||||
});
|
||||
|
||||
const contentDispositionHeader = headers['content-disposition'] || '';
|
||||
const filename =
|
||||
contentDispositionHeader.replace('attachment; filename=', '').trim() ||
|
||||
'logs.csv';
|
||||
saveAs(data, filename);
|
||||
} catch (err) {
|
||||
throw parseAxiosError(err, 'Failed loading user activity logs csv');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
import WojtekmajRangePicker from '@wojtekmaj/react-daterange-picker';
|
||||
import { Calendar, X } from 'lucide-react';
|
||||
import { date, object, SchemaOf } from 'yup';
|
||||
import { FormikErrors } from 'formik';
|
||||
|
||||
import '@wojtekmaj/react-daterange-picker/dist/DateRangePicker.css';
|
||||
import 'react-calendar/dist/Calendar.css';
|
||||
|
||||
import { FormControl } from '@@/form-components/FormControl';
|
||||
|
||||
import 'react-datetime-picker/dist/DateTimePicker.css';
|
||||
|
||||
type Value = { start: Date; end: Date | null };
|
||||
|
||||
export function DateRangePicker({
|
||||
value,
|
||||
onChange,
|
||||
name,
|
||||
error,
|
||||
}: {
|
||||
value: Value | undefined;
|
||||
onChange: (value?: Value) => void;
|
||||
name?: string;
|
||||
error?: FormikErrors<Value>;
|
||||
}) {
|
||||
return (
|
||||
<FormControl label="Date Range" errors={error}>
|
||||
<div className="w-1/2">
|
||||
<WojtekmajRangePicker
|
||||
format="y-MM-dd"
|
||||
className="form-control [&>div]:border-0"
|
||||
value={value ? [value.start, value.end] : null}
|
||||
onChange={(date) => {
|
||||
if (!date) {
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
if (Array.isArray(date)) {
|
||||
if (date.length === 2 && date[0] && date[1]) {
|
||||
onChange({
|
||||
start: date[0],
|
||||
end: date[1],
|
||||
});
|
||||
return;
|
||||
}
|
||||
onChange(undefined);
|
||||
return;
|
||||
}
|
||||
onChange({ start: date, end: null });
|
||||
}}
|
||||
name={name}
|
||||
calendarIcon={<Calendar />}
|
||||
clearIcon={<X />}
|
||||
/>
|
||||
</div>
|
||||
</FormControl>
|
||||
);
|
||||
}
|
||||
|
||||
export function dateRangePickerValidation(): SchemaOf<Value> {
|
||||
return object({
|
||||
start: date().required(),
|
||||
end: date().nullable().default(null).required(),
|
||||
});
|
||||
}
|
|
@ -57,6 +57,7 @@
|
|||
"@uirouter/react-hybrid": "^1.0.4",
|
||||
"@uiw/codemirror-themes": "^4.19.9",
|
||||
"@uiw/react-codemirror": "^4.19.5",
|
||||
"@wojtekmaj/react-daterange-picker": "^5.5.0",
|
||||
"angular": "1.8.2",
|
||||
"angular-clipboard": "^1.6.2",
|
||||
"angular-file-saver": "^1.1.3",
|
||||
|
@ -108,10 +109,12 @@
|
|||
"parse-duration": "^1.0.2",
|
||||
"rc-slider": "^10.0.0",
|
||||
"react": "^17.0.2",
|
||||
"react-datetime-picker": "^4.2.0",
|
||||
"react-calendar": "^4.8.0",
|
||||
"react-datetime-picker": "^5.6.0",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.12.0",
|
||||
"react-is": "^17.0.2",
|
||||
"react-json-view-lite": "^1.2.1",
|
||||
"react-query": "^3.33.4",
|
||||
"react-select": "^5.2.1",
|
||||
"sanitize-html": "^2.8.1",
|
||||
|
|
202
yarn.lock
202
yarn.lock
|
@ -5944,6 +5944,18 @@
|
|||
"@types/fined" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/lodash.memoize@^4.1.7":
|
||||
version "4.1.9"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash.memoize/-/lodash.memoize-4.1.9.tgz#9f8912d39b6e450c0d342a2b74c99d331bf2016b"
|
||||
integrity sha512-glY1nQuoqX4Ft8Uk+KfJudOD7DQbbEDF6k9XpGncaohW3RW4eSWBlx6AA0fZCrh40tZcQNH4jS/Oc59J6Eq+aw==
|
||||
dependencies:
|
||||
"@types/lodash" "*"
|
||||
|
||||
"@types/lodash@*":
|
||||
version "4.14.175"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.175.tgz#b78dfa959192b01fae0ad90e166478769b215f45"
|
||||
integrity sha512-XmdEOrKQ8a1Y/yxQFOMbC47G/V2VDO1GvMRnl4O75M4GW/abC5tnfzadQYkqEveqRM1dEJGFFegfPNA2vvx2iw==
|
||||
|
||||
"@types/lodash@^4.14.167":
|
||||
version "4.14.194"
|
||||
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.194.tgz#b71eb6f7a0ff11bff59fc987134a093029258a76"
|
||||
|
@ -6039,13 +6051,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.4.tgz#cd667bcfdd025213aafb7ca5915a932590acdcdc"
|
||||
integrity sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==
|
||||
|
||||
"@types/react-calendar@^3.0.0":
|
||||
version "3.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-calendar/-/react-calendar-3.5.2.tgz#e401034e4bb82f4510ba87aa490e98b5746e16e0"
|
||||
integrity sha512-8gkU9KaE33VVbu3YWvxXjEk4BsalgSYR3c/5XF9XNJiQ/2MKxiGkTg/PfOHUX/BvcADykRBMAEJiCi6jFPEE3A==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-datetime-picker@^3.4.1":
|
||||
version "3.4.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-datetime-picker/-/react-datetime-picker-3.4.1.tgz#8acbc3e6f4e69fac0f91be4e920c3efdc28f3ed7"
|
||||
|
@ -6679,10 +6684,22 @@
|
|||
resolved "https://registry.yarnpkg.com/@webpack-cli/serve/-/serve-2.0.5.tgz#325db42395cd49fe6c14057f9a900e427df8810e"
|
||||
integrity sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==
|
||||
|
||||
"@wojtekmaj/date-utils@^1.0.0", "@wojtekmaj/date-utils@^1.0.2", "@wojtekmaj/date-utils@^1.0.3":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@wojtekmaj/date-utils/-/date-utils-1.0.3.tgz#2dcfd92881425c5923e429c2aec86fb3609032a1"
|
||||
integrity sha512-1VPkkTBk07gMR1fjpBtse4G+oJqpmE+0gUFB0dg3VIL7qJmUVaBoD/vlzMm/jNeOPfvlmerl1lpnsZyBUFIRuw==
|
||||
"@wojtekmaj/date-utils@^1.1.3", "@wojtekmaj/date-utils@^1.5.0":
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@wojtekmaj/date-utils/-/date-utils-1.5.1.tgz#c3cd67177ac781cfa5736219d702a55a2aea5f2b"
|
||||
integrity sha512-+i7+JmNiE/3c9FKxzWFi2IjRJ+KzZl1QPu6QNrsgaa2MuBgXvUy4gA1TVzf/JMdIIloB76xSKikTWuyYAIVLww==
|
||||
|
||||
"@wojtekmaj/react-daterange-picker@^5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@wojtekmaj/react-daterange-picker/-/react-daterange-picker-5.5.0.tgz#634daf8874a6f704dc5bbe45279e10b826bb41e6"
|
||||
integrity sha512-xW0J5akOO0pmnPyStEndcHj3gQKTYrZue7HSfUp1F7pDgn9vAJD7AfwOBIA3iqUDUnIBl+jgrl1eP1+/EuTn7g==
|
||||
dependencies:
|
||||
clsx "^2.0.0"
|
||||
make-event-props "^1.6.0"
|
||||
prop-types "^15.6.0"
|
||||
react-calendar "^4.6.0"
|
||||
react-date-picker "^10.5.0"
|
||||
react-fit "^1.7.0"
|
||||
|
||||
"@xtuc/ieee754@^1.2.0":
|
||||
version "1.2.0"
|
||||
|
@ -8152,10 +8169,10 @@ clsx@^1.1.1:
|
|||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
|
||||
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
|
||||
|
||||
clsx@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
|
||||
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
|
||||
clsx@^2.0.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb"
|
||||
integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==
|
||||
|
||||
codemirror@^6.0.0, codemirror@^6.0.1:
|
||||
version "6.0.1"
|
||||
|
@ -8958,10 +8975,10 @@ destroy@1.2.0:
|
|||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||
|
||||
detect-element-overflow@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-element-overflow/-/detect-element-overflow-1.2.0.tgz#86e504292ffedc3aef813395fbdf0261aaf6afa9"
|
||||
integrity sha512-Jtr9ivYPhpd9OJux+hjL0QjUKiS1Ghgy8tvIufUjFslQgIWvgGr4mn57H190APbKkiOmXnmtMI6ytaKzMusecg==
|
||||
detect-element-overflow@^1.4.0:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/detect-element-overflow/-/detect-element-overflow-1.4.2.tgz#2e48509e5aa07647f4335b5f4f52c146b92f99c5"
|
||||
integrity sha512-4m6cVOtvm/GJLjo7WFkPfwXoEIIbM7GQwIh4WEa4g7IsNi1YzwUsGL5ApNLrrHL29bHeNeQ+/iZhw+YHqgE2Fw==
|
||||
|
||||
detect-file@^1.0.0:
|
||||
version "1.0.0"
|
||||
|
@ -10649,11 +10666,12 @@ get-tsconfig@^4.5.0:
|
|||
dependencies:
|
||||
resolve-pkg-maps "^1.0.0"
|
||||
|
||||
get-user-locale@^1.2.0, get-user-locale@^1.4.0:
|
||||
version "1.5.1"
|
||||
resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-1.5.1.tgz#18a9ba2cfeed0e713ea00968efa75d620523a5ea"
|
||||
integrity sha512-WiNpoFRcHn1qxP9VabQljzGwkAQDrcpqUtaP0rNBEkFxJdh4f3tik6MfZsMYZc+UgQJdGCxWEjL9wnCUlRQXag==
|
||||
get-user-locale@^2.2.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/get-user-locale/-/get-user-locale-2.3.1.tgz#fc7319429c8a70fac01b3b2a0b08b0c71c1d3fe2"
|
||||
integrity sha512-VEvcsqKYx7zhZYC1CjecrDC5ziPSpl1gSm0qFFJhHSGDrSC+x4+p1KojWC/83QX//j476gFhkVXP/kNUc9q+bQ==
|
||||
dependencies:
|
||||
"@types/lodash.memoize" "^4.1.7"
|
||||
lodash.memoize "^4.1.1"
|
||||
|
||||
giget@^1.0.0:
|
||||
|
@ -12557,10 +12575,10 @@ make-dir@^4.0.0:
|
|||
dependencies:
|
||||
semver "^7.5.3"
|
||||
|
||||
make-event-props@^1.1.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/make-event-props/-/make-event-props-1.3.0.tgz#2434cb390d58bcf40898d009ef5b1f936de9671b"
|
||||
integrity sha512-oWiDZMcVB1/A487251hEWza1xzgCzl6MXxe9aF24l5Bt9N9UEbqTqKumEfuuLhmlhRZYnc+suVvW4vUs8bwO7Q==
|
||||
make-event-props@^1.6.0:
|
||||
version "1.6.2"
|
||||
resolved "https://registry.yarnpkg.com/make-event-props/-/make-event-props-1.6.2.tgz#c8e0e48eb28b9b808730de38359f6341de7ec5a2"
|
||||
integrity sha512-iDwf7mA03WPiR8QxvcVHmVWEPfMY1RZXerDVNCRYW7dUr2ppH3J58Rwb39/WG39yTZdRSxr3x+2v22tvI0VEvA==
|
||||
|
||||
make-iterator@^1.0.0:
|
||||
version "1.0.1"
|
||||
|
@ -14438,15 +14456,16 @@ rc-util@^5.27.0:
|
|||
"@babel/runtime" "^7.18.3"
|
||||
react-is "^16.12.0"
|
||||
|
||||
react-calendar@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-4.0.0.tgz#99ad73dd0c7c5b25aa535a5fdeee3d71bfe45faa"
|
||||
integrity sha512-y9Q5Oo3Mq869KExbOCP3aJ3hEnRZKZ0TqUa9QU1wJGgDZFrW1qTaWp5v52oZpmxTTrpAMTUcUGaC0QJcO1f8Nw==
|
||||
react-calendar@^4.6.0, react-calendar@^4.8.0:
|
||||
version "4.8.0"
|
||||
resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-4.8.0.tgz#61edbba6d17e7ef8a8012de9143b5e5ff41104c8"
|
||||
integrity sha512-qFgwo+p58sgv1QYMI1oGNaop90eJVKuHTZ3ZgBfrrpUb+9cAexxsKat0sAszgsizPMVo7vOXedV7Lqa0GQGMvA==
|
||||
dependencies:
|
||||
"@wojtekmaj/date-utils" "^1.0.2"
|
||||
clsx "^1.2.1"
|
||||
get-user-locale "^1.2.0"
|
||||
"@wojtekmaj/date-utils" "^1.1.3"
|
||||
clsx "^2.0.0"
|
||||
get-user-locale "^2.2.1"
|
||||
prop-types "^15.6.0"
|
||||
warning "^4.0.0"
|
||||
|
||||
react-clientside-effect@^1.2.6:
|
||||
version "1.2.6"
|
||||
|
@ -14455,14 +14474,14 @@ react-clientside-effect@^1.2.6:
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.12.13"
|
||||
|
||||
react-clock@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/react-clock/-/react-clock-4.0.0.tgz#29d087159154d789c6c93048ae47534b7a7b3fbb"
|
||||
integrity sha512-CBevN5B40TDUegSWzXk6bSwXhYzyerL9JGTme8GMAY0zO4FiEhVTGN1uzgC0rn/oSAMJw3M5wSf/OJpp9vcN2Q==
|
||||
react-clock@^4.5.0:
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-clock/-/react-clock-4.6.0.tgz#61aea8af2b63883e79d258f723abd77d3183a413"
|
||||
integrity sha512-Yz+vwrwrfVRSBw3BdmX/Mc7mVdQYJQ5Pi00qDzGLyLNWQuEmp5PC2oYjQAsDalLjekeDwBIGD7OLcKnkAp1kcw==
|
||||
dependencies:
|
||||
"@wojtekmaj/date-utils" "^1.0.0"
|
||||
clsx "^1.2.1"
|
||||
get-user-locale "^1.4.0"
|
||||
"@wojtekmaj/date-utils" "^1.5.0"
|
||||
clsx "^2.0.0"
|
||||
get-user-locale "^2.2.1"
|
||||
prop-types "^15.6.0"
|
||||
|
||||
react-colorful@^5.1.2:
|
||||
|
@ -14470,36 +14489,35 @@ react-colorful@^5.1.2:
|
|||
resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b"
|
||||
integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==
|
||||
|
||||
react-date-picker@^9.2.0:
|
||||
version "9.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-date-picker/-/react-date-picker-9.2.0.tgz#ee194a694fa9891d93e4d40e76fbcdae7eafbe86"
|
||||
integrity sha512-kAE7HFLq1ic4pS0Pk9SyPTjejIfjTyPov04a2eZzLxfZh8ss8EPaaaX7bBUP4RUCkbxHpR0P4UHloD0/fFDCZw==
|
||||
react-date-picker@^10.5.0:
|
||||
version "10.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-date-picker/-/react-date-picker-10.6.0.tgz#b49ad556cff7009255a8dcbd0f59f4d9e9fdeab1"
|
||||
integrity sha512-db5lcmU/52X8ur8SU1QU3PYBiaDG5SbzZDlqWk3YruPx5Ti9w6UpqCRsd1TXycVla9Ut2I3Qb4BUe27jxSwHeg==
|
||||
dependencies:
|
||||
"@types/react-calendar" "^3.0.0"
|
||||
"@wojtekmaj/date-utils" "^1.0.3"
|
||||
clsx "^1.2.1"
|
||||
get-user-locale "^1.2.0"
|
||||
make-event-props "^1.1.0"
|
||||
"@wojtekmaj/date-utils" "^1.1.3"
|
||||
clsx "^2.0.0"
|
||||
get-user-locale "^2.2.1"
|
||||
make-event-props "^1.6.0"
|
||||
prop-types "^15.6.0"
|
||||
react-calendar "^4.0.0"
|
||||
react-fit "^1.4.0"
|
||||
update-input-width "^1.2.2"
|
||||
react-calendar "^4.6.0"
|
||||
react-fit "^1.7.0"
|
||||
update-input-width "^1.4.0"
|
||||
|
||||
react-datetime-picker@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-datetime-picker/-/react-datetime-picker-4.2.0.tgz#747b86013fa59ce6f9d201317f0df486a343769a"
|
||||
integrity sha512-5K7s4yVpG7e/Y3HspF2iHdaa2OYymqnoV2aUho5J6fQOtVfkOEkMJOwSG4PbSHisq0Xz3CXgOjn88X0GscZoAw==
|
||||
react-datetime-picker@^5.6.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-datetime-picker/-/react-datetime-picker-5.6.0.tgz#2a0bfa041f3333cc9afca349eb8661287aa9abfe"
|
||||
integrity sha512-zbYSuYuiRj4/6lR9xGjAgw7V4gpTGtzOwZIfw1TONj6K6OKuaDreczS+6ijJjwLYlMcx8V+Sw1IMP+K059wnnA==
|
||||
dependencies:
|
||||
"@wojtekmaj/date-utils" "^1.0.3"
|
||||
clsx "^1.2.1"
|
||||
get-user-locale "^1.2.0"
|
||||
make-event-props "^1.1.0"
|
||||
"@wojtekmaj/date-utils" "^1.1.3"
|
||||
clsx "^2.0.0"
|
||||
get-user-locale "^2.2.1"
|
||||
make-event-props "^1.6.0"
|
||||
prop-types "^15.6.0"
|
||||
react-calendar "^4.0.0"
|
||||
react-clock "^4.0.0"
|
||||
react-date-picker "^9.2.0"
|
||||
react-fit "^1.4.0"
|
||||
react-time-picker "^5.2.0"
|
||||
react-calendar "^4.6.0"
|
||||
react-clock "^4.5.0"
|
||||
react-date-picker "^10.5.0"
|
||||
react-fit "^1.7.0"
|
||||
react-time-picker "^6.5.0"
|
||||
|
||||
react-docgen-typescript-plugin@^1.0.5:
|
||||
version "1.0.5"
|
||||
|
@ -14558,12 +14576,12 @@ react-fast-compare@^2.0.1:
|
|||
resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-fit@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/react-fit/-/react-fit-1.4.0.tgz#6b6e3c75215561cc3cfb9854a6811b4347628666"
|
||||
integrity sha512-cf9sFKbr1rlTB9fNIKE5Uy4NCMUOqrX2mdJ69V4RtmV4KubPdtnbIP1tEar16GXaToCRr7I7c9d2wkTNk9TV5g==
|
||||
react-fit@^1.7.0:
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/react-fit/-/react-fit-1.7.1.tgz#95259e90cfa9c4d243a8013d03ea59c9c5c51a6f"
|
||||
integrity sha512-y/TYovCCBzfIwRJsbLj0rH4Es40wPQhU5GPPq9GlbdF09b0OdzTdMSkBza0QixSlgFzTm6dkM7oTFzaVvaBx+w==
|
||||
dependencies:
|
||||
detect-element-overflow "^1.2.0"
|
||||
detect-element-overflow "^1.4.0"
|
||||
prop-types "^15.6.0"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
|
@ -14616,6 +14634,11 @@ react-is@^18.0.0:
|
|||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
|
||||
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
|
||||
|
||||
react-json-view-lite@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/react-json-view-lite/-/react-json-view-lite-1.2.1.tgz#c59a0bea4ede394db331d482ee02e293d38f8218"
|
||||
integrity sha512-Itc0g86fytOmKZoIoJyGgvNqohWSbh3NXIKNgH6W6FT9PC1ck4xas1tT3Rr/b3UlFXyA9Jjaw9QSXdZy2JwGMQ==
|
||||
|
||||
react-query@^3.33.4:
|
||||
version "3.34.4"
|
||||
resolved "https://registry.yarnpkg.com/react-query/-/react-query-3.34.4.tgz#da926717683fd9e9e310d46ab6f60f76a80ffaae"
|
||||
|
@ -14699,19 +14722,19 @@ react-style-singleton@^2.2.1:
|
|||
invariant "^2.2.4"
|
||||
tslib "^2.0.0"
|
||||
|
||||
react-time-picker@^5.2.0:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-time-picker/-/react-time-picker-5.2.0.tgz#e2c49a2b852b63009627084d674705d262f1b7f8"
|
||||
integrity sha512-lM3gISzmPWsG3pZ+D2P/QNF0lrRW9qwpv9mejvwOAlVCuwX7O3nXDHE7gShi/aAd6i9YdU53r3gtDdYg2k+IRQ==
|
||||
react-time-picker@^6.5.0:
|
||||
version "6.6.0"
|
||||
resolved "https://registry.yarnpkg.com/react-time-picker/-/react-time-picker-6.6.0.tgz#5c5264d053dff22cbed9ad0ba927b1ea786c3a49"
|
||||
integrity sha512-1PCetwrYcFNXALU9Oml32NAcFgPCPZLB5U8AQEgBoavJw61YmA0B0OSto6cOz9syGmPdcLZhDqRtN+EkZji+3w==
|
||||
dependencies:
|
||||
"@wojtekmaj/date-utils" "^1.0.0"
|
||||
clsx "^1.2.1"
|
||||
get-user-locale "^1.2.0"
|
||||
make-event-props "^1.1.0"
|
||||
"@wojtekmaj/date-utils" "^1.1.3"
|
||||
clsx "^2.0.0"
|
||||
get-user-locale "^2.2.1"
|
||||
make-event-props "^1.6.0"
|
||||
prop-types "^15.6.0"
|
||||
react-clock "^4.0.0"
|
||||
react-fit "^1.4.0"
|
||||
update-input-width "^1.2.2"
|
||||
react-clock "^4.5.0"
|
||||
react-fit "^1.7.0"
|
||||
update-input-width "^1.4.0"
|
||||
|
||||
react-transition-group@^4.3.0:
|
||||
version "4.4.2"
|
||||
|
@ -16814,10 +16837,10 @@ update-browserslist-db@^1.0.13:
|
|||
escalade "^3.1.1"
|
||||
picocolors "^1.0.0"
|
||||
|
||||
update-input-width@^1.2.2:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/update-input-width/-/update-input-width-1.2.2.tgz#9a6a35858ae8e66fbfe0304437b23a4934fc7d37"
|
||||
integrity sha512-6QwD9ZVSXb96PxOZ01DU0DJTPwQGY7qBYgdniZKJN02Xzom2m+9J6EPxMbefskqtj4x78qbe5psDSALq9iNEYg==
|
||||
update-input-width@^1.4.0:
|
||||
version "1.4.2"
|
||||
resolved "https://registry.yarnpkg.com/update-input-width/-/update-input-width-1.4.2.tgz#49d327a39395185b0fd440b9c3b1d6f81173655c"
|
||||
integrity sha512-/p0XLhrQQQ4bMWD7bL9duYObwYCO1qGr8R19xcMmoMSmXuQ7/1//veUnCObQ7/iW6E2pGS6rFkS4TfH4ur7e/g==
|
||||
|
||||
upper-case-first@^2.0.2:
|
||||
version "2.0.2"
|
||||
|
@ -17079,6 +17102,13 @@ walker@^1.0.8:
|
|||
dependencies:
|
||||
makeerror "1.0.12"
|
||||
|
||||
warning@^4.0.0:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3"
|
||||
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
|
||||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
watchpack@^2.2.0, watchpack@^2.4.0:
|
||||
version "2.4.0"
|
||||
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
|
||||
|
|
Loading…
Reference in New Issue