mirror of
https://github.com/mermaid-js/mermaid.git
synced 2025-12-02 18:45:14 +01:00
5
.changeset/tricky-rivers-stand.md
Normal file
5
.changeset/tricky-rivers-stand.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'mermaid': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: validate dates and tick interval to prevent UI freeze/crash in gantt diagramtype
|
||||||
@@ -833,4 +833,34 @@ describe('Gantt diagram', () => {
|
|||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
it('should handle seconds-only format with tickInterval (issue #5496)', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
gantt
|
||||||
|
tickInterval 1second
|
||||||
|
dateFormat ss
|
||||||
|
axisFormat %s
|
||||||
|
|
||||||
|
section Network Request
|
||||||
|
RTT : rtt, 0, 20
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
it('should handle dates with year typo like 202 instead of 2024 (issue #5496)', () => {
|
||||||
|
imgSnapshotTest(
|
||||||
|
`
|
||||||
|
gantt
|
||||||
|
title Schedule
|
||||||
|
dateFormat YYYY-MM-DD
|
||||||
|
tickInterval 1week
|
||||||
|
axisFormat %m-%d
|
||||||
|
|
||||||
|
section Vacation
|
||||||
|
London : 2024-12-01, 7d
|
||||||
|
London : 202-12-01, 7d
|
||||||
|
`,
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -268,7 +268,15 @@ const fixTaskDates = function (startTime, endTime, dateFormat, excludes, include
|
|||||||
|
|
||||||
const getStartDate = function (prevTime, dateFormat, str) {
|
const getStartDate = function (prevTime, dateFormat, str) {
|
||||||
str = str.trim();
|
str = str.trim();
|
||||||
if ((dateFormat.trim() === 'x' || dateFormat.trim() === 'X') && /^\d+$/.test(str)) {
|
|
||||||
|
// Helper function to check if format is a timestamp format (x or X)
|
||||||
|
const isTimestampFormat = (format) => {
|
||||||
|
const trimmedFormat = format.trim();
|
||||||
|
return trimmedFormat === 'x' || trimmedFormat === 'X';
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle timestamp formats (x, X) with numeric strings
|
||||||
|
if (isTimestampFormat(dateFormat) && /^\d+$/.test(str)) {
|
||||||
return new Date(Number(str));
|
return new Date(Number(str));
|
||||||
}
|
}
|
||||||
// Test for after
|
// Test for after
|
||||||
@@ -293,13 +301,15 @@ const getStartDate = function (prevTime, dateFormat, str) {
|
|||||||
return today;
|
return today;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for actual date set
|
// Check for actual date set using dayjs strict parsing
|
||||||
let mDate = dayjs(str, dateFormat.trim(), true);
|
let mDate = dayjs(str, dateFormat.trim(), true);
|
||||||
if (mDate.isValid()) {
|
if (mDate.isValid()) {
|
||||||
return mDate.toDate();
|
return mDate.toDate();
|
||||||
} else {
|
} else {
|
||||||
log.debug('Invalid date:' + str);
|
log.debug('Invalid date:' + str);
|
||||||
log.debug('With date format:' + dateFormat.trim());
|
log.debug('With date format:' + dateFormat.trim());
|
||||||
|
|
||||||
|
// Timestamp formats can fall back to new Date()
|
||||||
const d = new Date(str);
|
const d = new Date(str);
|
||||||
if (
|
if (
|
||||||
d === undefined ||
|
d === undefined ||
|
||||||
|
|||||||
@@ -505,4 +505,27 @@ describe('when using the ganttDb', function () {
|
|||||||
ganttDb.addTask('test1', 'id1,202304,1d');
|
ganttDb.addTask('test1', 'id1,202304,1d');
|
||||||
expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304');
|
expect(() => ganttDb.getTasks()).toThrowError('Invalid date:202304');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle seconds-only format with valid numeric values (issue #5496)', function () {
|
||||||
|
ganttDb.setDateFormat('ss');
|
||||||
|
ganttDb.addSection('Network Request');
|
||||||
|
ganttDb.addTask('RTT', 'rtt, 0, 20');
|
||||||
|
const tasks = ganttDb.getTasks();
|
||||||
|
expect(tasks).toHaveLength(1);
|
||||||
|
expect(tasks[0].task).toBe('RTT');
|
||||||
|
expect(tasks[0].id).toBe('rtt');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle dates with year typo like 202 instead of 2024 (issue #5496)', function () {
|
||||||
|
ganttDb.setDateFormat('YYYY-MM-DD');
|
||||||
|
ganttDb.addSection('Vacation');
|
||||||
|
ganttDb.addTask('London Trip 1', '2024-12-01, 7d');
|
||||||
|
ganttDb.addTask('London Trip 2', '202-12-01, 7d');
|
||||||
|
const tasks = ganttDb.getTasks();
|
||||||
|
expect(tasks).toHaveLength(2);
|
||||||
|
// First task should be in year 2024
|
||||||
|
expect(tasks[0].startTime.getFullYear()).toBe(2024);
|
||||||
|
// Second task will be parsed as year 202 (fallback to new Date())
|
||||||
|
expect(tasks[1].startTime.getFullYear()).toBe(202);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
import dayjsDuration from 'dayjs/plugin/duration.js';
|
||||||
import { log } from '../../logger.js';
|
import { log } from '../../logger.js';
|
||||||
import {
|
import {
|
||||||
select,
|
select,
|
||||||
@@ -28,6 +29,8 @@ import common from '../common/common.js';
|
|||||||
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
import { getConfig } from '../../diagram-api/diagramAPI.js';
|
||||||
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
import { configureSvgSize } from '../../setupGraphViewbox.js';
|
||||||
|
|
||||||
|
dayjs.extend(dayjsDuration);
|
||||||
|
|
||||||
export const setConf = function () {
|
export const setConf = function () {
|
||||||
log.debug('Something is calling, setConf, remove the call');
|
log.debug('Something is calling, setConf, remove the call');
|
||||||
};
|
};
|
||||||
@@ -78,6 +81,7 @@ const getMaxIntersections = (tasks, orderOffset) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let w;
|
let w;
|
||||||
|
const MAX_TICK_COUNT = 10000;
|
||||||
export const draw = function (text, id, version, diagObj) {
|
export const draw = function (text, id, version, diagObj) {
|
||||||
const conf = getConfig().gantt;
|
const conf = getConfig().gantt;
|
||||||
|
|
||||||
@@ -602,6 +606,27 @@ export const draw = function (text, id, version, diagObj) {
|
|||||||
.attr('class', 'exclude-range');
|
.attr('class', 'exclude-range');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates the estimated number of ticks based on the time domain and tick interval.
|
||||||
|
* Returns the estimated number of ticks as a number.
|
||||||
|
* @param {Date} minTime - The minimum time in the domain
|
||||||
|
* @param {Date} maxTime - The maximum time in the domain
|
||||||
|
* @param {number} every - The interval count (e.g., 1 for "1second")
|
||||||
|
* @param {string} interval - The interval unit (e.g., "second", "day")
|
||||||
|
* @returns {number} The estimated number of ticks
|
||||||
|
*/
|
||||||
|
function getEstimatedTickCount(minTime, maxTime, every, interval) {
|
||||||
|
if (every <= 0 || minTime > maxTime) {
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
const timeDiffMs = maxTime - minTime;
|
||||||
|
const intervalMs = dayjs.duration({ [interval ?? 'day']: every }).asMilliseconds();
|
||||||
|
if (intervalMs <= 0) {
|
||||||
|
return Infinity;
|
||||||
|
}
|
||||||
|
return Math.ceil(timeDiffMs / intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param theSidePad
|
* @param theSidePad
|
||||||
* @param theTopPad
|
* @param theTopPad
|
||||||
@@ -630,10 +655,30 @@ export const draw = function (text, id, version, diagObj) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (resultTickInterval !== null) {
|
if (resultTickInterval !== null) {
|
||||||
const every = resultTickInterval[1];
|
const every = parseInt(resultTickInterval[1], 10);
|
||||||
|
if (isNaN(every) || every <= 0) {
|
||||||
|
log.warn(
|
||||||
|
`Invalid tick interval value: "${resultTickInterval[1]}". Skipping custom tick interval.`
|
||||||
|
);
|
||||||
|
// Skip applying custom ticks
|
||||||
|
} else {
|
||||||
const interval = resultTickInterval[2];
|
const interval = resultTickInterval[2];
|
||||||
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
||||||
|
|
||||||
|
// Get the time domain to check tick count
|
||||||
|
const domain = timeScale.domain();
|
||||||
|
const minTime = domain[0];
|
||||||
|
const maxTime = domain[1];
|
||||||
|
const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval);
|
||||||
|
|
||||||
|
if (estimatedTicks > MAX_TICK_COUNT) {
|
||||||
|
log.warn(
|
||||||
|
`The tick interval "${every}${interval}" would generate ${estimatedTicks} ticks, ` +
|
||||||
|
`which exceeds the maximum allowed (${MAX_TICK_COUNT}). ` +
|
||||||
|
`This may indicate an invalid date or time range. Skipping custom tick interval.`
|
||||||
|
);
|
||||||
|
// D3 will use its default automatic tick generation
|
||||||
|
} else {
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case 'millisecond':
|
case 'millisecond':
|
||||||
bottomXAxis.ticks(timeMillisecond.every(every));
|
bottomXAxis.ticks(timeMillisecond.every(every));
|
||||||
@@ -658,6 +703,8 @@ export const draw = function (text, id, version, diagObj) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svg
|
svg
|
||||||
.append('g')
|
.append('g')
|
||||||
@@ -677,10 +724,24 @@ export const draw = function (text, id, version, diagObj) {
|
|||||||
.tickFormat(timeFormat(axisFormat));
|
.tickFormat(timeFormat(axisFormat));
|
||||||
|
|
||||||
if (resultTickInterval !== null) {
|
if (resultTickInterval !== null) {
|
||||||
const every = resultTickInterval[1];
|
const every = parseInt(resultTickInterval[1], 10);
|
||||||
|
if (isNaN(every) || every <= 0) {
|
||||||
|
log.warn(
|
||||||
|
`Invalid tick interval value: "${resultTickInterval[1]}". Skipping custom tick interval.`
|
||||||
|
);
|
||||||
|
// Skip applying custom ticks
|
||||||
|
} else {
|
||||||
const interval = resultTickInterval[2];
|
const interval = resultTickInterval[2];
|
||||||
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
const weekday = diagObj.db.getWeekday() || conf.weekday;
|
||||||
|
|
||||||
|
// Get the time domain to check tick count
|
||||||
|
const domain = timeScale.domain();
|
||||||
|
const minTime = domain[0];
|
||||||
|
const maxTime = domain[1];
|
||||||
|
const estimatedTicks = getEstimatedTickCount(minTime, maxTime, every, interval);
|
||||||
|
|
||||||
|
// Only apply custom ticks if the count is reasonable
|
||||||
|
if (estimatedTicks <= MAX_TICK_COUNT) {
|
||||||
switch (interval) {
|
switch (interval) {
|
||||||
case 'millisecond':
|
case 'millisecond':
|
||||||
topXAxis.ticks(timeMillisecond.every(every));
|
topXAxis.ticks(timeMillisecond.every(every));
|
||||||
@@ -705,6 +766,8 @@ export const draw = function (text, id, version, diagObj) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
svg
|
svg
|
||||||
.append('g')
|
.append('g')
|
||||||
|
|||||||
Reference in New Issue
Block a user