diff --git a/app/assets/css/app.css b/app/assets/css/app.css index 3bdfa2f8f..afcf49470 100644 --- a/app/assets/css/app.css +++ b/app/assets/css/app.css @@ -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); diff --git a/app/assets/css/vendor-override.css b/app/assets/css/vendor-override.css index d927ce52d..248d759cc 100644 --- a/app/assets/css/vendor-override.css +++ b/app/assets/css/vendor-override.css @@ -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); diff --git a/app/portainer/react/views/activity-logs.ts b/app/portainer/react/views/activity-logs.ts new file mode 100644 index 000000000..e7427673a --- /dev/null +++ b/app/portainer/react/views/activity-logs.ts @@ -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; diff --git a/app/portainer/react/views/index.ts b/app/portainer/react/views/index.ts index f3ad06fda..0fbba928e 100644 --- a/app/portainer/react/views/index.ts +++ b/app/portainer/react/views/index.ts @@ -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', diff --git a/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.controller.js b/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.controller.js deleted file mode 100644 index aa3f0c854..000000000 --- a/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.controller.js +++ /dev/null @@ -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); - } - } -} diff --git a/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.css b/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.css deleted file mode 100644 index 42eb3b401..000000000 --- a/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.css +++ /dev/null @@ -1,3 +0,0 @@ -.activity-logs-datatable .small-column { - width: 150px; -} diff --git a/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.html b/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.html deleted file mode 100644 index 970090f69..000000000 --- a/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/activity-logs-datatable.html +++ /dev/null @@ -1,95 +0,0 @@ -
- - -
-
-
- -
- Activity Logs -
-
- -
-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - -
-
-
- - -
-
-
- - -
-
-
- -
-
-
- -
-
- -
Loading...
No logs available.
-
- -
-
-
diff --git a/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/index.js b/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/index.js deleted file mode 100644 index 9550fb36e..000000000 --- a/app/portainer/user-activity/activity-logs-view/activity-logs-datatable/index.js +++ /dev/null @@ -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: '<', - }, -}; diff --git a/app/portainer/user-activity/activity-logs-view/activity-logs-view.controller.js b/app/portainer/user-activity/activity-logs-view/activity-logs-view.controller.js deleted file mode 100644 index bd4547b71..000000000 --- a/app/portainer/user-activity/activity-logs-view/activity-logs-view.controller.js +++ /dev/null @@ -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(); - }); - } -} diff --git a/app/portainer/user-activity/activity-logs-view/activity-logs-view.html b/app/portainer/user-activity/activity-logs-view/activity-logs-view.html deleted file mode 100644 index d0e5801f5..000000000 --- a/app/portainer/user-activity/activity-logs-view/activity-logs-view.html +++ /dev/null @@ -1,47 +0,0 @@ - - -
-
- -
- - -
-
- -
- -
-
-
-

- - Portainer user activity logs have a maximum retention of 7 days. -

