fix: 2.24 regressions (#190)

Co-authored-by: andres-portainer <andres-portainer@users.noreply.github.com>
Co-authored-by: LP B <xAt0mZ@users.noreply.github.com>
Co-authored-by: testA113 <aliharriss1995@gmail.com>
Co-authored-by: oscarzhou <oscar.zhou@portainer.io>
release/2.24.1 2.24.1
andres-portainer 2024-12-02 21:52:21 -03:00 committed by oscarzhou
parent 5d311031e3
commit b46bff06c6
No known key found for this signature in database
GPG Key ID: A51976F725210F4B
7 changed files with 224 additions and 105 deletions

View File

@ -5,82 +5,110 @@
library css for buttons is overriden by `.widget .widget-body button` library css for buttons is overriden by `.widget .widget-body button`
so we have to force margin: 0 so we have to force margin: 0
*/ */
.react-datetime-picker .react-calendar button { .react-daterange-picker__calendar .react-calendar button {
margin: 0 !important; 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); background: var(--bg-calendar-color);
color: var(--text-main-color); color: var(--text-main-color);
} }
/* calendar nav buttons */ /* calendar nav buttons */
.react-datetime-picker .react-calendar__navigation button:disabled { .react-daterange-picker__calendar .react-calendar__navigation button:disabled {
background-color: var(--bg-calendar-color); background: var(--bg-calendar-color);
@apply opacity-60; @apply opacity-60;
@apply brightness-95 th-dark:brightness-110; @apply brightness-95 th-dark:brightness-110;
} }
.react-datetime-picker .react-calendar__navigation button:enabled:hover, .react-daterange-picker__calendar .react-calendar__navigation button:enabled:hover,
.react-datetime-picker .react-calendar__navigation button:enabled:focus { .react-daterange-picker__calendar .react-calendar__navigation button:enabled:focus {
background-color: var(--bg-daterangepicker-color); background: var(--bg-daterangepicker-color);
} }
/* date tile */ /* date tile */
.react-datetime-picker .react-calendar__tile:disabled { .react-daterange-picker__calendar .react-calendar__tile:disabled {
background-color: var(--bg-calendar-color); background: var(--bg-calendar-color);
@apply opacity-60; @apply opacity-60;
@apply brightness-95 th-dark:brightness-110; @apply brightness-95 th-dark:brightness-110;
} }
.react-datetime-picker .react-calendar__tile:enabled:hover, .react-daterange-picker__calendar .react-calendar__tile:enabled:hover,
.react-datetime-picker .react-calendar__tile:enabled:focus { .react-daterange-picker__calendar .react-calendar__tile:enabled:focus {
background-color: var(--bg-daterangepicker-hover); background: var(--bg-daterangepicker-hover);
} }
/* today's date tile */ /* today's date tile */
.react-datetime-picker .react-calendar__tile--now { .react-daterange-picker__calendar .react-calendar__tile--now {
/* use background color to avoid white on yellow in dark/high contrast modes */
@apply th-highcontrast:text-[color:var(--bg-calendar-color)] th-dark:text-[color:var(--bg-calendar-color)]; @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-daterange-picker__calendar .react-calendar__tile--now:enabled:hover,
.react-datetime-picker .react-calendar__tile--now:enabled:focus { .react-daterange-picker__calendar .react-calendar__tile--now:enabled:focus {
background: var(--bg-daterangepicker-hover); background: var(--bg-daterangepicker-hover);
color: var(--text-daterangepicker-hover); color: var(--text-daterangepicker-hover);
} }
/* probably date tile in range */ /* 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); background: var(--bg-daterangepicker-end-date);
color: var(--text-daterangepicker-end-date); color: var(--text-daterangepicker-end-date);
} }
.react-datetime-picker .react-calendar__tile--hasActive:enabled:hover, .react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:hover,
.react-datetime-picker .react-calendar__tile--hasActive:enabled:focus { .react-daterange-picker__calendar .react-calendar__tile--hasActive:enabled:focus {
background: var(--bg-daterangepicker-hover); background: var(--bg-daterangepicker-hover);
color: var(--text-daterangepicker-hover); color: var(--text-daterangepicker-hover);
} }
/* selected date tile */ .react-daterange-picker__calendar .react-calendar__tile--active:enabled:hover,
.react-datetime-picker .react-calendar__tile--active { .react-daterange-picker__calendar .react-calendar__tile--active:enabled:focus {
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 {
background: var(--bg-daterangepicker-hover); background: var(--bg-daterangepicker-hover);
color: var(--text-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 */ /* on range select hover */
.react-datetime-picker .react-calendar--selectRange .react-calendar__tile--hover { .react-daterange-picker__calendar .react-calendar--selectRange .react-calendar__tile--hover {
background-color: var(--bg-daterangepicker-in-range); background: var(--bg-daterangepicker-in-range);
color: var(--text-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; @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);
}

View File

@ -14,13 +14,8 @@ type StringPortBinding = {
containerPort: number; containerPort: number;
}; };
type NumericPortBinding = {
hostPort: number;
protocol: Protocol;
containerPort: number;
};
type RangePortBinding = { type RangePortBinding = {
hostIp: string;
hostPort: Range; hostPort: Range;
protocol: Protocol; protocol: Protocol;
containerPort: Range; containerPort: Range;
@ -42,9 +37,7 @@ export function toViewModel(portBindings: PortMap): Values {
return value === 'tcp' || value === 'udp'; return value === 'tcp' || value === 'udp';
} }
function parsePorts( function parsePorts(portBindings: PortMap): Array<StringPortBinding> {
portBindings: PortMap
): Array<StringPortBinding | NumericPortBinding> {
return Object.entries(portBindings).flatMap(([key, bindings]) => { return Object.entries(portBindings).flatMap(([key, bindings]) => {
const [containerPort, protocol] = key.split('/'); const [containerPort, protocol] = key.split('/');
@ -63,15 +56,24 @@ export function toViewModel(portBindings: PortMap): Values {
} }
return bindings.map((binding) => { return bindings.map((binding) => {
let port = '';
if (binding.HostPort) {
port = binding.HostPort;
}
if (binding.HostIp) {
port = `${binding.HostIp}:${port}`;
}
if (binding.HostPort?.includes('-')) { if (binding.HostPort?.includes('-')) {
// Range port
return { return {
hostPort: binding.HostPort, hostPort: port,
protocol, protocol,
containerPort: containerPortNumber, containerPort: containerPortNumber,
}; };
} }
return { return {
hostPort: parseInt(binding.HostPort || '0', 10), hostPort: port,
protocol, protocol,
containerPort: containerPortNumber, containerPort: containerPortNumber,
}; };
@ -79,9 +81,9 @@ export function toViewModel(portBindings: PortMap): Values {
}); });
} }
function sortPorts(ports: Array<StringPortBinding | NumericPortBinding>) { function sortPorts(ports: Array<StringPortBinding>) {
const rangePorts = ports.filter(isStringPortBinding); const rangePorts = ports.filter(isRangePortBinding);
const nonRangePorts = ports.filter(isNumericPortBinding); const nonRangePorts = ports.filter((port) => !isRangePortBinding(port));
return { return {
rangePorts, rangePorts,
@ -93,27 +95,40 @@ export function toViewModel(portBindings: PortMap): Values {
}; };
} }
function combinePorts(ports: Array<NumericPortBinding>) { function combinePorts(ports: Array<StringPortBinding>) {
return ports return ports
.reduce((acc, port) => { .reduce((acc, port) => {
let hostIp = '';
let hostPort = 0;
if (port.hostPort.includes(':')) {
const [ipStr, portStr] = port.hostPort.split(':');
hostIp = ipStr;
hostPort = parseInt(portStr || '0', 10);
} else {
hostPort = parseInt(port.hostPort || '0', 10);
}
const lastPort = acc[acc.length - 1]; const lastPort = acc[acc.length - 1];
if ( if (
lastPort && lastPort &&
lastPort.hostIp === hostIp &&
lastPort.containerPort.end === port.containerPort - 1 && lastPort.containerPort.end === port.containerPort - 1 &&
lastPort.hostPort.end === port.hostPort - 1 && lastPort.hostPort.end === hostPort - 1 &&
lastPort.protocol === port.protocol lastPort.protocol === port.protocol
) { ) {
lastPort.hostIp = hostIp;
lastPort.containerPort.end = port.containerPort; lastPort.containerPort.end = port.containerPort;
lastPort.hostPort.end = port.hostPort; lastPort.hostPort.end = hostPort;
return acc; return acc;
} }
return [ return [
...acc, ...acc,
{ {
hostIp,
hostPort: { hostPort: {
start: port.hostPort, start: hostPort,
end: port.hostPort, end: hostPort,
}, },
containerPort: { containerPort: {
start: port.containerPort, start: port.containerPort,
@ -123,34 +138,32 @@ export function toViewModel(portBindings: PortMap): Values {
}, },
]; ];
}, [] as Array<RangePortBinding>) }, [] as Array<RangePortBinding>)
.map(({ protocol, containerPort, hostPort }) => ({ .map(({ protocol, containerPort, hostPort, hostIp }) => ({
hostPort: getRange(hostPort.start, hostPort.end), hostPort: getRange(hostPort.start, hostPort.end, hostIp),
containerPort: getRange(containerPort.start, containerPort.end), containerPort: getRange(containerPort.start, containerPort.end),
protocol, protocol,
})); }));
function getRange(start: number, end: number): string { function getRange(start: number, end: number, hostIp?: string): string {
if (start === end) { if (start === end) {
if (start === 0) { if (start === 0) {
return ''; return '';
} }
if (hostIp) {
return `${hostIp}:${start}`;
}
return start.toString(); return start.toString();
} }
if (hostIp) {
return `${hostIp}:${start}-${end}`;
}
return `${start}-${end}`; return `${start}-${end}`;
} }
} }
} }
function isNumericPortBinding( function isRangePortBinding(port: StringPortBinding): boolean {
port: StringPortBinding | NumericPortBinding return port.hostPort.includes('-');
): port is NumericPortBinding {
return port.hostPort !== 'string';
}
function isStringPortBinding(
port: StringPortBinding | NumericPortBinding
): port is StringPortBinding {
return port.hostPort === 'string';
} }

View File

@ -57,10 +57,15 @@ export async function buildImageFromDockerfileContentAndFiles(
const dockerfile = new Blob([content], { type: 'text/plain' }); const dockerfile = new Blob([content], { type: 'text/plain' });
const uploadFiles = [dockerfile, ...files]; const uploadFiles = [dockerfile, ...files];
const formData = new FormData();
uploadFiles.forEach((file, index) => {
formData.append(`file${index}`, file);
});
return buildImage( return buildImage(
environmentId, environmentId,
{ t: names }, { t: names },
{ file: uploadFiles }, formData,
'multipart/form-data' 'multipart/form-data'
); );
} }

View File

@ -1,8 +1,19 @@
export interface ActivityLog { interface BaseActivityLog {
timestamp: number; timestamp: number;
action: string; action: string;
context: string; context: string;
id: number; id: number;
payload: object;
username: string; username: string;
} }
export interface ActivityLogResponse extends BaseActivityLog {
payload: string;
}
export interface ActivityLog extends BaseActivityLog {
payload: string | object;
}
export interface ActivityLogsResponse {
logs: Array<ActivityLogResponse>;
totalCount: number;
}

View File

@ -4,7 +4,7 @@ import axios, { parseAxiosError } from '@/portainer/services/axios';
import { isBE } from '../../feature-flags/feature-flags.service'; 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 const sortKeys = ['Context', 'Action', 'Timestamp', 'Username'] as const;
export type SortKey = (typeof sortKeys)[number]; export type SortKey = (typeof sortKeys)[number];
@ -30,19 +30,18 @@ export function useActivityLogs(query: Query) {
queryKey: ['activityLogs', query] as const, queryKey: ['activityLogs', query] as const,
queryFn: () => fetchActivityLogs(query), queryFn: () => fetchActivityLogs(query),
keepPreviousData: true, keepPreviousData: true,
select: (data) => ({
...data,
logs: decorateLogs(data.logs),
}),
}); });
} }
interface ActivityLogsResponse {
logs: Array<ActivityLog>;
totalCount: number;
}
async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> { async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> {
try { try {
if (!isBE) { if (!isBE) {
return { return {
logs: [{}, {}, {}, {}, {}] as Array<ActivityLog>, logs: [{}, {}, {}, {}, {}] as Array<ActivityLogResponse>,
totalCount: 5, totalCount: 5,
}; };
} }
@ -56,3 +55,40 @@ async function fetchActivityLogs(query: Query): Promise<ActivityLogsResponse> {
throw parseAxiosError(err, 'Failed loading user activity logs csv'); 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;
}
}

View File

@ -70,32 +70,24 @@ func withComposeService(
return withCli(ctx, options, func(ctx context.Context, cli *command.DockerCli) error { return withCli(ctx, options, func(ctx context.Context, cli *command.DockerCli) error {
composeService := compose.NewComposeService(cli) composeService := compose.NewComposeService(cli)
if len(filePaths) == 0 {
return composeFn(composeService, nil)
}
env, err := parseEnvironment(options)
if err != nil {
return err
}
configDetails := types.ConfigDetails{ configDetails := types.ConfigDetails{
WorkingDir: options.WorkingDir, Environment: env,
Environment: make(map[string]string), WorkingDir: filepath.Dir(filePaths[0]),
} }
for _, p := range filePaths { for _, p := range filePaths {
configDetails.ConfigFiles = append(configDetails.ConfigFiles, types.ConfigFile{Filename: p}) configDetails.ConfigFiles = append(configDetails.ConfigFiles, types.ConfigFile{Filename: p})
} }
envFile := make(map[string]string)
if options.EnvFilePath != "" {
env, err := dotenv.GetEnvFromFile(make(map[string]string), []string{options.EnvFilePath})
if err != nil {
return fmt.Errorf("unable to get the environment from the env file: %w", err)
}
maps.Copy(envFile, env)
configDetails.Environment = env
}
if len(configDetails.ConfigFiles) == 0 {
return composeFn(composeService, nil)
}
project, err := loader.LoadWithContext(ctx, configDetails, project, err := loader.LoadWithContext(ctx, configDetails,
func(o *loader.Options) { func(o *loader.Options) {
o.SkipResolveEnvironment = true o.SkipResolveEnvironment = true
@ -110,22 +102,21 @@ func withComposeService(
return fmt.Errorf("failed to load the compose file: %w", err) return fmt.Errorf("failed to load the compose file: %w", err)
} }
if options.EnvFilePath != "" {
// Work around compose path handling // Work around compose path handling
for i, service := range project.Services { for i, service := range project.Services {
for j, envFile := range service.EnvFiles { for j, envFile := range service.EnvFiles {
if !filepath.IsAbs(envFile.Path) { if !filepath.IsAbs(envFile.Path) {
project.Services[i].EnvFiles[j].Path = filepath.Join(project.WorkingDir, envFile.Path) project.Services[i].EnvFiles[j].Path = filepath.Join(configDetails.WorkingDir, envFile.Path)
} }
} }
} }
// Set the services environment variables
if p, err := project.WithServicesEnvironmentResolved(true); err == nil { if p, err := project.WithServicesEnvironmentResolved(true); err == nil {
project = p project = p
} else { } else {
return fmt.Errorf("failed to resolve services environment: %w", err) return fmt.Errorf("failed to resolve services environment: %w", err)
} }
}
return composeFn(composeService, project) return composeFn(composeService, project)
}) })
@ -136,6 +127,8 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option
return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error { return withComposeService(ctx, filePaths, options.Options, func(composeService api.Service, project *types.Project) error {
addServiceLabels(project, false) addServiceLabels(project, false)
project = project.WithoutUnnecessaryResources()
var opts api.UpOptions var opts api.UpOptions
if options.ForceRecreate { if options.ForceRecreate {
opts.Create.Recreate = api.RecreateForce opts.Create.Recreate = api.RecreateForce
@ -144,6 +137,10 @@ func (c *ComposeDeployer) Deploy(ctx context.Context, filePaths []string, option
opts.Create.RemoveOrphans = options.RemoveOrphans opts.Create.RemoveOrphans = options.RemoveOrphans
opts.Start.CascadeStop = options.AbortOnContainerExit opts.Start.CascadeStop = options.AbortOnContainerExit
if err := composeService.Build(ctx, project, api.BuildOptions{}); err != nil {
return fmt.Errorf("compose build operation failed: %w", err)
}
if err := composeService.Up(ctx, project, opts); err != nil { if err := composeService.Up(ctx, project, opts); err != nil {
return fmt.Errorf("compose up operation failed: %w", err) return fmt.Errorf("compose up operation failed: %w", err)
} }
@ -256,10 +253,36 @@ func addServiceLabels(project *types.Project, oneOff bool) {
api.ProjectLabel: project.Name, api.ProjectLabel: project.Name,
api.ServiceLabel: s.Name, api.ServiceLabel: s.Name,
api.VersionLabel: api.ComposeVersion, api.VersionLabel: api.ComposeVersion,
api.WorkingDirLabel: "/", api.WorkingDirLabel: project.WorkingDir,
api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","), api.ConfigFilesLabel: strings.Join(project.ComposeFiles, ","),
api.OneoffLabel: oneOffLabel, api.OneoffLabel: oneOffLabel,
} }
project.Services[i] = s project.Services[i] = s
} }
} }
func parseEnvironment(options libstack.Options) (map[string]string, error) {
env := make(map[string]string)
for _, envLine := range options.Env {
e, err := dotenv.UnmarshalWithLookup(envLine, nil)
if err != nil {
return nil, fmt.Errorf("unable to parse environment variables: %w", err)
}
maps.Copy(env, e)
}
if options.EnvFilePath == "" {
return env, nil
}
e, err := dotenv.GetEnvFromFile(make(map[string]string), []string{options.EnvFilePath})
if err != nil {
return nil, fmt.Errorf("unable to get the environment from the env file: %w", err)
}
maps.Copy(env, e)
return env, nil
}

View File

@ -14,6 +14,9 @@ export default defineConfig({
}, },
bail: 2, bail: 2,
include: ['./app/**/*.test.ts', './app/**/*.test.tsx'], include: ['./app/**/*.test.ts', './app/**/*.test.tsx'],
env: {
PORTAINER_EDITION: 'CE',
},
}, },
plugins: [svgr({ include: /\?c$/ }), tsconfigPaths()], plugins: [svgr({ include: /\?c$/ }), tsconfigPaths()],
}); });