Skip to content

Commit 2e960b1

Browse files
make progress animation prettier with a spinner
1 parent c324f49 commit 2e960b1

File tree

2 files changed

+44
-19
lines changed

2 files changed

+44
-19
lines changed

packages/app/src/cli/services/watch-bulk-operation.test.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,27 @@ import {watchBulkOperation} from './watch-bulk-operation.js'
22
import {adminRequest} from '@shopify/cli-kit/node/api/admin'
33
import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session'
44
import {sleep} from '@shopify/cli-kit/node/system'
5-
import {outputInfo} from '@shopify/cli-kit/node/output'
6-
import {describe, test, expect, vi, beforeEach} from 'vitest'
5+
import {describe, test, expect, vi, beforeEach, afterEach} from 'vitest'
76

87
vi.mock('@shopify/cli-kit/node/api/admin')
98
vi.mock('@shopify/cli-kit/node/session')
109
vi.mock('@shopify/cli-kit/node/system')
11-
vi.mock('@shopify/cli-kit/node/output')
1210

1311
describe('watchBulkOperation', () => {
1412
const mockSession = {token: 'test-token', storeFqdn: 'test-store.myshopify.com'}
1513
const operationId = 'gid://shopify/BulkOperation/123'
14+
let stdoutWriteSpy: unknown
1615

1716
beforeEach(() => {
1817
vi.mocked(ensureAuthenticatedAdmin).mockResolvedValue(mockSession)
1918
vi.mocked(sleep).mockResolvedValue()
20-
vi.mocked(outputInfo).mockReturnValue()
19+
stdoutWriteSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
20+
})
21+
22+
afterEach(() => {
23+
if (stdoutWriteSpy && typeof stdoutWriteSpy === 'object' && 'mockRestore' in stdoutWriteSpy) {
24+
;(stdoutWriteSpy as {mockRestore: () => void}).mockRestore()
25+
}
2126
})
2227

2328
test('polls until operation reaches COMPLETED status', async () => {
@@ -33,6 +38,7 @@ describe('watchBulkOperation', () => {
3338
})
3439

3540
expect(result).toEqual(bulkOperation(400, 'COMPLETED'))
41+
expect(stdoutWriteSpy).toHaveBeenCalled()
3642
})
3743

3844
test('stops polling and returns the bulk operation when it is FAILED', async () => {

packages/app/src/cli/services/watch-bulk-operation.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {GetBulkOperationById, BulkOperation, GetBulkOperationByIdSchema} from '.
22
import {adminRequest} from '@shopify/cli-kit/node/api/admin'
33
import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session'
44
import {sleep} from '@shopify/cli-kit/node/system'
5-
import {outputInfo} from '@shopify/cli-kit/node/output'
65

76
interface WatchBulkOperationOptions {
87
id: string
@@ -11,28 +10,48 @@ interface WatchBulkOperationOptions {
1110

1211
const TERMINAL_STATUSES = ['COMPLETED', 'FAILED', 'CANCELED', 'EXPIRED']
1312
const POLL_INTERVAL_SECONDS = 5
13+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
1414

1515
export async function watchBulkOperation(options: WatchBulkOperationOptions): Promise<BulkOperation | null> {
1616
const {id, storeFqdn} = options
1717
const adminSession = await ensureAuthenticatedAdmin(storeFqdn)
1818

19-
while (true) {
20-
// eslint-disable-next-line no-await-in-loop
21-
const response = await adminRequest<GetBulkOperationByIdSchema>(GetBulkOperationById, adminSession, {id})
19+
let frameIndex = 0
20+
let currentOperation: BulkOperation | null = null
21+
let isDone = false
2222

23-
if (!response.node) {
24-
outputInfo(`bulk operation ${id} not found`)
25-
return null
26-
}
23+
const spinnerInterval = setInterval(() => {
24+
if (isDone) return
25+
if (!currentOperation) return
2726

28-
const operation = response.node
29-
outputInfo(`${operation.status} - ${operation.objectCount} objects`)
27+
const spinner = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]
28+
frameIndex++
3029

31-
if (TERMINAL_STATUSES.includes(operation.status)) {
32-
return operation
33-
}
30+
process.stdout.write(`\r\x1b[K${spinner} ${currentOperation.status} - ${currentOperation.objectCount} objects`)
31+
}, 80)
32+
33+
try {
34+
while (true) {
35+
// eslint-disable-next-line no-await-in-loop
36+
const response = await adminRequest<GetBulkOperationByIdSchema>(GetBulkOperationById, adminSession, {id})
37+
38+
if (!response.node) {
39+
process.stdout.write('\r\x1b[K')
40+
return null
41+
}
3442

35-
// eslint-disable-next-line no-await-in-loop
36-
await sleep(POLL_INTERVAL_SECONDS)
43+
currentOperation = response.node
44+
45+
if (TERMINAL_STATUSES.includes(currentOperation.status)) {
46+
process.stdout.write('\n')
47+
return currentOperation
48+
}
49+
50+
// eslint-disable-next-line no-await-in-loop
51+
await sleep(POLL_INTERVAL_SECONDS)
52+
}
53+
} finally {
54+
isDone = true
55+
clearInterval(spinnerInterval)
3756
}
3857
}

0 commit comments

Comments
 (0)