@@ -3,7 +3,8 @@ import React, { useEffect, useState, useCallback, useMemo } from 'react'
3
3
import * as H from 'history'
4
4
import { parse as parseJSONC } from 'jsonc-parser'
5
5
import { Redirect , useHistory } from 'react-router'
6
- import { Observable , Subject } from 'rxjs'
6
+ import { Subject } from 'rxjs'
7
+ import { delay , repeatWhen } from 'rxjs/operators'
7
8
8
9
import { ErrorAlert } from '@sourcegraph/branded/src/components/alerts'
9
10
import { hasProperty } from '@sourcegraph/common'
@@ -20,6 +21,7 @@ import {
20
21
ExternalServiceSyncJobConnectionFields ,
21
22
ExternalServiceResult ,
22
23
ExternalServiceVariables ,
24
+ ExternalServiceSyncJobState ,
23
25
} from '../../graphql-operations'
24
26
import { FilteredConnection , FilteredConnectionQueryArguments } from '../FilteredConnection'
25
27
import { LoaderButton } from '../LoaderButton'
@@ -32,12 +34,15 @@ import {
32
34
queryExternalServiceSyncJobs as _queryExternalServiceSyncJobs ,
33
35
useUpdateExternalService ,
34
36
FETCH_EXTERNAL_SERVICE ,
37
+ useCancelExternalServiceSync ,
35
38
} from './backend'
36
39
import { ExternalServiceCard } from './ExternalServiceCard'
37
40
import { ExternalServiceForm } from './ExternalServiceForm'
38
41
import { defaultExternalServices , codeHostExternalServices } from './externalServices'
39
42
import { ExternalServiceWebhook } from './ExternalServiceWebhook'
40
43
44
+ import styles from './ExternalServicePage.module.scss'
45
+
41
46
interface Props extends TelemetryProps {
42
47
externalServiceID : Scalars [ 'ID' ]
43
48
isLightTheme : boolean
@@ -235,7 +240,7 @@ export const ExternalServicePage: React.FunctionComponent<React.PropsWithChildre
235
240
236
241
interface ExternalServiceSyncJobsListProps {
237
242
externalServiceID : Scalars [ 'ID' ]
238
- updates : Observable < void >
243
+ updates : Subject < void >
239
244
240
245
/** For testing only. */
241
246
queryExternalServiceSyncJobs ?: typeof _queryExternalServiceSyncJobs
@@ -251,7 +256,7 @@ const ExternalServiceSyncJobsList: React.FunctionComponent<ExternalServiceSyncJo
251
256
queryExternalServiceSyncJobs ( {
252
257
first : args . first ?? null ,
253
258
externalService : externalServiceID ,
254
- } ) ,
259
+ } ) . pipe ( repeatWhen ( obs => obs . pipe ( delay ( 1500 ) ) ) ) ,
255
260
[ externalServiceID , queryExternalServiceSyncJobs ]
256
261
)
257
262
@@ -272,7 +277,7 @@ const ExternalServiceSyncJobsList: React.FunctionComponent<ExternalServiceSyncJo
272
277
pluralNoun = "sync jobs"
273
278
queryConnection = { queryConnection }
274
279
nodeComponent = { ExternalServiceSyncJobNode }
275
- nodeComponentProps = { { } }
280
+ nodeComponentProps = { { onUpdate : updates } }
276
281
hideSearch = { true }
277
282
noSummaryIfAllNodesVisible = { true }
278
283
history = { history }
@@ -285,47 +290,83 @@ const ExternalServiceSyncJobsList: React.FunctionComponent<ExternalServiceSyncJo
285
290
286
291
interface ExternalServiceSyncJobNodeProps {
287
292
node : ExternalServiceSyncJobListFields
293
+ onUpdate : Subject < void >
288
294
}
289
295
290
- const ExternalServiceSyncJobNode : React . FunctionComponent < ExternalServiceSyncJobNodeProps > = ( { node } ) => (
291
- < li className = "list-group-item py-3" >
292
- < div className = "d-flex align-items-center justify-content-between" >
293
- < div className = "flex-shrink-0 mr-2" >
294
- < Badge > { node . state } </ Badge >
295
- </ div >
296
- < div className = "flex-shrink-0" >
297
- { node . startedAt && (
298
- < >
299
- { node . finishedAt === null && < > Running since </ > }
300
- { node . finishedAt !== null && < > Ran for </ > }
301
- < Duration
302
- start = { node . startedAt }
303
- end = { node . finishedAt ?? undefined }
304
- stableWidth = { false }
305
- className = "d-inline"
306
- />
307
- </ >
308
- ) }
309
- </ div >
310
- < div className = "text-right flex-grow-1" >
311
- < div >
312
- { node . startedAt === null && 'Not started yet' }
313
- { node . startedAt !== null && (
314
- < >
315
- Started < Timestamp date = { node . startedAt } />
316
- </ >
317
- ) }
296
+ const ExternalServiceSyncJobNode : React . FunctionComponent < ExternalServiceSyncJobNodeProps > = ( { node, onUpdate } ) => {
297
+ const [
298
+ cancelExternalServiceSync ,
299
+ { error : cancelSyncJobError , loading : cancelSyncJobLoading } ,
300
+ ] = useCancelExternalServiceSync ( )
301
+
302
+ const cancelJob = useCallback (
303
+ ( ) =>
304
+ cancelExternalServiceSync ( { variables : { id : node . id } } ) . then ( ( ) => {
305
+ onUpdate . next ( )
306
+ // Optimistically set state.
307
+ node . state = ExternalServiceSyncJobState . CANCELING
308
+ } ) ,
309
+ [ cancelExternalServiceSync , node , onUpdate ]
310
+ )
311
+
312
+ return (
313
+ < li className = "list-group-item py-3" >
314
+ < div className = "d-flex align-items-center justify-content-between" >
315
+ < div className = "flex-shrink-0 mr-2" >
316
+ < Badge > { node . state } </ Badge >
318
317
</ div >
319
- < div >
320
- { node . finishedAt === null && 'Not finished yet' }
321
- { node . finishedAt !== null && (
318
+ < div className = "flex-shrink-0 flex-grow-1 mr-2" >
319
+ { node . startedAt && (
322
320
< >
323
- Finished < Timestamp date = { node . finishedAt } />
321
+ { node . finishedAt === null && < > Running since </ > }
322
+ { node . finishedAt !== null && < > Ran for </ > }
323
+ < Duration
324
+ start = { node . startedAt }
325
+ end = { node . finishedAt ?? undefined }
326
+ stableWidth = { false }
327
+ className = "d-inline"
328
+ />
329
+ { cancelSyncJobError && < ErrorAlert error = { cancelSyncJobError } /> }
324
330
</ >
325
331
) }
326
332
</ div >
333
+ { [
334
+ ExternalServiceSyncJobState . QUEUED ,
335
+ ExternalServiceSyncJobState . PROCESSING ,
336
+ ExternalServiceSyncJobState . CANCELING ,
337
+ ] . includes ( node . state ) && (
338
+ < LoaderButton
339
+ label = "Cancel"
340
+ alwaysShowLabel = { true }
341
+ variant = "danger"
342
+ outline = { true }
343
+ size = "sm"
344
+ onClick = { cancelJob }
345
+ loading = { cancelSyncJobLoading || node . state === ExternalServiceSyncJobState . CANCELING }
346
+ disabled = { cancelSyncJobLoading || node . state === ExternalServiceSyncJobState . CANCELING }
347
+ className = { styles . cancelButton }
348
+ />
349
+ ) }
350
+ < div className = "text-right flex-shrink-0" >
351
+ < div >
352
+ { node . startedAt === null && 'Not started yet' }
353
+ { node . startedAt !== null && (
354
+ < >
355
+ Started < Timestamp date = { node . startedAt } />
356
+ </ >
357
+ ) }
358
+ </ div >
359
+ < div >
360
+ { node . finishedAt === null && 'Not finished yet' }
361
+ { node . finishedAt !== null && (
362
+ < >
363
+ Finished < Timestamp date = { node . finishedAt } />
364
+ </ >
365
+ ) }
366
+ </ div >
367
+ </ div >
327
368
</ div >
328
- </ div >
329
- { node . failureMessage && < ErrorAlert error = { node . failureMessage } className = "mt-2 mb-0" /> }
330
- </ li >
331
- )
369
+ { node . failureMessage && < ErrorAlert error = { node . failureMessage } className = "mt-2 mb-0" /> }
370
+ </ li >
371
+ )
372
+ }
0 commit comments