@checkstack/ui 0.4.1 → 0.5.1
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,59 @@
|
|
|
1
1
|
# @checkstack/ui
|
|
2
2
|
|
|
3
|
+
## 0.5.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 090143b: ### Health Check Aggregation & UI Fixes
|
|
8
|
+
|
|
9
|
+
**Backend (`healthcheck-backend`):**
|
|
10
|
+
|
|
11
|
+
- 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
|
|
12
|
+
- Added `rangeEnd` parameter to `reaggregateBuckets()` to properly extend the last bucket
|
|
13
|
+
- Fixed cross-tier merge logic (`mergeTieredBuckets`) to prevent hourly aggregates from blocking fresh raw data
|
|
14
|
+
|
|
15
|
+
**Schema (`healthcheck-common`):**
|
|
16
|
+
|
|
17
|
+
- Added `bucketEnd` field to `AggregatedBucketBaseSchema` so frontends know the actual end time of each bucket
|
|
18
|
+
|
|
19
|
+
**Frontend (`healthcheck-frontend`):**
|
|
20
|
+
|
|
21
|
+
- Updated all components to use `bucket.bucketEnd` instead of calculating from `bucketIntervalSeconds`
|
|
22
|
+
- Fixed aggregation mode detection: changed `>` to `>=` so 7-day queries use aggregated data when `rawRetentionDays` is 7
|
|
23
|
+
- Added ref-based memoization in `useHealthCheckData` to prevent layout shift during signal-triggered refetches
|
|
24
|
+
- Exposed `isFetching` state to show loading spinner during background refetches
|
|
25
|
+
- Added debounced custom date range with Apply button to prevent fetching on every field change
|
|
26
|
+
- Added validation preventing start date >= end date in custom ranges
|
|
27
|
+
- Added sparkline downsampling: when there are 60+ data points, they are aggregated into buckets with informative tooltips
|
|
28
|
+
|
|
29
|
+
**UI (`ui`):**
|
|
30
|
+
|
|
31
|
+
- Fixed `DateRangeFilter` presets to use true sliding windows (removed `startOfDay` from 7-day and 30-day ranges)
|
|
32
|
+
- Added `disabled` prop to `DateRangeFilter` and `DateTimePicker` components
|
|
33
|
+
- Added `onCustomChange` prop to `DateRangeFilter` for debounced custom date handling
|
|
34
|
+
- Improved layout: custom date pickers now inline with preset buttons on desktop
|
|
35
|
+
- Added responsive mobile layout: date pickers stack vertically with down arrow
|
|
36
|
+
- Added validation error display for invalid date ranges
|
|
37
|
+
|
|
38
|
+
## 0.5.0
|
|
39
|
+
|
|
40
|
+
### Minor Changes
|
|
41
|
+
|
|
42
|
+
- 223081d: Add icon support to PageLayout and improve mobile responsiveness
|
|
43
|
+
|
|
44
|
+
**PageLayout Icons:**
|
|
45
|
+
|
|
46
|
+
- Added required `icon` prop to `PageLayout` and `PageHeader` components that accepts a Lucide icon component reference
|
|
47
|
+
- Icons are rendered with consistent `h-6 w-6 text-primary` styling
|
|
48
|
+
- Updated all page components to include appropriate icons in their headers
|
|
49
|
+
|
|
50
|
+
**Mobile Layout Improvements:**
|
|
51
|
+
|
|
52
|
+
- Standardized responsive padding in main app shell (`p-3` on mobile, `p-6` on desktop)
|
|
53
|
+
- Added `CardHeaderRow` component for mobile-safe card headers with proper wrapping
|
|
54
|
+
- Improved `DateRangeFilter` responsive behavior with vertical stacking on mobile
|
|
55
|
+
- Migrated pages to use `PageLayout` for consistent responsive behavior
|
|
56
|
+
|
|
3
57
|
## 0.4.1
|
|
4
58
|
|
|
5
59
|
### Patch Changes
|
package/package.json
CHANGED
package/src/components/Card.tsx
CHANGED
|
@@ -8,7 +8,7 @@ export const Card = ({
|
|
|
8
8
|
<div
|
|
9
9
|
className={cn(
|
|
10
10
|
"rounded-lg border border-border bg-card text-card-foreground shadow-sm",
|
|
11
|
-
className
|
|
11
|
+
className,
|
|
12
12
|
)}
|
|
13
13
|
{...props}
|
|
14
14
|
/>
|
|
@@ -21,6 +21,23 @@ export const CardHeader = ({
|
|
|
21
21
|
<div className={cn("flex flex-col space-y-1.5 p-6", className)} {...props} />
|
|
22
22
|
);
|
|
23
23
|
|
|
24
|
+
/**
|
|
25
|
+
* A row layout for card headers with title and actions.
|
|
26
|
+
* Provides mobile-friendly defaults with flex-wrap and gap.
|
|
27
|
+
*/
|
|
28
|
+
export const CardHeaderRow = ({
|
|
29
|
+
className,
|
|
30
|
+
...props
|
|
31
|
+
}: React.HTMLAttributes<HTMLDivElement>) => (
|
|
32
|
+
<div
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex flex-wrap items-center justify-between gap-3",
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
|
|
24
41
|
export const CardTitle = ({
|
|
25
42
|
className,
|
|
26
43
|
...props
|
|
@@ -28,7 +45,7 @@ export const CardTitle = ({
|
|
|
28
45
|
<h3
|
|
29
46
|
className={cn(
|
|
30
47
|
"text-2xl font-semibold leading-none tracking-tight",
|
|
31
|
-
className
|
|
48
|
+
className,
|
|
32
49
|
)}
|
|
33
50
|
{...props}
|
|
34
51
|
/>
|
|
@@ -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,15 +18,24 @@ 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
|
|
|
25
|
-
const PRESETS: Array<{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
const PRESETS: Array<{
|
|
31
|
+
id: DateRangePreset;
|
|
32
|
+
label: string;
|
|
33
|
+
shortLabel: string;
|
|
34
|
+
}> = [
|
|
35
|
+
{ id: DateRangePreset.Last24Hours, label: "Last 24h", shortLabel: "24h" },
|
|
36
|
+
{ id: DateRangePreset.Last7Days, label: "Last 7 days", shortLabel: "7d" },
|
|
37
|
+
{ id: DateRangePreset.Last30Days, label: "Last 30 days", shortLabel: "30d" },
|
|
38
|
+
{ id: DateRangePreset.Custom, label: "Custom", shortLabel: "Custom" },
|
|
30
39
|
];
|
|
31
40
|
|
|
32
41
|
export function getPresetRange(preset: DateRangePreset): DateRange {
|
|
@@ -36,13 +45,13 @@ export function getPresetRange(preset: DateRangePreset): DateRange {
|
|
|
36
45
|
return { startDate: subHours(now, 24), endDate: now };
|
|
37
46
|
}
|
|
38
47
|
case DateRangePreset.Last7Days: {
|
|
39
|
-
return { startDate:
|
|
48
|
+
return { startDate: subDays(now, 7), endDate: now };
|
|
40
49
|
}
|
|
41
50
|
case DateRangePreset.Last30Days: {
|
|
42
|
-
return { startDate:
|
|
51
|
+
return { startDate: subDays(now, 30), endDate: now };
|
|
43
52
|
}
|
|
44
53
|
case DateRangePreset.Custom: {
|
|
45
|
-
return { startDate:
|
|
54
|
+
return { startDate: subDays(now, 7), endDate: now };
|
|
46
55
|
}
|
|
47
56
|
}
|
|
48
57
|
}
|
|
@@ -65,6 +74,8 @@ function detectPreset(range: DateRange): DateRangePreset {
|
|
|
65
74
|
export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
66
75
|
value,
|
|
67
76
|
onChange,
|
|
77
|
+
onCustomChange,
|
|
78
|
+
disabled = false,
|
|
68
79
|
className,
|
|
69
80
|
}) => {
|
|
70
81
|
const activePreset = useMemo(() => detectPreset(value), [value]);
|
|
@@ -73,6 +84,7 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
73
84
|
);
|
|
74
85
|
|
|
75
86
|
const handlePresetClick = (preset: DateRangePreset) => {
|
|
87
|
+
if (disabled) return;
|
|
76
88
|
if (preset === DateRangePreset.Custom) {
|
|
77
89
|
setShowCustom(true);
|
|
78
90
|
} else {
|
|
@@ -81,14 +93,20 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
81
93
|
}
|
|
82
94
|
};
|
|
83
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
|
+
|
|
84
102
|
return (
|
|
85
|
-
<div className={
|
|
103
|
+
<div className={className}>
|
|
86
104
|
<div className="flex items-center gap-2 flex-wrap">
|
|
87
|
-
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
88
|
-
<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">
|
|
89
107
|
Time range:
|
|
90
108
|
</span>
|
|
91
|
-
<div className="flex gap-1">
|
|
109
|
+
<div className="flex gap-1 flex-wrap">
|
|
92
110
|
{PRESETS.map((preset) => (
|
|
93
111
|
<Button
|
|
94
112
|
key={preset.id}
|
|
@@ -101,37 +119,73 @@ export const DateRangeFilter: React.FC<DateRangeFilterProps> = ({
|
|
|
101
119
|
}
|
|
102
120
|
size="sm"
|
|
103
121
|
onClick={() => handlePresetClick(preset.id)}
|
|
122
|
+
disabled={disabled}
|
|
104
123
|
>
|
|
105
|
-
{preset.
|
|
124
|
+
<span className="sm:hidden">{preset.shortLabel}</span>
|
|
125
|
+
<span className="hidden sm:inline">{preset.label}</span>
|
|
106
126
|
</Button>
|
|
107
127
|
))}
|
|
108
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
|
+
)}
|
|
109
184
|
</div>
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
<DateTimePicker
|
|
115
|
-
value={value.startDate}
|
|
116
|
-
onChange={(startDate) => {
|
|
117
|
-
if (startDate) {
|
|
118
|
-
onChange({ ...value, startDate });
|
|
119
|
-
}
|
|
120
|
-
}}
|
|
121
|
-
maxDate={value.endDate}
|
|
122
|
-
/>
|
|
123
|
-
<span className="text-sm text-muted-foreground">To:</span>
|
|
124
|
-
<DateTimePicker
|
|
125
|
-
value={value.endDate}
|
|
126
|
-
onChange={(endDate) => {
|
|
127
|
-
if (endDate) {
|
|
128
|
-
onChange({ ...value, endDate });
|
|
129
|
-
}
|
|
130
|
-
}}
|
|
131
|
-
minDate={value.startDate}
|
|
132
|
-
maxDate={new Date()}
|
|
133
|
-
/>
|
|
134
|
-
</div>
|
|
185
|
+
{isInvalidRange && (
|
|
186
|
+
<p className="text-sm text-destructive mt-2">
|
|
187
|
+
Start date must be before end date
|
|
188
|
+
</p>
|
|
135
189
|
)}
|
|
136
190
|
</div>
|
|
137
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/src/components/Page.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
3
|
import { cn } from "../utils";
|
|
3
4
|
|
|
4
5
|
interface PageProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -14,33 +15,37 @@ export const Page = React.forwardRef<HTMLDivElement, PageProps>(
|
|
|
14
15
|
>
|
|
15
16
|
{children}
|
|
16
17
|
</div>
|
|
17
|
-
)
|
|
18
|
+
),
|
|
18
19
|
);
|
|
19
20
|
Page.displayName = "Page";
|
|
20
21
|
|
|
21
22
|
interface PageHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
22
23
|
title: string;
|
|
23
24
|
subtitle?: string;
|
|
25
|
+
icon: LucideIcon;
|
|
24
26
|
actions?: React.ReactNode;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
export const PageHeader = React.forwardRef<HTMLDivElement, PageHeaderProps>(
|
|
28
|
-
({ className, title, subtitle, actions, ...props }, ref) => (
|
|
30
|
+
({ className, title, subtitle, icon: Icon, actions, ...props }, ref) => (
|
|
29
31
|
<div
|
|
30
32
|
ref={ref}
|
|
31
33
|
className={cn(
|
|
32
|
-
"flex flex-col md:flex-row items-center justify-between
|
|
33
|
-
className
|
|
34
|
+
"flex flex-col md:flex-row items-center justify-between py-3 pb-2 md:py-6 md:pb-2",
|
|
35
|
+
className,
|
|
34
36
|
)}
|
|
35
37
|
{...props}
|
|
36
38
|
>
|
|
37
39
|
<div className="space-y-1">
|
|
38
|
-
<
|
|
40
|
+
<div className="flex items-center gap-3">
|
|
41
|
+
<Icon className="h-6 w-6 text-primary" />
|
|
42
|
+
<h2 className="text-2xl font-bold tracking-tight">{title}</h2>
|
|
43
|
+
</div>
|
|
39
44
|
{subtitle && <p className="text-muted-foreground">{subtitle}</p>}
|
|
40
45
|
</div>
|
|
41
46
|
{actions && <div className="flex items-center space-x-2">{actions}</div>}
|
|
42
47
|
</div>
|
|
43
|
-
)
|
|
48
|
+
),
|
|
44
49
|
);
|
|
45
50
|
PageHeader.displayName = "PageHeader";
|
|
46
51
|
|
|
@@ -50,9 +55,13 @@ interface PageContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
|
50
55
|
|
|
51
56
|
export const PageContent = React.forwardRef<HTMLDivElement, PageContentProps>(
|
|
52
57
|
({ className, children, ...props }, ref) => (
|
|
53
|
-
<div
|
|
58
|
+
<div
|
|
59
|
+
ref={ref}
|
|
60
|
+
className={cn("flex-1 py-3 pt-2 md:py-6 md:pt-2", className)}
|
|
61
|
+
{...props}
|
|
62
|
+
>
|
|
54
63
|
{children}
|
|
55
64
|
</div>
|
|
56
|
-
)
|
|
65
|
+
),
|
|
57
66
|
);
|
|
58
67
|
PageContent.displayName = "PageContent";
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { LucideIcon } from "lucide-react";
|
|
2
3
|
import {
|
|
3
4
|
Page,
|
|
4
5
|
PageHeader,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
interface PageLayoutProps {
|
|
11
12
|
title: string;
|
|
12
13
|
subtitle?: string;
|
|
14
|
+
icon: LucideIcon;
|
|
13
15
|
actions?: React.ReactNode;
|
|
14
16
|
loading?: boolean;
|
|
15
17
|
allowed?: boolean;
|
|
@@ -31,6 +33,7 @@ interface PageLayoutProps {
|
|
|
31
33
|
export const PageLayout: React.FC<PageLayoutProps> = ({
|
|
32
34
|
title,
|
|
33
35
|
subtitle,
|
|
36
|
+
icon,
|
|
34
37
|
actions,
|
|
35
38
|
loading,
|
|
36
39
|
allowed,
|
|
@@ -46,7 +49,12 @@ export const PageLayout: React.FC<PageLayoutProps> = ({
|
|
|
46
49
|
if (isLoading) {
|
|
47
50
|
return (
|
|
48
51
|
<Page>
|
|
49
|
-
<PageHeader
|
|
52
|
+
<PageHeader
|
|
53
|
+
title={title}
|
|
54
|
+
subtitle={subtitle}
|
|
55
|
+
icon={icon}
|
|
56
|
+
actions={actions}
|
|
57
|
+
/>
|
|
50
58
|
<PageContent>
|
|
51
59
|
<div className="flex justify-center py-12">
|
|
52
60
|
<LoadingSpinner />
|
|
@@ -60,7 +68,12 @@ export const PageLayout: React.FC<PageLayoutProps> = ({
|
|
|
60
68
|
if (allowed === false) {
|
|
61
69
|
return (
|
|
62
70
|
<Page>
|
|
63
|
-
<PageHeader
|
|
71
|
+
<PageHeader
|
|
72
|
+
title={title}
|
|
73
|
+
subtitle={subtitle}
|
|
74
|
+
icon={icon}
|
|
75
|
+
actions={actions}
|
|
76
|
+
/>
|
|
64
77
|
<PageContent>
|
|
65
78
|
<AccessDenied />
|
|
66
79
|
</PageContent>
|
|
@@ -70,10 +83,17 @@ export const PageLayout: React.FC<PageLayoutProps> = ({
|
|
|
70
83
|
|
|
71
84
|
return (
|
|
72
85
|
<Page>
|
|
73
|
-
<PageHeader
|
|
86
|
+
<PageHeader
|
|
87
|
+
title={title}
|
|
88
|
+
subtitle={subtitle}
|
|
89
|
+
icon={icon}
|
|
90
|
+
actions={actions}
|
|
91
|
+
/>
|
|
74
92
|
<PageContent>
|
|
75
93
|
<div
|
|
76
|
-
className={
|
|
94
|
+
className={
|
|
95
|
+
maxWidth === "full" ? "space-y-6" : `max-w-${maxWidth} space-y-6`
|
|
96
|
+
}
|
|
77
97
|
>
|
|
78
98
|
{children}
|
|
79
99
|
</div>
|