-
- -
-
-
-
- -
-
-
-
diff --git a/app/portainer/user-activity/activity-logs-view/activity-logs-view.js b/app/portainer/user-activity/activity-logs-view/activity-logs-view.js deleted file mode 100644 index 52082d363..000000000 --- a/app/portainer/user-activity/activity-logs-view/activity-logs-view.js +++ /dev/null @@ -1,6 +0,0 @@ -import controller from './activity-logs-view.controller.js'; - -export const activityLogsView = { - templateUrl: './activity-logs-view.html', - controller, -}; diff --git a/app/portainer/user-activity/activity-logs-view/index.js b/app/portainer/user-activity/activity-logs-view/index.js deleted file mode 100644 index da8c69cb4..000000000 --- a/app/portainer/user-activity/activity-logs-view/index.js +++ /dev/null @@ -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; diff --git a/app/portainer/user-activity/index.js b/app/portainer/user-activity/index.js index d6fb1b67c..0a04640e0 100644 --- a/app/portainer/user-activity/index.js +++ b/app/portainer/user-activity/index.js @@ -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) { diff --git a/app/portainer/user-activity/user-activity.rest.js b/app/portainer/user-activity/user-activity.rest.js new file mode 100644 index 000000000..4ed527e81 --- /dev/null +++ b/app/portainer/user-activity/user-activity.rest.js @@ -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', + }, + }); + } +} diff --git a/app/portainer/user-activity/user-activity.service.js b/app/portainer/user-activity/user-activity.service.js new file mode 100644 index 000000000..5c3c454de --- /dev/null +++ b/app/portainer/user-activity/user-activity.service.js @@ -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'); + } +} diff --git a/app/react/components/JsonTree.css b/app/react/components/JsonTree.css new file mode 100644 index 000000000..181d93437 --- /dev/null +++ b/app/react/components/JsonTree.css @@ -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 */ diff --git a/app/react/components/JsonTree.tsx b/app/react/components/JsonTree.tsx new file mode 100644 index 000000000..4dfc78687 --- /dev/null +++ b/app/react/components/JsonTree.tsx @@ -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) { + const currentStyle = getCurrentStyle(style); + return ( + + ); +} + +type StyleProps = ComponentProps['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'), + }; +} diff --git a/app/react/portainer/environments/update-schedules/common/ScheduledTimeField.tsx b/app/react/portainer/environments/update-schedules/common/ScheduledTimeField.tsx index cacb17cb9..61c6dd069 100644 --- a/app/react/portainer/environments/update-schedules/common/ScheduledTimeField.tsx +++ b/app/react/portainer/environments/update-schedules/common/ScheduledTimeField.tsx @@ -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; } diff --git a/app/react/portainer/logs/.keep b/app/react/portainer/logs/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/portainer/logs/ActivityLogsView/.keep b/app/react/portainer/logs/ActivityLogsView/.keep deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/react/portainer/logs/ActivityLogsView/ActivityLogsTable.tsx b/app/react/portainer/logs/ActivityLogsView/ActivityLogsTable.tsx new file mode 100644 index 000000000..086f99427 --- /dev/null +++ b/app/react/portainer/logs/ActivityLogsView/ActivityLogsTable.tsx @@ -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(); + +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() ? ( + + ) : 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; +}) { + return ( + + 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) => } + /> + ); +} + +function SubRow({ item }: { item: ActivityLog }) { + return ( + + + + + + ); +} diff --git a/app/react/portainer/logs/ActivityLogsView/ActivityLogsView.tsx b/app/react/portainer/logs/ActivityLogsView/ActivityLogsView.tsx new file mode 100644 index 000000000..59f18f7d0 --- /dev/null +++ b/app/react/portainer/logs/ActivityLogsView/ActivityLogsView.tsx @@ -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 ( + <> + + +
+ + + +
+ + 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} + /> +
+
+
+ + ); + + function handleExport() { + exportMutation.mutate(query); + } +} + +function seconds(ms?: number) { + if (!ms) { + return undefined; + } + + return Math.floor(ms / 1000); +} diff --git a/app/react/portainer/logs/ActivityLogsView/FilterBar.tsx b/app/react/portainer/logs/ActivityLogsView/FilterBar.tsx new file mode 100644 index 000000000..822372701 --- /dev/null +++ b/app/react/portainer/logs/ActivityLogsView/FilterBar.tsx @@ -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 ( + + +
+ + + + Portainer user activity logs have a maximum retention of 7 days. + + +
+ + +
+ +
+
+ ); +} diff --git a/app/react/portainer/logs/ActivityLogsView/types.ts b/app/react/portainer/logs/ActivityLogsView/types.ts new file mode 100644 index 000000000..237c8a257 --- /dev/null +++ b/app/react/portainer/logs/ActivityLogsView/types.ts @@ -0,0 +1,8 @@ +export interface ActivityLog { + timestamp: number; + action: string; + context: string; + id: number; + payload: object; + username: string; +} diff --git a/app/react/portainer/logs/ActivityLogsView/useActivityLogs.ts b/app/react/portainer/logs/ActivityLogsView/useActivityLogs.ts new file mode 100644 index 000000000..3883e9d2a --- /dev/null +++ b/app/react/portainer/logs/ActivityLogsView/useActivityLogs.ts @@ -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; + totalCount: number; +} + +async function fetchActivityLogs(query: Query): Promise { + try { + if (!isBE) { + return { + logs: [{}, {}, {}, {}, {}] as Array, + totalCount: 5, + }; + } + + const { data } = await axios.get( + '/useractivity/logs', + { params: query } + ); + return data; + } catch (err) { + throw parseAxiosError(err, 'Failed loading user activity logs csv'); + } +} diff --git a/app/react/portainer/logs/ActivityLogsView/useExportMutation.ts b/app/react/portainer/logs/ActivityLogsView/useExportMutation.ts new file mode 100644 index 000000000..c9bcfe87e --- /dev/null +++ b/app/react/portainer/logs/ActivityLogsView/useExportMutation.ts @@ -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) { + try { + const { data, headers } = await axios.get('/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'); + } +} diff --git a/app/react/portainer/logs/components/DateRangePicker.tsx b/app/react/portainer/logs/components/DateRangePicker.tsx new file mode 100644 index 000000000..4ad27e6f6 --- /dev/null +++ b/app/react/portainer/logs/components/DateRangePicker.tsx @@ -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; +}) { + return ( + +
+ { + 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={} + clearIcon={} + /> +
+
+ ); +} + +export function dateRangePickerValidation(): SchemaOf { + return object({ + start: date().required(), + end: date().nullable().default(null).required(), + }); +} diff --git a/package.json b/package.json index 384a686fd..45e8e6af0 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 9f812ce2d..a9f2a697f 100644 --- a/yarn.lock +++ b/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"