@checkstack/healthcheck-frontend 0.5.0 → 0.6.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/CHANGELOG.md CHANGED
@@ -1,5 +1,35 @@
1
1
  # @checkstack/healthcheck-frontend
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 11d2679: Add ability to pause health check configurations globally. When paused, health checks continue to be scheduled but execution is skipped for all systems using that configuration. Users with manage access can pause/resume from the Health Checks config page.
8
+
9
+ ### Patch Changes
10
+
11
+ - 223081d: Add icon support to PageLayout and improve mobile responsiveness
12
+
13
+ **PageLayout Icons:**
14
+
15
+ - Added required `icon` prop to `PageLayout` and `PageHeader` components that accepts a Lucide icon component reference
16
+ - Icons are rendered with consistent `h-6 w-6 text-primary` styling
17
+ - Updated all page components to include appropriate icons in their headers
18
+
19
+ **Mobile Layout Improvements:**
20
+
21
+ - Standardized responsive padding in main app shell (`p-3` on mobile, `p-6` on desktop)
22
+ - Added `CardHeaderRow` component for mobile-safe card headers with proper wrapping
23
+ - Improved `DateRangeFilter` responsive behavior with vertical stacking on mobile
24
+ - Migrated pages to use `PageLayout` for consistent responsive behavior
25
+
26
+ - Updated dependencies [11d2679]
27
+ - Updated dependencies [223081d]
28
+ - @checkstack/healthcheck-common@0.6.0
29
+ - @checkstack/ui@0.5.0
30
+ - @checkstack/auth-frontend@0.5.5
31
+ - @checkstack/dashboard-frontend@0.3.10
32
+
3
33
  ## 0.5.0
4
34
 
5
35
  ### Minor Changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkstack/healthcheck-frontend",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "main": "src/index.tsx",
