@checkstack/ui 0.5.0 → 0.5.2
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/CHANGELOG.md +47 -0
- package/package.json +6 -6
- package/src/components/DateRangeFilter.tsx +81 -32
- package/src/components/DateTimePicker.tsx +14 -5
- package/bunfig.toml +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 0.5.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 0b9fc58: Fix workspace:\* protocol resolution in published packages
|
|
8
|
+
|
|
9
|
+
Published packages now correctly have resolved dependency versions instead of `workspace:*` references. This is achieved by using `bun publish` which properly resolves workspace protocol references.
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [0b9fc58]
|
|
12
|
+
- @checkstack/common@0.6.1
|
|
13
|
+
- @checkstack/frontend-api@0.3.4
|
|
14
|
+
|
|
15
|
+
## 0.5.1
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 090143b: ### Health Check Aggregation & UI Fixes
|
|
20
|
+
|
|
21
|
+
**Backend (`healthcheck-backend`):**
|
|
22
|
+
|
|
23
|
+
- Fixed tail-end bucket truncation where the last aggregated bucket was cut off at the interval boundary instead of extending to the query end date
|
|
24
|
+
- Added `rangeEnd` parameter to `reaggregateBuckets()` to properly extend the last bucket
|
|
25
|
+
- Fixed cross-tier merge logic (`mergeTieredBuckets`) to prevent hourly aggregates from blocking fresh raw data
|
|
26
|
+
|
|
27
|
+
**Schema (`healthcheck-common`):**
|
|
28
|
+
|
|
29
|
+
- Added `bucketEnd` field to `AggregatedBucketBaseSchema` so frontends know the actual end time of each bucket
|
|
30
|
+
|
|
31
|
+
**Frontend (`healthcheck-frontend`):**
|
|
32
|
+
|
|
33
|
+
- Updated all components to use `bucket.bucketEnd` instead of calculating from `bucketIntervalSeconds`
|
|
34
|
+
- Fixed aggregation mode detection: changed `>` to `>=` so 7-day queries use aggregated data when `rawRetentionDays` is 7
|
|
35
|
+
- Added ref-based memoization in `useHealthCheckData` to prevent layout shift during signal-triggered refetches
|
|
36
|
+
- Exposed `isFetching` state to show loading spinner during background refetches
|
|
37
|
+
- Added debounced custom date range with Apply button to prevent fetching on every field change
|
|
38
|
+
- Added validation preventing start date >= end date in custom ranges
|
|
39
|
+
- Added sparkline downsampling: when there are 60+ data points, they are aggregated into buckets with informative tooltips
|
|
40
|
+
|
|
41
|
+
**UI (`ui`):**
|
|
42
|
+
|
|
43
|
+
- Fixed `DateRangeFilter` presets to use true sliding windows (removed `startOfDay` from 7-day and 30-day ranges)
|
|
44
|
+
- Added `disabled` prop to `DateRangeFilter` and `DateTimePicker` components
|
|
45
|
+
- Added `onCustomChange` prop to `DateRangeFilter` for debounced custom date handling
|
|
46
|
+
- Improved layout: custom date pickers now inline with preset buttons on desktop
|
|
47
|
+
- Added responsive mobile layout: date pickers stack vertically with down arrow
|
|
48
|
+
- Added validation error display for invalid date ranges
|
|
49
|
+
|
|
3
50
|
## 0.5.0
|
|
4
51
|
|
|
5
52
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@checkstack/ui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"dependencies": {
|
|
7
|
-
"@checkstack/common": "
|
|
8
|
-
"@checkstack/frontend-api": "
|
|
7
|
+
"@checkstack/common": "0.6.0",
|
|
8
|
+
"@checkstack/frontend-api": "0.3.3",
|
|
9
9
|
"@codemirror/autocomplete": "^6.20.0",
|
|
10
10
|
"@codemirror/lang-json": "^6.0.2",
|
|
11
11
|
"@codemirror/lang-markdown": "^6.5.0",
|
|
@@ -36,9 +36,9 @@
|
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"typescript": "^5.0.0",
|
|
38
38
|
"@types/react": "^18.2.0",
|
|
39
|
-
"@checkstack/test-utils-frontend": "
|
|
40
|
-
"@checkstack/tsconfig": "
|
|
41
|
-
"@checkstack/scripts": "
|
|
39
|
+
"@checkstack/test-utils-frontend": "0.0.2",
|
|
40
|
+
"@checkstack/tsconfig": "0.0.2",
|
|
41
|
+
"@checkstack/scripts": "0.1.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"typecheck": "tsc --noEmit",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useState, useMemo } from "react";
|
|
2
|
-
import { subDays, subHours
|
|
2
|
+
import { subDays, subHours } from "date-fns";
|
|
3
3
|
import { DateTimePicker } from "./DateTimePicker";
|
|
4
4
|
import { Button } from "./Button";
|
|
5
5
|
import { Calendar } from "lucide-react";
|
|
@@ -18,7 +18,12 @@ export enum DateRangePreset {
|
|
|
18
18
|
|
|
19
19
|
export interface DateRangeFilterProps {
|
|
20
20
|
value: DateRange;
|
|
21
|
+
/** Called when a preset is clicked (immediate) */
|
|
21
22
|
onChange: (range: DateRange) => void;
|
|
23
|
+
/** Optional: Called when custom date picker values change (for debounced Apply pattern) */
|
|
24
|
+
onCustomChange?: (range: DateRange) => void;
|
|
25
|
+
/** Disable all interactions (buttons and date pickers) */
|
|
26
|
+
disabled?: boolean;
|
|
22
27
|
className?: string;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -40,13 +45,13 @@ export function getPresetRange(preset: DateRangePreset): DateRange {
|
|
|
40
45
|
return { startDate: subHours(now, 24), endDate: now };
|
|
41
46
|
}
|
|
42
47
|
case DateRangePreset.Last7Days: {
|
|
43
|
-
return { startDate:
|
|
48
|
+
return { startDate: subDays(now, 7), endDate: now };
|
|
44
49
|
}
|
|
45
50
|
case DateRangePreset.Last30Days: {
|
|
46
|
-
return { startDate:
|
|
51
|
+
return { startDate: subDays(now, 30), endDate: now };
|
|
47
52
|
}
|
|
48
53
|
case DateRangePreset.Custom: {
|
|
49
|
-
return { startDate:
|
|
54
|
+
return { startDate: subDays(now, 7), endDate: now };
|
|
50
55
|
}
|
|
51
56
|
}
|
|
52
57
|
}
|
|
@@ -69,6 +74,8 @@ function detectPreset(range: DateRange): DateRangePreset {
|
|
|
69
74
|
export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
70
75
|
value,
|
|
71
76
|
onChange,
|
|
77
|
+
onCustomChange,
|
|
78
|
+
disabled = false,
|
|
72
79
|
className,
|
|
73
80
|
}) => {
|
|
74
81
|
const activePreset = useMemo(() => detectPreset(value), [value]);
|
|
@@ -77,6 +84,7 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
77
84
|
);
|
|
78
85
|
|
|
79
86
|
const handlePresetClick = (preset: DateRangePreset) => {
|
|
87
|
+
if (disabled) return;
|
|
80
88
|
if (preset === DateRangePreset.Custom) {
|
|
81
89
|
setShowCustom(true);
|
|
82
90
|
} else {
|
|
@@ -85,11 +93,17 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
85
93
|
}
|
|
86
94
|
};
|
|
87
95
|
|
|
96
|
+
// Use onCustomChange if provided, otherwise fall back to onChange
|
|
97
|
+
const handleCustomDateChange = onCustomChange ?? onChange;
|
|
98
|
+
|
|
99
|
+
// Validate date range
|
|
100
|
+
const isInvalidRange = showCustom && value.startDate >= value.endDate;
|
|
101
|
+
|
|
88
102
|
return (
|
|
89
|
-
<div className={
|
|
103
|
+
<div className={className}>
|
|
90
104
|
<div className="flex items-center gap-2 flex-wrap">
|
|
91
|
-
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
92
|
-
<span className="text-sm font-medium text-muted-foreground">
|
|
105
|
+
<Calendar className="h-4 w-4 text-muted-foreground shrink-0" />
|
|
106
|
+
<span className="text-sm font-medium text-muted-foreground shrink-0">
|
|
93
107
|
Time range:
|
|
94
108
|
</span>
|
|
95
109
|
<div className="flex gap-1 flex-wrap">
|
|
@@ -105,38 +119,73 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
105
119
|
}
|
|
106
120
|
size="sm"
|
|
107
121
|
onClick={() => handlePresetClick(preset.id)}
|
|
122
|
+
disabled={disabled}
|
|
108
123
|
>
|
|
109
124
|
<span className="sm:hidden">{preset.shortLabel}</span>
|
|
110
125
|
<span className="hidden sm:inline">{preset.label}</span>
|
|
111
126
|
</Button>
|
|
112
127
|
))}
|
|
113
128
|
</div>
|
|
129
|
+
{showCustom && (
|
|
130
|
+
<>
|
|
131
|
+
<div className="w-px h-5 bg-border hidden sm:block" />
|
|
132
|
+
{/* Desktop: inline with right arrow */}
|
|
133
|
+
<div className="hidden sm:flex items-center gap-2">
|
|
134
|
+
<DateTimePicker
|
|
135
|
+
value={value.startDate}
|
|
136
|
+
onChange={(startDate) => {
|
|
137
|
+
if (startDate && !disabled) {
|
|
138
|
+
handleCustomDateChange({ ...value, startDate });
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
maxDate={value.endDate}
|
|
142
|
+
disabled={disabled}
|
|
143
|
+
/>
|
|
144
|
+
<span className="text-sm text-muted-foreground">→</span>
|
|
145
|
+
<DateTimePicker
|
|
146
|
+
value={value.endDate}
|
|
147
|
+
onChange={(endDate) => {
|
|
148
|
+
if (endDate && !disabled) {
|
|
149
|
+
handleCustomDateChange({ ...value, endDate });
|
|
150
|
+
}
|
|
151
|
+
}}
|
|
152
|
+
minDate={value.startDate}
|
|
153
|
+
maxDate={new Date()}
|
|
154
|
+
disabled={disabled}
|
|
155
|
+
/>
|
|
156
|
+
</div>
|
|
157
|
+
{/* Mobile: stacked vertically with down arrow, centered */}
|
|
158
|
+
<div className="flex sm:hidden flex-col items-center gap-1 w-full">
|
|
159
|
+
<DateTimePicker
|
|
160
|
+
value={value.startDate}
|
|
161
|
+
onChange={(startDate) => {
|
|
162
|
+
if (startDate && !disabled) {
|
|
163
|
+
handleCustomDateChange({ ...value, startDate });
|
|
164
|
+
}
|
|
165
|
+
}}
|
|
166
|
+
maxDate={value.endDate}
|
|
167
|
+
disabled={disabled}
|
|
168
|
+
/>
|
|
169
|
+
<span className="text-sm text-muted-foreground">↓</span>
|
|
170
|
+
<DateTimePicker
|
|
171
|
+
value={value.endDate}
|
|
172
|
+
onChange={(endDate) => {
|
|
173
|
+
if (endDate && !disabled) {
|
|
174
|
+
handleCustomDateChange({ ...value, endDate });
|
|
175
|
+
}
|
|
176
|
+
}}
|
|
177
|
+
minDate={value.startDate}
|
|
178
|
+
maxDate={new Date()}
|
|
179
|
+
disabled={disabled}
|
|
180
|
+
/>
|
|
181
|
+
</div>
|
|
182
|
+
</>
|
|
183
|
+
)}
|
|
114
184
|
</div>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<DateTimePicker
|
|
120
|
-
value={value.startDate}
|
|
121
|
-
onChange={(startDate) => {
|
|
122
|
-
if (startDate) {
|
|
123
|
-
onChange({ ...value, startDate });
|
|
124
|
-
}
|
|
125
|
-
}}
|
|
126
|
-
maxDate={value.endDate}
|
|
127
|
-
/>
|
|
128
|
-
<span className="text-sm text-muted-foreground">To:</span>
|
|
129
|
-
<DateTimePicker
|
|
130
|
-
value={value.endDate}
|
|
131
|
-
onChange={(endDate) => {
|
|
132
|
-
if (endDate) {
|
|
133
|
-
onChange({ ...value, endDate });
|
|
134
|
-
}
|
|
135
|
-
}}
|
|
136
|
-
minDate={value.startDate}
|
|
137
|
-
maxDate={new Date()}
|
|
138
|
-
/>
|
|
139
|
-
</div>
|
|
185
|
+
{isInvalidRange && (
|
|
186
|
+
<p className="text-sm text-destructive mt-2">
|
|
187
|
+
Start date must be before end date
|
|
188
|
+
</p>
|
|
140
189
|
)}
|
|
141
190
|
</div>
|
|
142
191
|
);
|
|
@@ -10,6 +10,8 @@ export interface DateTimePickerProps {
|
|
|
10
10
|
onChange: (date: Date | undefined) => void;
|
|
11
11
|
minDate?: Date;
|
|
12
12
|
maxDate?: Date;
|
|
13
|
+
/** Disable all interactions */
|
|
14
|
+
disabled?: boolean;
|
|
13
15
|
className?: string;
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -37,6 +39,7 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
37
39
|
onChange,
|
|
38
40
|
minDate,
|
|
39
41
|
maxDate,
|
|
42
|
+
disabled = false,
|
|
40
43
|
className,
|
|
41
44
|
}) => {
|
|
42
45
|
const isValidDate = value instanceof Date && !Number.isNaN(value.getTime());
|
|
@@ -258,6 +261,7 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
258
261
|
size="icon"
|
|
259
262
|
className="h-9 w-9 rounded-none border-r"
|
|
260
263
|
type="button"
|
|
264
|
+
disabled={disabled}
|
|
261
265
|
>
|
|
262
266
|
<Calendar className="h-4 w-4" />
|
|
263
267
|
</Button>
|
|
@@ -271,8 +275,9 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
271
275
|
onChange={(e) => handleFieldChange("day", e.target.value)}
|
|
272
276
|
onBlur={() => handleFieldBlur("day")}
|
|
273
277
|
placeholder="DD"
|
|
274
|
-
className="w-7 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
278
|
+
className="w-7 text-center bg-transparent border-none outline-none text-sm font-mono disabled:opacity-50"
|
|
275
279
|
maxLength={2}
|
|
280
|
+
disabled={disabled}
|
|
276
281
|
/>
|
|
277
282
|
<span className="text-muted-foreground">/</span>
|
|
278
283
|
<input
|
|
@@ -282,8 +287,9 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
282
287
|
onChange={(e) => handleFieldChange("month", e.target.value)}
|
|
283
288
|
onBlur={() => handleFieldBlur("month")}
|
|
284
289
|
placeholder="MM"
|
|
285
|
-
className="w-7 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
290
|
+
className="w-7 text-center bg-transparent border-none outline-none text-sm font-mono disabled:opacity-50"
|
|
286
291
|
maxLength={2}
|
|
292
|
+
disabled={disabled}
|
|
287
293
|
/>
|
|
288
294
|
<span className="text-muted-foreground">/</span>
|
|
289
295
|
<input
|
|
@@ -293,8 +299,9 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
293
299
|
onChange={(e) => handleFieldChange("year", e.target.value)}
|
|
294
300
|
onBlur={() => handleFieldBlur("year")}
|
|
295
301
|
placeholder="YYYY"
|
|
296
|
-
className="w-11 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
302
|
+
className="w-11 text-center bg-transparent border-none outline-none text-sm font-mono disabled:opacity-50"
|
|
297
303
|
maxLength={4}
|
|
304
|
+
disabled={disabled}
|
|
298
305
|
/>
|
|
299
306
|
</div>
|
|
300
307
|
|
|
@@ -337,8 +344,9 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
337
344
|
onChange={(e) => handleFieldChange("hour", e.target.value)}
|
|
338
345
|
onBlur={() => handleFieldBlur("hour")}
|
|
339
346
|
placeholder="HH"
|
|
340
|
-
className="w-6 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
347
|
+
className="w-6 text-center bg-transparent border-none outline-none text-sm font-mono disabled:opacity-50"
|
|
341
348
|
maxLength={2}
|
|
349
|
+
disabled={disabled}
|
|
342
350
|
/>
|
|
343
351
|
<span className="text-muted-foreground">:</span>
|
|
344
352
|
<input
|
|
@@ -348,8 +356,9 @@ export const DateTimePicker: React.FC<DateTimePickerProps> = ({
|
|
|
348
356
|
onChange={(e) => handleFieldChange("minute", e.target.value)}
|
|
349
357
|
onBlur={() => handleFieldBlur("minute")}
|
|
350
358
|
placeholder="MM"
|
|
351
|
-
className="w-6 text-center bg-transparent border-none outline-none text-sm font-mono"
|
|
359
|
+
className="w-6 text-center bg-transparent border-none outline-none text-sm font-mono disabled:opacity-50"
|
|
352
360
|
maxLength={2}
|
|
361
|
+
disabled={disabled}
|
|
353
362
|
/>
|
|
354
363
|
</div>
|
|
355
364
|
</div>
|
package/bunfig.toml
DELETED