@@ -3,7 +3,11 @@ import {
3
3
provisionerDaemonGroups ,
4
4
provisionerJobs ,
5
5
} from "api/queries/organizations" ;
6
- import type { Organization , ProvisionerJobStatus } from "api/typesGenerated" ;
6
+ import type {
7
+ Organization ,
8
+ ProvisionerJob ,
9
+ ProvisionerJobStatus ,
10
+ } from "api/typesGenerated" ;
7
11
import { Avatar } from "components/Avatar/Avatar" ;
8
12
import { Badge } from "components/Badge/Badge" ;
9
13
import { Button } from "components/Button/Button" ;
@@ -32,13 +36,21 @@ import {
32
36
TooltipTrigger ,
33
37
} from "components/Tooltip/Tooltip" ;
34
38
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata" ;
35
- import { BanIcon , TriangleAlertIcon } from "lucide-react" ;
39
+ import { useSearchParamsKey } from "hooks/useSearchParamsKey" ;
40
+ import {
41
+ BanIcon ,
42
+ ChevronDownIcon ,
43
+ ChevronRightIcon ,
44
+ Tangent ,
45
+ TriangleAlertIcon ,
46
+ } from "lucide-react" ;
36
47
import { useDashboard } from "modules/dashboard/useDashboard" ;
37
48
import { useOrganizationSettings } from "modules/management/OrganizationSettingsLayout" ;
38
- import type { FC } from "react" ;
49
+ import { useState , type FC } from "react" ;
39
50
import { Helmet } from "react-helmet-async" ;
40
51
import { useQuery } from "react-query" ;
41
52
import { useParams } from "react-router-dom" ;
53
+ import { cn } from "utils/cn" ;
42
54
import { docs } from "utils/docs" ;
43
55
import { pageTitle } from "utils/page" ;
44
56
import { relativeTime } from "utils/time" ;
@@ -48,6 +60,10 @@ const OrganizationProvisionersPage: FC = () => {
48
60
// organization: string;
49
61
// };
50
62
const { organization } = useOrganizationSettings ( ) ;
63
+ const tab = useSearchParamsKey ( {
64
+ key : "tab" ,
65
+ defaultValue : "jobs" ,
66
+ } ) ;
51
67
// const { entitlements } = useDashboard();
52
68
// const { metadata } = useEmbeddedMetadata();
53
69
// const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
@@ -76,7 +92,7 @@ const OrganizationProvisionersPage: FC = () => {
76
92
</ header >
77
93
78
94
< main >
79
- < Tabs active = "jobs" >
95
+ < Tabs active = { tab . value } >
80
96
< TabsList >
81
97
< TabLink value = "jobs" to = "?tab=jobs" >
82
98
Jobs
@@ -88,7 +104,7 @@ const OrganizationProvisionersPage: FC = () => {
88
104
</ Tabs >
89
105
90
106
< div className = "mt-6" >
91
- < JobsTabContent org = { organization } />
107
+ { tab . value === "jobs" && < JobsTabContent org = { organization } /> }
92
108
</ div >
93
109
</ main >
94
110
</ div >
@@ -101,7 +117,6 @@ type JobsTabContentProps = {
101
117
} ;
102
118
103
119
const JobsTabContent : FC < JobsTabContentProps > = ( { org } ) => {
104
- const { organization } = useOrganizationSettings ( ) ;
105
120
const { data : jobs , isLoadingError } = useQuery ( provisionerJobs ( org . id ) ) ;
106
121
107
122
return (
@@ -125,76 +140,7 @@ const JobsTabContent: FC<JobsTabContentProps> = ({ org }) => {
125
140
< TableBody >
126
141
{ jobs ? (
127
142
jobs . length > 0 ? (
128
- jobs . map ( ( { metadata, ...job } ) => {
129
- if ( ! metadata ) {
130
- throw new Error (
131
- `Metadata is required but it is missing in the job ${ job . id } ` ,
132
- ) ;
133
- }
134
-
135
- const canCancel = [ "pending" , "running" ] . includes ( job . status ) ;
136
-
137
- return (
138
- < TableRow key = { job . id } >
139
- < TableCell className = "[&:first-letter]:uppercase" >
140
- { relativeTime ( new Date ( job . created_at ) ) }
141
- </ TableCell >
142
- < TableCell >
143
- < Badge size = "sm" > { job . type } </ Badge >
144
- </ TableCell >
145
- < TableCell >
146
- < div className = "flex items-center gap-1" >
147
- < Avatar
148
- variant = "icon"
149
- src = { metadata . template_icon }
150
- fallback = {
151
- metadata . template_display_name ||
152
- metadata . template_name
153
- }
154
- />
155
- { metadata . template_display_name ??
156
- metadata . template_name }
157
- </ div >
158
- </ TableCell >
159
- < TableCell >
160
- < Badge size = "sm" > [foo=bar]</ Badge >
161
- </ TableCell >
162
- < TableCell >
163
- < StatusIndicator
164
- size = "sm"
165
- variant = { statusIndicatorVariant ( job . status ) }
166
- >
167
- < StatusIndicatorDot />
168
- < span className = "[&:first-letter]:uppercase" >
169
- { job . status }
170
- </ span >
171
- { job . status === "failed" && (
172
- < TriangleAlertIcon className = "size-icon-xs p-[1px]" />
173
- ) }
174
- { job . status === "pending" &&
175
- `(${ job . queue_position } /${ job . queue_size } )` }
176
- </ StatusIndicator >
177
- </ TableCell >
178
- < TableCell className = "text-right" >
179
- < TooltipProvider >
180
- < Tooltip >
181
- < TooltipTrigger asChild >
182
- < Button
183
- disabled = { ! canCancel }
184
- aria-label = "Cancel job"
185
- size = "icon"
186
- variant = "outline"
187
- >
188
- < BanIcon />
189
- </ Button >
190
- </ TooltipTrigger >
191
- < TooltipContent > Cancel job</ TooltipContent >
192
- </ Tooltip >
193
- </ TooltipProvider >
194
- </ TableCell >
195
- </ TableRow >
196
- ) ;
197
- } )
143
+ jobs . map ( ( j ) => < JobRow key = { j . id } job = { j } /> )
198
144
) : (
199
145
< TableEmpty message = "No provisioner jobs found" />
200
146
)
@@ -209,6 +155,136 @@ const JobsTabContent: FC<JobsTabContentProps> = ({ org }) => {
209
155
) ;
210
156
} ;
211
157
158
+ type JobRowProps = {
159
+ job : ProvisionerJob ;
160
+ } ;
161
+
162
+ const JobRow : FC < JobRowProps > = ( { job } ) => {
163
+ const metadata = job . metadata ;
164
+ const canCancel = [ "pending" , "running" ] . includes ( job . status ) ;
165
+ const [ isOpen , setIsOpen ] = useState ( false ) ;
166
+
167
+ return (
168
+ < >
169
+ < TableRow key = { job . id } >
170
+ < TableCell >
171
+ < button
172
+ className = { cn ( [
173
+ "flex items-center gap-1 p-0 bg-transparent border-0 text-inherit text-xs cursor-pointer" ,
174
+ "transition-colors hover:text-content-primary font-medium" ,
175
+ isOpen && "text-content-primary" ,
176
+ ] ) }
177
+ type = "button"
178
+ onClick = { ( ) => {
179
+ setIsOpen ( ( v ) => ! v ) ;
180
+ } }
181
+ >
182
+ { isOpen ? (
183
+ < ChevronDownIcon className = "size-icon-sm p-0.5" />
184
+ ) : (
185
+ < ChevronRightIcon className = "size-icon-sm p-0.5" />
186
+ ) }
187
+ < span className = "[&:first-letter]:uppercase" >
188
+ { relativeTime ( new Date ( job . created_at ) ) }
189
+ </ span >
190
+ </ button >
191
+ </ TableCell >
192
+ < TableCell >
193
+ < Badge size = "sm" > { job . type } </ Badge >
194
+ </ TableCell >
195
+ < TableCell >
196
+ { job . metadata . template_name ? (
197
+ < div className = "flex items-center gap-1" >
198
+ < Avatar
199
+ variant = "icon"
200
+ src = { metadata . template_icon }
201
+ fallback = {
202
+ metadata . template_display_name || metadata . template_name
203
+ }
204
+ />
205
+ { metadata . template_display_name ?? metadata . template_name }
206
+ </ div >
207
+ ) : (
208
+ "Not linked to any template"
209
+ ) }
210
+ </ TableCell >
211
+ < TableCell >
212
+ < Badge size = "sm" > [foo=bar]</ Badge >
213
+ </ TableCell >
214
+ < TableCell >
215
+ < StatusIndicator
216
+ size = "sm"
217
+ variant = { statusIndicatorVariant ( job . status ) }
218
+ >
219
+ < StatusIndicatorDot />
220
+ < span className = "[&:first-letter]:uppercase" > { job . status } </ span >
221
+ { job . status === "failed" && (
222
+ < TriangleAlertIcon className = "size-icon-xs p-[1px]" />
223
+ ) }
224
+ { job . status === "pending" &&
225
+ `(${ job . queue_position } /${ job . queue_size } )` }
226
+ </ StatusIndicator >
227
+ </ TableCell >
228
+ < TableCell className = "text-right" >
229
+ < TooltipProvider >
230
+ < Tooltip >
231
+ < TooltipTrigger asChild >
232
+ < Button
233
+ disabled = { ! canCancel }
234
+ aria-label = "Cancel job"
235
+ size = "icon"
236
+ variant = "outline"
237
+ >
238
+ < BanIcon />
239
+ </ Button >
240
+ </ TooltipTrigger >
241
+ < TooltipContent > Cancel job</ TooltipContent >
242
+ </ Tooltip >
243
+ </ TooltipProvider >
244
+ </ TableCell >
245
+ </ TableRow >
246
+
247
+ { isOpen && (
248
+ < TableRow >
249
+ < TableCell colSpan = { 999 } className = "p-4 border-t-0" >
250
+ < div
251
+ className = { cn ( [
252
+ "grid grid-cols-[auto_1fr] gap-x-4 items-center" ,
253
+ "[&_span:nth-child(even)]:text-content-primary [&_span:nth-child(even)]:font-mono" ,
254
+ "[&_span:nth-child(even)]:leading-[22px]" ,
255
+ ] ) }
256
+ >
257
+ < span > Job ID:</ span >
258
+ < span > { job . id } </ span >
259
+
260
+ < span > Available provisioners:</ span >
261
+ < span >
262
+ { job . available_workers
263
+ ? JSON . stringify ( job . available_workers )
264
+ : "[]" }
265
+ </ span >
266
+
267
+ < span > Completed by provisioner:</ span >
268
+ < span > { job . worker_id } </ span >
269
+
270
+ < span > Associated workspace:</ span >
271
+ < span > { job . metadata . workspace_name ?? "null" } </ span >
272
+
273
+ < span > Creation time:</ span >
274
+ < span > { job . created_at } </ span >
275
+
276
+ < span > Queue:</ span >
277
+ < span >
278
+ { job . queue_position } /{ job . queue_size }
279
+ </ span >
280
+ </ div >
281
+ </ TableCell >
282
+ </ TableRow >
283
+ ) }
284
+ </ >
285
+ ) ;
286
+ } ;
287
+
212
288
function statusIndicatorVariant (
213
289
status : ProvisionerJobStatus ,
214
290
) : StatusIndicatorProps [ "variant" ] {
0 commit comments