6
6
  "scripts": {
@@ -7,6 +7,7 @@ import {
7
7
  Card,
8
8
  CardContent,
9
9
  CardHeader,
10
+ CardHeaderRow,
10
11
  CardTitle,
11
12
  Label,
12
13
  Select,
@@ -157,62 +158,68 @@ export const CollectorList: React.FC<CollectorListProps> = ({
157
158
 
158
159
  return (
159
160
  <Card>
160
- <CardHeader className="flex flex-row items-center justify-between">
161
- <CardTitle className="text-base">Check Items</CardTitle>
162
- {addableCollectors.length > 0 && (
163
- <Select value="" onValueChange={handleAdd}>
164
- <SelectTrigger className="w-[200px]">
165
- <Plus className="h-4 w-4 mr-2" />
166
- <SelectValue placeholder="Add collector..." />
167
- </SelectTrigger>
168
- <SelectContent>
169
- {/* Built-in collectors first */}
170
- {builtInCollectors.length > 0 && (
171
- <>
172
- <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
173
- Built-in
174
- </div>
175
- {builtInCollectors
176
- .filter((c) => addableCollectors.some((a) => a.id === c.id))
177
- .map((collector) => (
178
- <SelectItem key={collector.id} value={collector.id}>
179
- <div className="flex items-center gap-2">
180
- <span>{collector.displayName}</span>
181
- {collector.allowMultiple && (
182
- <Badge variant="outline" className="text-xs">
183
- Multiple
184
- </Badge>
185
- )}
186
- </div>
187
- </SelectItem>
188
- ))}
189
- </>
190
- )}
191
- {/* External collectors */}
192
- {externalCollectors.length > 0 && (
193
- <>
194
- <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
195
- External
196
- </div>
197
- {externalCollectors
198
- .filter((c) => addableCollectors.some((a) => a.id === c.id))
199
- .map((collector) => (
200
- <SelectItem key={collector.id} value={collector.id}>
201
- <div className="flex items-center gap-2">
202
- <span>{collector.displayName}</span>
203
- {collector.allowMultiple && (
204
- <Badge variant="outline" className="text-xs">
205
- Multiple
206
- </Badge>
207
- )}
208
- </div>
209
- </SelectItem>
210
- ))}
211
- </>
212
- )}
213
- </SelectContent>
214
- </Select>
215
- )}
161
+ <CardHeader>
162
+ <CardHeaderRow>
163
+ <CardTitle className="text-base">Check Items</CardTitle>
164
+ {addableCollectors.length > 0 && (
165
+ <Select value="" onValueChange={handleAdd}>
166
+ <SelectTrigger className="w-[200px]">
167
+ <Plus className="h-4 w-4 mr-2" />
168
+ <SelectValue placeholder="Add collector..." />
169
+ </SelectTrigger>
170
+ <SelectContent>
171
+ {/* Built-in collectors first */}
172
+ {builtInCollectors.length > 0 && (
173
+ <>
174
+ <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
175
+ Built-in
176
+ </div>
177
+ {builtInCollectors
178
+ .filter((c) =>
179
+ addableCollectors.some((a) => a.id === c.id),
180
+ )
181
+ .map((collector) => (
182
+ <SelectItem key={collector.id} value={collector.id}>
183
+ <div className="flex items-center gap-2">
184
+ <span>{collector.displayName}</span>
185
+ {collector.allowMultiple && (
186
+ <Badge variant="outline" className="text-xs">
187
+ Multiple
188
+ </Badge>
189
+ )}
190
+ </div>
191
+ </SelectItem>
192
+ ))}
193
+ </>
194
+ )}
195
+ {/* External collectors */}
196
+ {externalCollectors.length > 0 && (
197
+ <>
198
+ <div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground">
199
+ External
200
+ </div>
201
+ {externalCollectors
202
+ .filter((c) =>
203
+ addableCollectors.some((a) => a.id === c.id),
204
+ )
205
+ .map((collector) => (
206
+ <SelectItem key={collector.id} value={collector.id}>
207
+ <div className="flex items-center gap-2">
208
+ <span>{collector.displayName}</span>
209
+ {collector.allowMultiple && (
210
+ <Badge variant="outline" className="text-xs">
211
+ Multiple
212
+ </Badge>
213
+ )}
214
+ </div>
215
+ </SelectItem>
216
+ ))}
217
+ </>
218
+ )}
219
+ </SelectContent>
220
+ </Select>
221
+ )}
222
+ </CardHeaderRow>
216
223
  </CardHeader>
217
224
  <CardContent>
218
225
  {configuredCollectors.length === 0 ? (
@@ -11,14 +11,18 @@ import {
11
11
  TableHeader,
12
12
  TableRow,
13
13
  Button,
14
+ Badge,
14
15
  } from "@checkstack/ui";
15
- import { Trash2, Edit } from "lucide-react";
16
+ import { Trash2, Edit, Pause, Play } from "lucide-react";
16
17
 
17
18
  interface HealthCheckListProps {
18
19
  configurations: HealthCheckConfiguration[];
19
20
  strategies: HealthCheckStrategyDto[];
20
21
  onEdit: (config: HealthCheckConfiguration) => void;
21
22
  onDelete: (id: string) => void;
23
+ onPause?: (id: string) => void;
24
+ onResume?: (id: string) => void;
25
+ canManage?: boolean;
22
26
  }
23
27
 
24
28
  export const HealthCheckList: React.FC<HealthCheckListProps> = ({
@@ -26,6 +30,9 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
26
30
  strategies,
27
31
  onEdit,
28
32
  onDelete,
33
+ onPause,
34
+ onResume,
35
+ canManage = true,
29
36
  }) => {
30
37
  const getStrategyName = (id: string) => {
31
38
  return strategies.find((s) => s.id === id)?.displayName || id;
@@ -39,24 +46,57 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
39
46
  <TableHead>Name</TableHead>
40
47
  <TableHead>Strategy</TableHead>
41
48
  <TableHead>Interval (s)</TableHead>
49
+ <TableHead>Status</TableHead>
42
50
  <TableHead className="text-right">Actions</TableHead>
43
51
  </TableRow>
44
52
  </TableHeader>
45
53
  <TableBody>
46
54
  {configurations.length === 0 ? (
47
55
  <TableRow>
48
- <TableCell colSpan={4} className="h-24 text-center">
56
+ <TableCell colSpan={5} className="h-24 text-center">
49
57
  No health checks configured.
50
58
  </TableCell>
51
59
  </TableRow>
52
60
  ) : (
53
61
  configurations.map((config) => (
54
- <TableRow key={config.id}>
62
+ <TableRow
63
+ key={config.id}
64
+ className={config.paused ? "opacity-60" : ""}
65
+ >
55
66
  <TableCell className="font-medium">{config.name}</TableCell>
56
67
  <TableCell>{getStrategyName(config.strategyId)}</TableCell>
57
68
  <TableCell>{config.intervalSeconds}</TableCell>
69
+ <TableCell>
70
+ {config.paused ? (
71
+ <Badge variant="secondary">Paused</Badge>
72
+ ) : (
73
+ <Badge variant="default">Active</Badge>
74
+ )}
75
+ </TableCell>
58
76
  <TableCell className="text-right">
59
77
  <div className="flex justify-end gap-2">
78
+ {canManage &&
79
+ onPause &&
80
+ onResume &&
81
+ (config.paused ? (
82
+ <Button
83
+ variant="ghost"
84
+ size="icon"
85
+ onClick={() => onResume(config.id)}
86
+ title="Resume health check"
87
+ >
88
+ <Play className="h-4 w-4" />
89
+ </Button>
90
+ ) : (
91
+ <Button
92
+ variant="ghost"
93
+ size="icon"
94
+ onClick={() => onPause(config.id)}
95
+ title="Pause health check"
96
+ >
97
+ <Pause className="h-4 w-4" />
98
+ </Button>
99
+ ))}
60
100
  <Button
61
101
  variant="ghost"
62
102
  size="icon"
@@ -64,14 +104,16 @@ export const HealthCheckList: React.FC<HealthCheckListProps> = ({
64
104
  >
65
105
  <Edit className="h-4 w-4" />
66
106
  </Button>
67
- <Button
68
- variant="ghost"
69
- size="icon"
70
- className="text-destructive hover:text-destructive"
71
- onClick={() => onDelete(config.id)}
72
- >
73
- <Trash2 className="h-4 w-4" />
74
- </Button>
107
+ {canManage && (
108
+ <Button
109
+ variant="ghost"
110
+ size="icon"
111
+ className="text-destructive hover:text-destructive"
112
+ onClick={() => onDelete(config.id)}
113
+ >
114
+ <Trash2 className="h-4 w-4" />
115
+ </Button>
116
+ )}
75
117
  </div>
76
118
  </TableCell>
77
119
  </TableRow>
@@ -180,10 +180,7 @@ const ExpandedDetails: React.FC<ExpandedRowProps> = ({ item, systemId }) => {
180
180
  </div>
181
181
 
182
182
  {/* Date Range Filter */}
183
- <div className="flex items-center gap-2">
184
- <span className="text-sm text-muted-foreground">Time Range:</span>
185
- <DateRangeFilter value={dateRange} onChange={setDateRange} />
186
- </div>
183
+ <DateRangeFilter value={dateRange} onChange={setDateRange} />
187
184
 
188
185
  {/* Charts Section */}
189
186
  {renderCharts()}
@@ -301,28 +298,31 @@ export function HealthCheckSystemOverview(props: SlotProps) {
301
298
  return (
302
299
  <div key={item.configurationId} className="rounded-md border bg-card">
303
300
  <button
304
- className="w-full flex items-center justify-between p-4 text-left hover:bg-muted/50 transition-colors"
301
+ className="w-full p-4 text-left hover:bg-muted/50 transition-colors"
305
302
  onClick={() =>
306
303
  setExpandedRow(isExpanded ? undefined : item.configurationId)
307
304
  }
308
305
  >
306
+ {/* Header row: chevron, name, badge */}
309
307
  <div className="flex items-center gap-3">
310
308
  {isExpanded ? (
311
- <ChevronDown className="h-4 w-4 text-muted-foreground" />
309
+ <ChevronDown className="h-4 w-4 text-muted-foreground shrink-0" />
312
310
  ) : (
313
- <ChevronRight className="h-4 w-4 text-muted-foreground" />
311
+ <ChevronRight className="h-4 w-4 text-muted-foreground shrink-0" />
314
312
  )}
315
- <div>
316
- <div className="font-medium">{item.name}</div>
317
- <div className="text-sm text-muted-foreground">
318
- Last run:{" "}
319
- {item.lastRunAt
320
- ? formatDistanceToNow(item.lastRunAt, { addSuffix: true })
321
- : "never"}
322
- </div>
313
+ <div className="flex-1 min-w-0 flex items-center justify-between gap-2">
314
+ <span className="font-medium truncate">{item.name}</span>
315
+ <HealthBadge status={item.state} />
323
316
  </div>
324
317
  </div>
325
- <div className="flex items-center gap-4">
318
+ {/* Details row: last run + sparkline */}
319
+ <div className="ml-7 mt-1 flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2">
320
+ <span className="text-sm text-muted-foreground">
321
+ Last run:{" "}
322
+ {item.lastRunAt
323
+ ? formatDistanceToNow(item.lastRunAt, { addSuffix: true })
324
+ : "never"}
325
+ </span>
326
326
  {item.recentStatusHistory.length > 0 && (
327
327
  <HealthCheckSparkline
328
328
  runs={item.recentStatusHistory.map((status) => ({
@@ -330,7 +330,6 @@ export function HealthCheckSystemOverview(props: SlotProps) {
330
330
  }))}
331
331
  />
332
332
  )}
333
- <HealthBadge status={item.state} />
334
333
  </div>
335
334
  </button>
336
335
  {isExpanded && <ExpandedDetails item={item} systemId={systemId} />}
@@ -21,7 +21,7 @@ import {
21
21
  PageLayout,
22
22
  useToast,
23
23
  } from "@checkstack/ui";
24
- import { Plus, History } from "lucide-react";
24
+ import { Plus, History, Activity } from "lucide-react";
25
25
  import { Link } from "react-router-dom";
26
26
  import { resolveRoute } from "@checkstack/common";
27
27
 
@@ -31,10 +31,10 @@ const HealthCheckConfigPageContent = () => {
31
31
  const toast = useToast();
32
32
  const [searchParams, setSearchParams] = useSearchParams();
33
33
  const { allowed: canRead, loading: accessLoading } = accessApi.useAccess(
34
- healthCheckAccess.configuration.read
34
+ healthCheckAccess.configuration.read,
35
35
  );
36
36
  const { allowed: canManage } = accessApi.useAccess(
37
- healthCheckAccess.configuration.manage
37
+ healthCheckAccess.configuration.manage,
38
38
  );
39
39
 
40
40
  const [isEditorOpen, setIsEditorOpen] = useState(false);
@@ -52,7 +52,7 @@ const HealthCheckConfigPageContent = () => {
52
52
 
53
53
  // Fetch strategies with useQuery
54
54
  const { data: strategies = [] } = healthCheckClient.getStrategies.useQuery(
55
- {}
55
+ {},
56
56
  );
57
57
 
58
58
  const configurations = configurationsData?.configurations ?? [];
@@ -100,6 +100,24 @@ const HealthCheckConfigPageContent = () => {
100
100
  },
101
101
  });
102
102
 
103
+ const pauseMutation = healthCheckClient.pauseConfiguration.useMutation({
104
+ onSuccess: () => {
105
+ void refetchConfigurations();
106
+ },
107
+ onError: (error) => {
108
+ toast.error(error instanceof Error ? error.message : "Failed to pause");
109
+ },
110
+ });
111
+
112
+ const resumeMutation = healthCheckClient.resumeConfiguration.useMutation({
113
+ onSuccess: () => {
114
+ void refetchConfigurations();
115
+ },
116
+ onError: (error) => {
117
+ toast.error(error instanceof Error ? error.message : "Failed to resume");
118
+ },
119
+ });
120
+
103
121
  const handleCreate = () => {
104
122
  setEditingConfig(undefined);
105
123
  setIsEditorOpen(true);
@@ -137,6 +155,7 @@ const HealthCheckConfigPageContent = () => {
137
155
  <PageLayout
138
156
  title="Health Checks"
139
157
  subtitle="Manage health check configurations"
158
+ icon={Activity}
140
159
  loading={accessLoading}
141
160
  allowed={canRead}
142
161
  actions={
@@ -159,6 +178,9 @@ const HealthCheckConfigPageContent = () => {
159
178
  strategies={strategies}
160
179
  onEdit={handleEdit}
161
180
  onDelete={handleDelete}
181
+ onPause={(id) => pauseMutation.mutate(id)}
182
+ onResume={(id) => resumeMutation.mutate(id)}
183
+ canManage={canManage}
162
184
  />
163
185
 
164
186
  <HealthCheckEditor
@@ -184,5 +206,5 @@ const HealthCheckConfigPageContent = () => {
184
206
  };
185
207
 
186
208
  export const HealthCheckConfigPage = wrapInSuspense(
187
- HealthCheckConfigPageContent
209
+ HealthCheckConfigPageContent,
188
210
  );
@@ -25,6 +25,7 @@ import {
25
25
  type DateRange,
26
26
  } from "@checkstack/ui";
27
27
  import { useParams } from "react-router-dom";
28
+ import { History } from "lucide-react";
28
29
  import {
29
30
  HealthCheckRunsTable,
30
31
  type HealthCheckRunDetailed,
@@ -39,7 +40,7 @@ const HealthCheckHistoryDetailPageContent = () => {
39
40
  const healthCheckClient = usePluginClient(HealthCheckApi);
40
41
  const accessApi = useApi(accessApiRef);
41
42
  const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
42
- healthCheckAccess.configuration.manage
43
+ healthCheckAccess.configuration.manage,
43
44
  );
44
45
 
45
46
  const [dateRange, setDateRange] = useState<DateRange>(getDefaultDateRange);
@@ -67,8 +68,9 @@ const HealthCheckHistoryDetailPageContent = () => {
67
68
  title="Health Check Run History"
68
69
  subtitle={`System: ${systemId} • Configuration: ${configurationId?.slice(
69
70
  0,
70
- 8
71
+ 8,
71
72
  )}...`}
73
+ icon={History}
72
74
  loading={accessLoading}
73
75
  allowed={canManage}
74
76
  actions={
@@ -100,5 +102,5 @@ const HealthCheckHistoryDetailPageContent = () => {
100
102
  };
101
103
 
102
104
  export const HealthCheckHistoryDetailPage = wrapInSuspense(
103
- HealthCheckHistoryDetailPageContent
105
+ HealthCheckHistoryDetailPageContent,
104
106
  );
@@ -21,12 +21,13 @@ import {
21
21
  healthCheckAccess,
22
22
  HealthCheckApi,
23
23
  } from "@checkstack/healthcheck-common";
24
+ import { History } from "lucide-react";
24
25
 
25
26
  const HealthCheckHistoryPageContent = () => {
26
27
  const healthCheckClient = usePluginClient(HealthCheckApi);
27
28
  const accessApi = useApi(accessApiRef);
28
29
  const { allowed: canManage, loading: accessLoading } = accessApi.useAccess(
29
- healthCheckAccess.configuration.manage
30
+ healthCheckAccess.configuration.manage,
30
31
  );
31
32
 
32
33
  // Pagination state
@@ -47,6 +48,7 @@ const HealthCheckHistoryPageContent = () => {
47
48
  <PageLayout
48
49
  title="Health Check History"
49
50
  subtitle="Detailed run history with full result data"
51
+ icon={History}
50
52
  loading={accessLoading}
51
53
  allowed={canManage}
52
54
  >
@@ -68,5 +70,5 @@ const HealthCheckHistoryPageContent = () => {
68
70
  };
69
71
 
70
72
  export const HealthCheckHistoryPage = wrapInSuspense(
71
- HealthCheckHistoryPageContent
73
+ HealthCheckHistoryPageContent,
72
74
  );