diff --git a/app/assets/css/react-datetime-picker-override.css b/app/assets/css/react-datetime-picker-override.css index f834265ce..acd26fb58 100644 --- a/app/assets/css/react-datetime-picker-override.css +++ b/app/assets/css/react-datetime-picker-override.css @@ -5,82 +5,110 @@ library css for buttons is overriden by `.widget .widget-body button` so we have to force margin: 0 */ -.react-datetime-picker .react-calendar button { +.react-daterange-picker__calendar .react-calendar button { margin: 0 !important; } /* - Extending Calendar.css from react-datetime-picker + Extending Calendar.css from react-daterange-picker__calendar */ -.react-datetime-picker .react-calendar { +.react-daterange-picker__calendar .react-calendar { background: var(--bg-calendar-color); color: var(--text-main-color); } /* calendar nav buttons */ -.react-datetime-picker .react-calendar__navigation button:disabled { - background-color: var(--bg-calendar-color); +.react-daterange-picker__calendar .react-calendar__navigation button:disabled { + background: var(--bg-calendar-color); @apply opacity-60; @apply brightness-95 th-dark:brightness-110; } -.react-datetime-picker .react-calendar__navigation button:enabled:hover, -.react-datetime-picker .react-calendar__navigation button:enabled:focus { - background-color: var(--bg-daterangepicker-color); +.react-daterange-picker__calendar .react-calendar__navigation button:enabled:hover, +.react-daterange-picker__calendar .react-calendar__navigation button:enabled:focus { + background: var(--bg-daterangepicker-color); } /* date tile */ -.react-datetime-picker .react-calendar__tile:disabled { - background-color: var(--bg-calendar-color); +.react-daterange-picker__calendar .react-calendar__tile:disabled { + background: var(--bg-calendar-color); @apply opacity-60; @apply brightness-95 th-dark:brightness-110; } -.react-datetime-picker .react-calendar__tile:enabled:hover, -.react-datetime-picker .react-calendar__tile:enabled:focus { - background-color: var(--bg-daterangepicker-hover); +.react-daterange-picker__calendar .react-calendar__tile:enabled:hover, +.react-daterange-picker__calendar .react-calendar__tile:enabled:focus { + background: var(--bg-daterangepicker-hover); } /* today's date tile */ -.react-datetime-picker .react-calendar__tile--now { - /* use background color to avoid white on yellow in dark/high contrast modes */ +.react-daterange-picker__calendar .react-calendar__tile--now { @apply th-highcontrast:text-[color:var(--bg-calendar-color)] th-dark:text-[color:var(--bg-calendar-color)]; + border-radius: 0.25rem !important; } -.react-datetime-picker .react-calendar__tile--now:enabled:hover, -.react-datetime-picker .react-calendar__tile--now:enabled:focus { +.react-daterange-picker__calendar .react-calendar__tile--now:enabled:hover, +.react-daterange-picker__calendar .react-calendar__tile--now:enabled:focus { background: var(--bg-daterangepicker-hover); color: var(--text-daterangepicker-hover); } /* probably date tile in range */ -.react-datetime-picker .react-calendar__tile--hasActive { +.react-daterange-picker__calendar .react-calendar__tile--hasActive { background: var(--bg-daterangepicker-end-date); color: var(--text-daterangepicker-end-date); } -.react-datetime-picker .react-calendar__tile--hasActive:enabled:hover, -.react-datetime-picker .react-calendar__tile--hasActive:enabled:focus { +.react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:hover, +.react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:focus { background: var(--bg-daterangepicker-hover); color: var(--text-daterangepicker-hover); } -/* selected date tile */ -.react-datetime-picker .react-calendar__tile--active { - background: var(--bg-daterangepicker-active); - color: var(--text-daterangepicker-active); -} -.react-datetime-picker .react-calendar__tile--active:enabled:hover, -.react-datetime-picker .react-calendar__tile--active:enabled:focus { +.react-daterange-picker__calendar .react-calendar__tile--active:enabled:hover, +.react-daterange-picker__calendar .react-calendar__tile--active:enabled:focus { background: var(--bg-daterangepicker-hover); color: var(--text-daterangepicker-hover); } +.react-daterange-picker__calendar + .react-calendar__month-view__days__day:hover:not(.react-daterange-picker__calendar .react-calendar__tile--hoverEnd):not( + .react-daterange-picker__calendar .react-calendar__tile--hoverStart + ):not(.react-calendar__tile--active) { + border-radius: 0.25rem !important; +} + /* on range select hover */ -.react-datetime-picker .react-calendar--selectRange .react-calendar__tile--hover { - background-color: var(--bg-daterangepicker-in-range); +.react-daterange-picker__calendar .react-calendar--selectRange .react-calendar__tile--hover { + background: var(--bg-daterangepicker-in-range); color: var(--text-daterangepicker-in-range); } /* - Extending DateTimePicker.css from react-datetime-picker + Extending DateTimePicker.css from react-daterange-picker__calendar */ -.react-datetime-picker .react-datetime-picker--disabled { +.react-daterange-picker__calendar .react-daterange-picker__calendar--disabled { @apply opacity-40; } + +/* selected date tile */ +.react-daterange-picker__calendar .react-calendar__tile--active { + background: var(--bg-daterangepicker-active) !important; + color: var(--text-daterangepicker-active) !important; +} + +.react-daterange-picker__calendar .react-calendar__tile--rangeStart:not(.react-calendar__tile--rangeEnd), +.react-daterange-picker__calendar .react-calendar__tile--hoverStart { + border-top-left-radius: 0.25rem; + border-bottom-left-radius: 0.25rem; +} + +.react-daterange-picker__calendar .react-calendar__tile--rangeEnd:not(.react-calendar__tile--rangeStart), +.react-daterange-picker__calendar .react-calendar__tile--hoverEnd { + border-top-right-radius: 0.25rem; + border-bottom-right-radius: 0.25rem; +} + +.react-daterange-picker__calendar .react-calendar__month-view__days__day--weekend { + color: inherit; +} + +.react-calendar__tile--active.react-calendar__month-view__days__day--weekend { + color: var(--text-daterangepicker-active); +} diff --git a/app/react/portainer/logs/ActivityLogsView/types.ts b/app/react/portainer/logs/ActivityLogsView/types.ts index 237c8a257..76176e7ad 100644 --- a/app/react/portainer/logs/ActivityLogsView/types.ts +++ b/app/react/portainer/logs/ActivityLogsView/types.ts @@ -1,8 +1,19 @@ -export interface ActivityLog { +interface BaseActivityLog { timestamp: number; action: string; context: string; id: number; - payload: object; username: string; } +export interface ActivityLogResponse extends BaseActivityLog { + payload: string; +} + +export interface ActivityLog extends BaseActivityLog { + payload: string | object; +} + +export interface ActivityLogsResponse { + logs: Array; + totalCount: number; +} diff --git a/app/react/portainer/logs/ActivityLogsView/useActivityLogs.ts b/app/react/portainer/logs/ActivityLogsView/useActivityLogs.ts index 65a246f10..f55b64416 100644 --- a/app/react/portainer/logs/ActivityLogsView/useActivityLogs.ts +++ b/app/react/portainer/logs/ActivityLogsView/useActivityLogs.ts @@ -4,7 +4,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios'; import { isBE } from '../../feature-flags/feature-flags.service'; -import { ActivityLog } from './types'; +import { ActivityLogResponse, ActivityLogsResponse } from './types'; export const sortKeys = ['Context', 'Action', 'Timestamp', 'Username'] as const; export type SortKey = (typeof sortKeys)[number]; @@ -30,19 +30,18 @@ export function useActivityLogs(query: Query) { queryKey: ['activityLogs', query] as const, queryFn: () => fetchActivityLogs(query), keepPreviousData: true, + select: (data) => ({ + ...data, + logs: decorateLogs(data.logs), + }), }); } -interface ActivityLogsResponse { - logs: Array; - totalCount: number; -} - async function fetchActivityLogs(query: Query): Promise { try { if (!isBE) { return { - logs: [{}, {}, {}, {}, {}] as Array, + logs: [{}, {}, {}, {}, {}] as Array, totalCount: 5, }; } @@ -56,3 +55,40 @@ async function fetchActivityLogs(query: Query): Promise { throw parseAxiosError(err, 'Failed loading user activity logs csv'); } } + +/** + * Decorates logs with the payload parsed from base64 + */ +function decorateLogs(logs?: ActivityLogResponse[]) { + if (!logs || logs.length === 0) { + return []; + } + + return logs.map((log) => ({ + ...log, + payload: parseBase64AsObject(log.payload), + })); +} + +function parseBase64AsObject(value: string): string | object { + if (!value) { + return value; + } + try { + return JSON.parse(safeAtob(value)); + } catch (err) { + return safeAtob(value); + } +} + +function safeAtob(value: string) { + if (!value) { + return value; + } + try { + return window.atob(value); + } catch (err) { + // If the payload is not base64 encoded, return the original value + return value; + } +} diff --git a/vitest.config.mts b/vitest.config.mts index c07c0096b..c54bcdbdd 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -14,6 +14,9 @@ export default defineConfig({ }, bail: 2, include: ['./app/**/*.test.ts', './app/**/*.test.tsx'], + env: { + PORTAINER_EDITION: 'CE', + }, }, plugins: [svgr({ include: /\?c$/ }), tsconfigPaths()], });