@n8n/frontend-module-insights 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +88 -0
- package/README.md +64 -0
- package/biome.jsonc +4 -0
- package/eslint.config.mjs +33 -0
- package/package.json +72 -0
- package/src/__tests__/setup.ts +4 -0
- package/src/chartjs.utils.ts +208 -0
- package/src/components/InsightsDashboard.test.ts +891 -0
- package/src/components/InsightsDashboard.vue +426 -0
- package/src/components/InsightsDataRangePicker.vue +195 -0
- package/src/components/InsightsDateRangeAlert.test.ts +134 -0
- package/src/components/InsightsDateRangeAlert.vue +89 -0
- package/src/components/InsightsPaywall.vue +42 -0
- package/src/components/InsightsSummary.test.ts +206 -0
- package/src/components/InsightsSummary.vue +357 -0
- package/src/components/InsightsUpgradeModal.vue +74 -0
- package/src/components/__snapshots__/InsightsSummary.test.ts.snap +446 -0
- package/src/components/charts/InsightsChartAverageRuntime.vue +67 -0
- package/src/components/charts/InsightsChartFailed.vue +57 -0
- package/src/components/charts/InsightsChartFailureRate.vue +58 -0
- package/src/components/charts/InsightsChartTimeSaved.vue +74 -0
- package/src/components/charts/InsightsChartTotal.vue +58 -0
- package/src/components/charts/insightChartProps.ts +9 -0
- package/src/components/tables/InsightsTableWorkflows.test.ts +509 -0
- package/src/components/tables/InsightsTableWorkflows.vue +279 -0
- package/src/index.ts +4 -0
- package/src/insights.api.test.ts +435 -0
- package/src/insights.api.ts +71 -0
- package/src/insights.constants.ts +69 -0
- package/src/insights.module.test.ts +59 -0
- package/src/insights.module.ts +45 -0
- package/src/insights.store.test.ts +118 -0
- package/src/insights.store.ts +88 -0
- package/src/insights.types.ts +16 -0
- package/src/insights.utils.test.ts +339 -0
- package/src/insights.utils.ts +172 -0
- package/stylelint.config.mjs +9 -0
- package/vite.config.ts +29 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import { CalendarDate, getLocalTimeZone, today } from '@internationalized/date';
|
|
2
|
+
import type { InsightsSummary } from '@n8n/api-types';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
INSIGHTS_UNIT_MAPPING,
|
|
6
|
+
INSIGHTS_DEVIATION_UNIT_MAPPING,
|
|
7
|
+
INSIGHTS_SUMMARY_ORDER,
|
|
8
|
+
} from './insights.constants';
|
|
9
|
+
import {
|
|
10
|
+
transformInsightsTimeSaved,
|
|
11
|
+
transformInsightsAverageRunTime,
|
|
12
|
+
transformInsightsFailureRate,
|
|
13
|
+
transformInsightsValues,
|
|
14
|
+
transformInsightsDeviation,
|
|
15
|
+
transformInsightsSummary,
|
|
16
|
+
formatDateRange,
|
|
17
|
+
getMatchingPreset,
|
|
18
|
+
timeRangeMappings,
|
|
19
|
+
} from './insights.utils';
|
|
20
|
+
|
|
21
|
+
describe('Insights Transformers', () => {
|
|
22
|
+
describe('transformInsightsTimeSaved', () => {
|
|
23
|
+
it('should return minutes if absolute value is less than 60', () => {
|
|
24
|
+
expect(transformInsightsTimeSaved(30)).toBe(30);
|
|
25
|
+
expect(transformInsightsTimeSaved(59)).toBe(59);
|
|
26
|
+
expect(transformInsightsTimeSaved(-30)).toBe(-30);
|
|
27
|
+
expect(transformInsightsTimeSaved(-59)).toBe(-59);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return hours if absolute value is 60 or more, rounding to nearest hour', () => {
|
|
31
|
+
expect(transformInsightsTimeSaved(60)).toBe(1);
|
|
32
|
+
expect(transformInsightsTimeSaved(120)).toBe(2);
|
|
33
|
+
expect(transformInsightsTimeSaved(89)).toBe(1);
|
|
34
|
+
expect(transformInsightsTimeSaved(90)).toBe(2);
|
|
35
|
+
expect(transformInsightsTimeSaved(-60)).toBe(-1);
|
|
36
|
+
expect(transformInsightsTimeSaved(-120)).toBe(-2);
|
|
37
|
+
expect(transformInsightsTimeSaved(-89)).toBe(-1);
|
|
38
|
+
expect(transformInsightsTimeSaved(-90)).toBe(-1);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should return 0 for 0 minutes', () => {
|
|
42
|
+
expect(transformInsightsTimeSaved(0)).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('transformInsightsAverageRunTime', () => {
|
|
47
|
+
it('should convert milliseconds to seconds', () => {
|
|
48
|
+
expect(transformInsightsAverageRunTime(1000)).toBe(1);
|
|
49
|
+
expect(transformInsightsAverageRunTime(500)).toBe(0.5);
|
|
50
|
+
expect(transformInsightsAverageRunTime(1234)).toBe(1.234);
|
|
51
|
+
expect(transformInsightsAverageRunTime(0)).toBe(0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('transformInsightsFailureRate', () => {
|
|
56
|
+
it('should convert decimal to percentage', () => {
|
|
57
|
+
expect(transformInsightsFailureRate(0.5)).toBe(50);
|
|
58
|
+
expect(transformInsightsFailureRate(1)).toBe(100);
|
|
59
|
+
expect(transformInsightsFailureRate(0.253)).toBe(25.3);
|
|
60
|
+
expect(transformInsightsFailureRate(0)).toBe(0);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('transformInsightsValues', () => {
|
|
65
|
+
it('should correctly transform values based on type', () => {
|
|
66
|
+
expect(transformInsightsValues.total(100)).toBe(100);
|
|
67
|
+
expect(transformInsightsValues.failed(10)).toBe(10);
|
|
68
|
+
expect(transformInsightsValues.timeSaved(120)).toBe(2);
|
|
69
|
+
expect(transformInsightsValues.timeSaved(30)).toBe(30);
|
|
70
|
+
expect(transformInsightsValues.averageRunTime(2000)).toBe(2);
|
|
71
|
+
expect(transformInsightsValues.failureRate(0.1)).toBe(10);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('transformInsightsDeviation', () => {
|
|
76
|
+
describe('for total and failed types', () => {
|
|
77
|
+
it('should calculate percentage deviation', () => {
|
|
78
|
+
expect(transformInsightsDeviation.total(110, 10)).toBe(10);
|
|
79
|
+
expect(transformInsightsDeviation.failed(55, 5)).toBe(10);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return 0 if value and deviation are 0', () => {
|
|
83
|
+
expect(transformInsightsDeviation.total(0, 0)).toBe(0);
|
|
84
|
+
expect(transformInsightsDeviation.failed(0, 0)).toBe(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it('should return Infinity if value equals deviation (and thus previous value is 0)', () => {
|
|
88
|
+
expect(transformInsightsDeviation.total(10, 10)).toBe(null);
|
|
89
|
+
expect(transformInsightsDeviation.failed(5, 5)).toBe(null);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return 0 if deviation is 0 and value is not 0', () => {
|
|
93
|
+
expect(transformInsightsDeviation.total(100, 0)).toBe(0);
|
|
94
|
+
expect(transformInsightsDeviation.failed(50, 0)).toBe(0);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('for timeSaved type, should transform deviation using transformInsightsTimeSaved', () => {
|
|
99
|
+
expect(transformInsightsDeviation.timeSaved(1000, 120)).toBe(2);
|
|
100
|
+
expect(transformInsightsDeviation.timeSaved(1000, 30)).toBe(30);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('for averageRunTime type, should transform deviation using transformInsightsAverageRunTime', () => {
|
|
104
|
+
expect(transformInsightsDeviation.averageRunTime(5000, 1000)).toBe(1);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('for failureRate type, should transform deviation using transformInsightsFailureRate', () => {
|
|
108
|
+
expect(transformInsightsDeviation.failureRate(0.5, 0.1)).toBe(10);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('transformInsightsSummary', () => {
|
|
113
|
+
beforeEach(() => {
|
|
114
|
+
vi.clearAllMocks();
|
|
115
|
+
|
|
116
|
+
INSIGHTS_SUMMARY_ORDER.forEach((key) => {
|
|
117
|
+
vi.spyOn(INSIGHTS_UNIT_MAPPING, key);
|
|
118
|
+
vi.spyOn(INSIGHTS_DEVIATION_UNIT_MAPPING, key);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return an empty array if data is null', () => {
|
|
123
|
+
expect(transformInsightsSummary(null)).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('should correctly transform InsightsSummary data and respect INSIGHTS_SUMMARY_ORDER', () => {
|
|
127
|
+
const summaryData: InsightsSummary = {
|
|
128
|
+
timeSaved: { value: 1200, deviation: 120, unit: 'minute' },
|
|
129
|
+
total: { value: 110, deviation: 10, unit: 'count' },
|
|
130
|
+
failureRate: { value: 0.05, deviation: 0.01, unit: 'ratio' },
|
|
131
|
+
averageRunTime: { value: 5000, deviation: 1000, unit: 'millisecond' },
|
|
132
|
+
failed: { value: 5, deviation: 1, unit: 'count' },
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const expectedOutput = [
|
|
136
|
+
{
|
|
137
|
+
id: 'total',
|
|
138
|
+
value: 110,
|
|
139
|
+
deviation: 10,
|
|
140
|
+
deviationUnit: '%',
|
|
141
|
+
unit: '',
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
id: 'failed',
|
|
145
|
+
value: 5,
|
|
146
|
+
deviation: 25,
|
|
147
|
+
deviationUnit: '%',
|
|
148
|
+
unit: '',
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
id: 'failureRate',
|
|
152
|
+
value: 5,
|
|
153
|
+
deviation: 1,
|
|
154
|
+
deviationUnit: 'pp',
|
|
155
|
+
unit: '%',
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
id: 'timeSaved',
|
|
159
|
+
value: 20,
|
|
160
|
+
deviation: 2,
|
|
161
|
+
deviationUnit: 'h',
|
|
162
|
+
unit: 'h',
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'averageRunTime',
|
|
166
|
+
value: 5,
|
|
167
|
+
deviation: 1,
|
|
168
|
+
deviationUnit: 's',
|
|
169
|
+
unit: 's',
|
|
170
|
+
},
|
|
171
|
+
];
|
|
172
|
+
|
|
173
|
+
const result = transformInsightsSummary(summaryData);
|
|
174
|
+
expect(result).toEqual(expectedOutput);
|
|
175
|
+
expect(result.map((item) => item.id)).toEqual([
|
|
176
|
+
'total',
|
|
177
|
+
'failed',
|
|
178
|
+
'failureRate',
|
|
179
|
+
'timeSaved',
|
|
180
|
+
'averageRunTime',
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
expect(INSIGHTS_UNIT_MAPPING.total).toHaveBeenCalledWith(110);
|
|
184
|
+
expect(INSIGHTS_DEVIATION_UNIT_MAPPING.total).toHaveBeenCalledWith(10);
|
|
185
|
+
expect(INSIGHTS_UNIT_MAPPING.failed).toHaveBeenCalledWith(5);
|
|
186
|
+
expect(INSIGHTS_DEVIATION_UNIT_MAPPING.failed).toHaveBeenCalledWith(1);
|
|
187
|
+
expect(INSIGHTS_UNIT_MAPPING.timeSaved).toHaveBeenCalledWith(1200);
|
|
188
|
+
expect(INSIGHTS_DEVIATION_UNIT_MAPPING.timeSaved).toHaveBeenCalledWith(120);
|
|
189
|
+
expect(INSIGHTS_UNIT_MAPPING.averageRunTime).toHaveBeenCalledWith(5000);
|
|
190
|
+
expect(INSIGHTS_DEVIATION_UNIT_MAPPING.averageRunTime).toHaveBeenCalledWith(1000);
|
|
191
|
+
expect(INSIGHTS_UNIT_MAPPING.failureRate).toHaveBeenCalledWith(0.05);
|
|
192
|
+
expect(INSIGHTS_DEVIATION_UNIT_MAPPING.failureRate).toHaveBeenCalledWith(0.01);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should handle null deviation correctly', () => {
|
|
196
|
+
const summaryData: InsightsSummary = {
|
|
197
|
+
total: { value: 100, deviation: null, unit: 'count' },
|
|
198
|
+
failed: { value: 5, deviation: 1, unit: 'count' },
|
|
199
|
+
timeSaved: { value: 30, deviation: null, unit: 'minute' },
|
|
200
|
+
averageRunTime: { value: 2000, deviation: 500, unit: 'millisecond' },
|
|
201
|
+
failureRate: { value: 0.1, deviation: null, unit: 'ratio' },
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const expectedOutput = [
|
|
205
|
+
{
|
|
206
|
+
id: 'total',
|
|
207
|
+
value: 100,
|
|
208
|
+
deviation: null,
|
|
209
|
+
deviationUnit: '',
|
|
210
|
+
unit: '',
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
id: 'failed',
|
|
214
|
+
value: 5,
|
|
215
|
+
deviation: 25,
|
|
216
|
+
deviationUnit: '%',
|
|
217
|
+
unit: '',
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
id: 'failureRate',
|
|
221
|
+
value: 10,
|
|
222
|
+
deviation: null,
|
|
223
|
+
deviationUnit: '',
|
|
224
|
+
unit: '%',
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: 'timeSaved',
|
|
228
|
+
value: 30,
|
|
229
|
+
deviation: null,
|
|
230
|
+
deviationUnit: '',
|
|
231
|
+
unit: 'm',
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: 'averageRunTime',
|
|
235
|
+
value: 2,
|
|
236
|
+
deviation: 0.5,
|
|
237
|
+
deviationUnit: 's',
|
|
238
|
+
unit: 's',
|
|
239
|
+
},
|
|
240
|
+
];
|
|
241
|
+
expect(transformInsightsSummary(summaryData)).toEqual(expectedOutput);
|
|
242
|
+
|
|
243
|
+
expect(INSIGHTS_UNIT_MAPPING.total).toHaveBeenCalledWith(100);
|
|
244
|
+
expect(INSIGHTS_DEVIATION_UNIT_MAPPING.total).not.toHaveBeenCalled(); // deviation is null
|
|
245
|
+
|
|
246
|
+
expect(INSIGHTS_UNIT_MAPPING.timeSaved).toHaveBeenCalledWith(30);
|
|
247
|
+
expect(INSIGHTS_DEVIATION_UNIT_MAPPING.timeSaved).not.toHaveBeenCalled(); // deviation is null
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
describe('formatDateRange', () => {
|
|
252
|
+
afterEach(() => {
|
|
253
|
+
vi.clearAllMocks();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it('should return empty string if start date is not provided', () => {
|
|
257
|
+
const result = formatDateRange({ end: new CalendarDate(2025, 6, 5) });
|
|
258
|
+
|
|
259
|
+
expect(result).toBe('');
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it('should return formatted start date if the end date is not provided', () => {
|
|
263
|
+
const result = formatDateRange({ start: new CalendarDate(2025, 2, 28) });
|
|
264
|
+
|
|
265
|
+
expect(result).toBe('28 Feb, 2025');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should return formatted start date if the end date is the same as the start date', () => {
|
|
269
|
+
const result = formatDateRange({
|
|
270
|
+
start: new CalendarDate(2025, 10, 20),
|
|
271
|
+
end: new CalendarDate(2025, 10, 20),
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
expect(result).toBe('20 Oct, 2025');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('should return formatted range for same year range', () => {
|
|
278
|
+
const result = formatDateRange({
|
|
279
|
+
start: new CalendarDate(2025, 10, 20),
|
|
280
|
+
end: new CalendarDate(2025, 10, 25),
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
expect(result).toBe('20 Oct - 25 Oct, 2025');
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it('should return formatted range for different year range', () => {
|
|
287
|
+
const result = formatDateRange({
|
|
288
|
+
start: new CalendarDate(2024, 12, 20),
|
|
289
|
+
end: new CalendarDate(2025, 1, 5),
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
expect(result).toBe('20 Dec, 2024 - 5 Jan, 2025');
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe('getMatchingPreset', () => {
|
|
297
|
+
it('should return null if the end date is not provided', () => {
|
|
298
|
+
const result = getMatchingPreset({ start: new CalendarDate(2025, 10, 20) });
|
|
299
|
+
|
|
300
|
+
expect(result).toBeNull();
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it('should return null if the start date is not provided', () => {
|
|
304
|
+
const result = getMatchingPreset({ end: new CalendarDate(2025, 10, 20) });
|
|
305
|
+
|
|
306
|
+
expect(result).toBeNull();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('should return null if the end date is not today', () => {
|
|
310
|
+
const result = getMatchingPreset({
|
|
311
|
+
start: new CalendarDate(2025, 10, 20),
|
|
312
|
+
end: new CalendarDate(2025, 10, 21),
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
expect(result).toBeNull();
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it('should return null for custom range', () => {
|
|
319
|
+
const result = getMatchingPreset({
|
|
320
|
+
start: new CalendarDate(2025, 10, 20),
|
|
321
|
+
end: today(getLocalTimeZone()),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(result).toBeNull();
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test.each(Object.entries(timeRangeMappings))(
|
|
328
|
+
'should return the %s" preset for for %d days',
|
|
329
|
+
(key, days) => {
|
|
330
|
+
const end = today(getLocalTimeZone());
|
|
331
|
+
const start = end.subtract({ days });
|
|
332
|
+
|
|
333
|
+
const result = getMatchingPreset({ start, end });
|
|
334
|
+
|
|
335
|
+
expect(result).toBe(key);
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
});
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import type { DateValue } from '@internationalized/date';
|
|
2
|
+
import { getLocalTimeZone, isToday, now, toCalendarDateTime, today } from '@internationalized/date';
|
|
3
|
+
import type { InsightsDateRange, InsightsSummary, InsightsSummaryType } from '@n8n/api-types';
|
|
4
|
+
import type { DateRange } from '@n8n/design-system';
|
|
5
|
+
import { useI18n } from '@n8n/i18n';
|
|
6
|
+
import dateformat from 'dateformat';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
INSIGHTS_DEVIATION_UNIT_MAPPING,
|
|
10
|
+
INSIGHTS_SUMMARY_ORDER,
|
|
11
|
+
INSIGHTS_UNIT_MAPPING,
|
|
12
|
+
} from './insights.constants';
|
|
13
|
+
import type { InsightsSummaryDisplay } from './insights.types';
|
|
14
|
+
|
|
15
|
+
const DATE_FORMAT_DAY_MONTH_YEAR = 'd mmm, yyyy';
|
|
16
|
+
const DATE_FORMAT_DAY_MONTH = 'd mmm';
|
|
17
|
+
|
|
18
|
+
export const transformInsightsTimeSaved = (minutes: number): number =>
|
|
19
|
+
Math.round(minutes / (Math.abs(minutes) < 60 ? 1 : 60)); // we want to show saved time in minutes or hours
|
|
20
|
+
export const transformInsightsAverageRunTime = (ms: number): number => ms / 1000; // we want to show average run time in seconds
|
|
21
|
+
export const transformInsightsFailureRate = (value: number): number => value * 100; // we want to show failure rate in percentage
|
|
22
|
+
|
|
23
|
+
export const transformInsightsValues: Record<InsightsSummaryType, (value: number) => number> = {
|
|
24
|
+
total: (value: number) => value,
|
|
25
|
+
failed: (value: number) => value,
|
|
26
|
+
timeSaved: transformInsightsTimeSaved,
|
|
27
|
+
averageRunTime: transformInsightsAverageRunTime,
|
|
28
|
+
failureRate: transformInsightsFailureRate,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const getPreviousValue = (value: number, deviation: number): number => value - deviation;
|
|
32
|
+
|
|
33
|
+
const getDeviation = (value: number, deviation: number): number | null => {
|
|
34
|
+
if (value === 0 && deviation === 0) return 0;
|
|
35
|
+
|
|
36
|
+
const previousValue = getPreviousValue(value, deviation);
|
|
37
|
+
if (previousValue === 0) return null; // avoid division by zero
|
|
38
|
+
|
|
39
|
+
return (deviation / previousValue) * 100;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const transformInsightsDeviation: Record<
|
|
43
|
+
InsightsSummaryType,
|
|
44
|
+
(value: number, deviation: number) => number | null
|
|
45
|
+
> = {
|
|
46
|
+
total: getDeviation,
|
|
47
|
+
failed: getDeviation,
|
|
48
|
+
timeSaved: (_: number, deviation: number) => transformInsightsTimeSaved(deviation),
|
|
49
|
+
averageRunTime: (_: number, deviation: number) => transformInsightsAverageRunTime(deviation),
|
|
50
|
+
failureRate: (_: number, deviation: number) => transformInsightsFailureRate(deviation),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const transformInsightsSummary = (data: InsightsSummary | null): InsightsSummaryDisplay =>
|
|
54
|
+
data
|
|
55
|
+
? INSIGHTS_SUMMARY_ORDER.map((key) => ({
|
|
56
|
+
id: key,
|
|
57
|
+
value: transformInsightsValues[key](data[key].value),
|
|
58
|
+
deviation:
|
|
59
|
+
data[key].deviation === null
|
|
60
|
+
? null
|
|
61
|
+
: transformInsightsDeviation[key](data[key].value, data[key].deviation),
|
|
62
|
+
deviationUnit:
|
|
63
|
+
data[key].deviation === null
|
|
64
|
+
? ''
|
|
65
|
+
: INSIGHTS_DEVIATION_UNIT_MAPPING[key](data[key].deviation),
|
|
66
|
+
unit: INSIGHTS_UNIT_MAPPING[key](data[key].value),
|
|
67
|
+
}))
|
|
68
|
+
: [];
|
|
69
|
+
|
|
70
|
+
export const timeRangeMappings: Record<InsightsDateRange['key'], number> = {
|
|
71
|
+
day: 1,
|
|
72
|
+
week: 7,
|
|
73
|
+
'2weeks': 14,
|
|
74
|
+
month: 30,
|
|
75
|
+
quarter: 90,
|
|
76
|
+
'6months': 180,
|
|
77
|
+
year: 365,
|
|
78
|
+
} as const;
|
|
79
|
+
|
|
80
|
+
export const getTimeRangeLabels = () => {
|
|
81
|
+
const i18n = useI18n();
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
day: i18n.baseText('insights.lastNHours', { interpolate: { count: 24 } }),
|
|
85
|
+
week: i18n.baseText('insights.lastNDays', { interpolate: { count: 7 } }),
|
|
86
|
+
'2weeks': i18n.baseText('insights.lastNDays', { interpolate: { count: 14 } }),
|
|
87
|
+
month: i18n.baseText('insights.lastNDays', { interpolate: { count: 30 } }),
|
|
88
|
+
quarter: i18n.baseText('insights.lastNDays', { interpolate: { count: 90 } }),
|
|
89
|
+
'6months': i18n.baseText('insights.months', { interpolate: { count: 6 } }),
|
|
90
|
+
year: i18n.baseText('insights.oneYear'),
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @returns A human readable string representing the date range e.g '01 Jan - 05 Jan 2025'
|
|
96
|
+
*/
|
|
97
|
+
export const formatDateRange = (range: { start?: DateValue; end?: DateValue }): string => {
|
|
98
|
+
const { start, end } = range;
|
|
99
|
+
if (!start) return '';
|
|
100
|
+
|
|
101
|
+
const startDate = start.toDate(getLocalTimeZone());
|
|
102
|
+
const endDate = end?.toDate(getLocalTimeZone());
|
|
103
|
+
|
|
104
|
+
if (!end || start.compare(end) === 0) {
|
|
105
|
+
return dateformat(startDate, DATE_FORMAT_DAY_MONTH_YEAR);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (start.year === end.year) {
|
|
109
|
+
return `${dateformat(startDate, DATE_FORMAT_DAY_MONTH)} - ${dateformat(endDate, DATE_FORMAT_DAY_MONTH_YEAR)}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `${dateformat(startDate, DATE_FORMAT_DAY_MONTH_YEAR)} - ${dateformat(endDate, DATE_FORMAT_DAY_MONTH_YEAR)}`;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @returns The matching preset key if the range matches a preset, null for custom ranges
|
|
117
|
+
*/
|
|
118
|
+
export const getMatchingPreset = (range: { start?: DateValue; end?: DateValue }):
|
|
119
|
+
| InsightsDateRange['key']
|
|
120
|
+
| null => {
|
|
121
|
+
const { start, end } = range;
|
|
122
|
+
if (!start || !end || !isToday(end, getLocalTimeZone())) return null;
|
|
123
|
+
|
|
124
|
+
const daysDiff = end.compare(start);
|
|
125
|
+
|
|
126
|
+
for (const [key, days] of Object.entries(timeRangeMappings)) {
|
|
127
|
+
if (daysDiff === days) return key as InsightsDateRange['key'];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Converts DateValue range to adjusted Date objects for API calls
|
|
135
|
+
* - For single-day ranges ending today: injects current time into both dates (exact 24 hours)
|
|
136
|
+
* - For multi-day ranges ending today: injects current time into end date only
|
|
137
|
+
* - For past dates: uses end-of-day for end date
|
|
138
|
+
*
|
|
139
|
+
* @param dateRange - The date range to adjust
|
|
140
|
+
*/
|
|
141
|
+
export const getAdjustedDateRange = (dateRange: DateRange): { startDate: Date; endDate: Date } => {
|
|
142
|
+
if (!dateRange.start || !dateRange.end) return { startDate: new Date(), endDate: new Date() };
|
|
143
|
+
|
|
144
|
+
const timezone = getLocalTimeZone();
|
|
145
|
+
|
|
146
|
+
const todayInTimezone = today(timezone);
|
|
147
|
+
const isEndDateToday = dateRange.end && dateRange.end.compare(todayInTimezone) === 0;
|
|
148
|
+
const daysDiff = dateRange.end && dateRange.start ? dateRange.end.compare(dateRange.start) : 0;
|
|
149
|
+
|
|
150
|
+
if (isEndDateToday) {
|
|
151
|
+
const nowInTimezone = now(timezone);
|
|
152
|
+
|
|
153
|
+
if (daysDiff === 1) {
|
|
154
|
+
// For single-day ranges (e.g., "Last 24 hours"), inject current time into both dates
|
|
155
|
+
// This gives exact 24-hour ranges
|
|
156
|
+
const startDate = toCalendarDateTime(dateRange.start, nowInTimezone).toDate(timezone);
|
|
157
|
+
const endDate = toCalendarDateTime(dateRange.end, nowInTimezone).toDate(timezone);
|
|
158
|
+
return { startDate, endDate };
|
|
159
|
+
} else {
|
|
160
|
+
// For multi-day ranges, use start-of-day for start but current time for end
|
|
161
|
+
const startDate = dateRange.start?.toDate(timezone) ?? new Date();
|
|
162
|
+
const endDate = toCalendarDateTime(dateRange.end, nowInTimezone).toDate(timezone);
|
|
163
|
+
return { startDate, endDate };
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// For past dates, use beginning of day for start and end of day for end to ensure full day coverage
|
|
167
|
+
const startDate = dateRange.start?.toDate(timezone) ?? new Date();
|
|
168
|
+
const endDate = dateRange.end?.toDate(timezone) ?? new Date();
|
|
169
|
+
endDate.setHours(23, 59, 59, 999);
|
|
170
|
+
return { startDate, endDate };
|
|
171
|
+
}
|
|
172
|
+
};
|
package/vite.config.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import vue from '@vitejs/plugin-vue';
|
|
2
|
+
import icons from 'unplugin-icons/vite';
|
|
3
|
+
import svgLoader from 'vite-svg-loader';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { defineConfig, mergeConfig } from 'vite';
|
|
7
|
+
import { frontendAliases } from '@n8n/frontend-vite-config';
|
|
8
|
+
import { vitestConfig } from '@n8n/vitest-config/frontend';
|
|
9
|
+
|
|
10
|
+
const packageDir = fileURLToPath(new URL('.', import.meta.url));
|
|
11
|
+
const packagesDir = resolve(packageDir, '..', '..', '..');
|
|
12
|
+
|
|
13
|
+
export default mergeConfig(
|
|
14
|
+
defineConfig({
|
|
15
|
+
// What lets a test render a design-system component. Its icon set imports
|
|
16
|
+
// `~icons/lucide/*` (unplugin-icons) and `./custom/*.svg` (svgLoader); without
|
|
17
|
+
// either, an icon resolves to a URL string and rendering throws. The shared
|
|
18
|
+
// vitest config stubs only the `nodes/*.svg` subset.
|
|
19
|
+
plugins: [vue(), icons({ compiler: 'vue3' }), svgLoader()],
|
|
20
|
+
resolve: {
|
|
21
|
+
// The same platform mapping the editor-ui dev server uses, so a test resolves
|
|
22
|
+
// `@n8n/stores/...` from source rather than from a stale `dist` — the two disagreeing is
|
|
23
|
+
// what put 1,111 specifiers on the wrong side of the src/dist line. Sibling modules are
|
|
24
|
+
// deliberately absent: nothing here should make a cross-module import resolve.
|
|
25
|
+
alias: frontendAliases(packagesDir),
|
|
26
|
+
},
|
|
27
|
+
}),
|
|
28
|
+
vitestConfig,
|
|
29
|
+
);
|