@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 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.0",
3
+ "version": "0.5.2",
4
4
  "type": "module",
5
5
  "main": "src/index.ts",
6
6
  "dependencies": {
7
- "@checkstack/common": "workspace:*",
8
- "@checkstack/frontend-api": "workspace:*",
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": "workspace:*",
40
- "@checkstack/tsconfig": "workspace:*",
41
- "@checkstack/scripts": "workspace:*"
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, startOfDay } from "date-fns";
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: startOfDay(subDays(now, 7)), endDate: now };
48
+ return { startDate: subDays(now, 7), endDate: now };
44
49
  }
45
50
  case DateRangePreset.Last30Days: {
46
- return { startDate: startOfDay(subDays(now, 30)), endDate: now };
51
+ return { startDate: subDays(now, 30), endDate: now };
47
52
  }
48
53
  case DateRangePreset.Custom: {
49
- return { startDate: startOfDay(subDays(now, 7)), endDate: now };
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={`space-y-3 ${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
- {showCustom && (
117
- <div className="flex items-center gap-2 flex-wrap">
118
- <span className="text-sm text-muted-foreground">From:</span>
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
@@ -1,2 +0,0 @@
1
- [test]
2
- preload = ["@checkstack/test-utils-frontend/setup"]