-
Notifications
You must be signed in to change notification settings - Fork 224
Guide users to use bulk status when an operation is still running in the background
#6678
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,7 +6,7 @@ import {downloadBulkOperationResults} from './download-bulk-operation-results.js | |
| import {BulkOperationRunQueryMutation} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-query.js' | ||
| import {BulkOperationRunMutationMutation} from '../../api/graphql/bulk-operations/generated/bulk-operation-run-mutation.js' | ||
| import {OrganizationApp} from '../../models/organization.js' | ||
| import {renderSuccess, renderWarning, renderError} from '@shopify/cli-kit/node/ui' | ||
| import {renderSuccess, renderWarning, renderError, renderInfo} from '@shopify/cli-kit/node/ui' | ||
| import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session' | ||
| import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs' | ||
| import {joinPath} from '@shopify/cli-kit/node/path' | ||
|
|
@@ -355,14 +355,45 @@ describe('executeBulkOperation', () => { | |
| watch: true, | ||
| }) | ||
|
|
||
| expect(watchBulkOperation).toHaveBeenCalledWith(mockAdminSession, createdBulkOperation.id) | ||
| expect(renderSuccess).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| headline: expect.stringContaining('Bulk operation succeeded:'), | ||
| }), | ||
| ) | ||
| }) | ||
|
|
||
| test('renders help message in an info banner when watch is provided and user aborts', async () => { | ||
| const query = '{ products { edges { node { id } } } }' | ||
| const initialResponse: BulkOperationRunQueryMutation['bulkOperationRunQuery'] = { | ||
| bulkOperation: createdBulkOperation, | ||
| userErrors: [], | ||
| } | ||
| const runningOperation = { | ||
| ...createdBulkOperation, | ||
| status: 'RUNNING' as const, | ||
| objectCount: '100', | ||
| } | ||
|
|
||
| vi.mocked(runBulkOperationQuery).mockResolvedValue(initialResponse) | ||
| vi.mocked(watchBulkOperation).mockImplementation(async (_session, _id, signal, onAbort) => { | ||
| onAbort() | ||
| return runningOperation | ||
| }) | ||
|
|
||
| await executeBulkOperation({ | ||
| remoteApp: mockRemoteApp, | ||
| storeFqdn, | ||
| query, | ||
| watch: true, | ||
| }) | ||
|
|
||
| expect(renderInfo).toHaveBeenCalledWith({ | ||
| headline: `Bulk operation ${createdBulkOperation.id} is still running in the background.`, | ||
| body: ['Monitor its progress with:', {command: expect.stringContaining('shopify app bulk status')}], | ||
| }) | ||
| expect(downloadBulkOperationResults).not.toHaveBeenCalled() | ||
| }) | ||
|
|
||
| test('writes results to file when --output-file flag is provided', async () => { | ||
| const query = '{ products { edges { node { id } } } }' | ||
| const outputFile = '/tmp/results.jsonl' | ||
|
|
@@ -450,7 +481,6 @@ describe('executeBulkOperation', () => { | |
| watch: true, | ||
| }) | ||
|
|
||
| expect(watchBulkOperation).toHaveBeenCalledWith(mockAdminSession, createdBulkOperation.id) | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| expect(renderError).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| headline: expect.any(String), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,10 +4,11 @@ import {watchBulkOperation, type BulkOperation} from './watch-bulk-operation.js' | |
| import {formatBulkOperationStatus} from './format-bulk-operation-status.js' | ||
| import {downloadBulkOperationResults} from './download-bulk-operation-results.js' | ||
| import {OrganizationApp} from '../../models/organization.js' | ||
| import {renderSuccess, renderInfo, renderError, renderWarning} from '@shopify/cli-kit/node/ui' | ||
| import {renderSuccess, renderInfo, renderError, renderWarning, TokenItem} from '@shopify/cli-kit/node/ui' | ||
| import {outputContent, outputToken, outputResult} from '@shopify/cli-kit/node/output' | ||
| import {ensureAuthenticatedAdminAsApp} from '@shopify/cli-kit/node/session' | ||
| import {AbortError, BugError} from '@shopify/cli-kit/node/error' | ||
| import {AbortController} from '@shopify/cli-kit/node/abort' | ||
| import {parse} from 'graphql' | ||
| import {readFile, writeFile, fileExists} from '@shopify/cli-kit/node/fs' | ||
|
|
||
|
|
@@ -76,8 +77,19 @@ export async function executeBulkOperation(input: ExecuteBulkOperationInput): Pr | |
| const createdOperation = bulkOperationResponse?.bulkOperation | ||
| if (createdOperation) { | ||
| if (watch) { | ||
| const finishedOperation = await watchBulkOperation(adminSession, createdOperation.id) | ||
| await renderBulkOperationResult(finishedOperation, outputFile) | ||
| const abortController = new AbortController() | ||
| const operation = await watchBulkOperation(adminSession, createdOperation.id, abortController.signal, () => | ||
| abortController.abort(), | ||
| ) | ||
|
|
||
| if (abortController.signal.aborted) { | ||
| renderInfo({ | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Made this an info banner per the brief, but I do think it looks a bit odd being all grey, maybe a success banner would more appropriately convey to the user "nothing went wrong, all is still good and running nicely". I'm not really sure. Let me know what you think is correct!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree a success banner would be better. Actually, why not showing exactly the same message as when you run it without
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I considered this but decided it was a slightly different context so the most helpful possible messaging to the user is slightly different. I'll log this for Nick to think about though! TY!
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since this is the Ctrl+C banner, I think info is correct, since it’s just informing you that the query is still running. |
||
| headline: `Bulk operation ${operation.id} is still running in the background.`, | ||
| body: statusCommandHelpMessage(operation.id), | ||
| }) | ||
| } else { | ||
| await renderBulkOperationResult(operation, outputFile) | ||
| } | ||
| } else { | ||
| await renderBulkOperationResult(createdOperation, outputFile) | ||
| } | ||
|
|
@@ -105,7 +117,11 @@ async function renderBulkOperationResult(operation: BulkOperation, outputFile?: | |
|
|
||
| switch (operation.status) { | ||
| case 'CREATED': | ||
| renderSuccess({headline: 'Bulk operation started.', customSections}) | ||
| renderSuccess({ | ||
| headline: 'Bulk operation started.', | ||
| body: statusCommandHelpMessage(operation.id), | ||
| customSections, | ||
| }) | ||
| break | ||
| case 'COMPLETED': | ||
| if (operation.url) { | ||
|
|
@@ -147,6 +163,10 @@ function validateGraphQLDocument(graphqlOperation: string, variablesJsonl?: stri | |
| } | ||
| } | ||
|
|
||
| function statusCommandHelpMessage(operationId: string): TokenItem { | ||
| return ['Monitor its progress with:', {command: `shopify app bulk status --id="${operationId}}"`}] | ||
| } | ||
|
|
||
| function isMutation(graphqlOperation: string): boolean { | ||
| const document = parse(graphqlOperation) | ||
| const operation = document.definitions.find((def) => def.kind === 'OperationDefinition') | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,21 +1,33 @@ | ||
| import {LoadingBar} from './LoadingBar.js' | ||
| import {useExitOnCtrlC} from '../hooks/use-exit-on-ctrl-c.js' | ||
| import {handleCtrlC} from '../../ui.js' | ||
| import {TokenizedString} from '../../../../public/node/output.js' | ||
| import React, {useEffect, useState} from 'react' | ||
| import {useApp} from 'ink' | ||
| import {useApp, useInput, useStdin} from 'ink' | ||
|
|
||
| interface SingleTaskProps<T> { | ||
| title: TokenizedString | ||
| task: (updateStatus: (status: TokenizedString) => void) => Promise<T> | ||
| onComplete?: (result: T) => void | ||
| onAbort?: () => void | ||
| noColor?: boolean | ||
| } | ||
|
|
||
| const SingleTask = <T,>({task, title, onComplete, noColor}: SingleTaskProps<T>) => { | ||
| const SingleTask = <T,>({task, title, onComplete, onAbort, noColor}: SingleTaskProps<T>) => { | ||
| const [status, setStatus] = useState(title) | ||
| const [isDone, setIsDone] = useState(false) | ||
| const {exit: unmountInk} = useApp() | ||
| useExitOnCtrlC() | ||
| const {isRawModeSupported} = useStdin() | ||
|
|
||
| useInput( | ||
| (input, key) => { | ||
| if (onAbort) { | ||
| handleCtrlC(input, key, onAbort) | ||
| } else { | ||
| handleCtrlC(input, key) | ||
| } | ||
| }, | ||
| {isActive: Boolean(isRawModeSupported)}, | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This part is basically saying: only activate this behaviour if we're in an interactive terminal. If STDIN isn't a TTY, for example if input is being piped, we're running in CI, or it's otherwise non-interactive, then |
||
| ) | ||
|
|
||
| useEffect(() => { | ||
| task(setStatus) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(I don't think this assertion was providing actual value, since the thing we actually care about is that the success banner was rendered; I opted to just remove it instead of fixing the parameters on the assertion.)