diff --git a/.changeset/devtools-cancel-scheduled-task.md b/.changeset/devtools-cancel-scheduled-task.md new file mode 100644 index 0000000000..fadd51f2aa --- /dev/null +++ b/.changeset/devtools-cancel-scheduled-task.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-devtools-common': patch +'@backstage/plugin-devtools': patch +--- + +Added `cancelScheduledTask` to the DevTools API and a cancel button to the scheduled tasks UI. diff --git a/plugins/devtools-common/src/alpha.ts b/plugins/devtools-common/src/alpha.ts index 984967c4f3..7abe4b1f5e 100644 --- a/plugins/devtools-common/src/alpha.ts +++ b/plugins/devtools-common/src/alpha.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { devToolsTaskSchedulerReadPermission, devToolsTaskSchedulerCreatePermission, diff --git a/plugins/devtools/src/api/DevToolsApi.ts b/plugins/devtools/src/api/DevToolsApi.ts index 4fc579ee06..2aaad49aa8 100644 --- a/plugins/devtools/src/api/DevToolsApi.ts +++ b/plugins/devtools/src/api/DevToolsApi.ts @@ -38,4 +38,5 @@ export interface DevToolsApi { plugin: string, taskId: string, ): Promise; + cancelScheduledTask(plugin: string, taskId: string): Promise; } diff --git a/plugins/devtools/src/api/DevToolsClient.ts b/plugins/devtools/src/api/DevToolsClient.ts index 0577757838..b6d9d4b119 100644 --- a/plugins/devtools/src/api/DevToolsClient.ts +++ b/plugins/devtools/src/api/DevToolsClient.ts @@ -24,7 +24,7 @@ import { ScheduledTasks, TriggerScheduledTask, } from '@backstage/plugin-devtools-common/alpha'; -import { ResponseError } from '@backstage/errors'; +import { ResponseError, NotFoundError, ConflictError } from '@backstage/errors'; import { DevToolsApi } from './DevToolsApi'; export class DevToolsClient implements DevToolsApi { @@ -82,7 +82,31 @@ export class DevToolsClient implements DevToolsApi { throw await ResponseError.fromResponse(response); } - return response.json() as Promise; + return {}; + } + + public async cancelScheduledTask( + plugin: string, + taskId: string, + ): Promise { + const baseUrl = `${await this.discoveryApi.getBaseUrl(plugin)}/`; + const url = new URL( + `.backstage/scheduler/v1/tasks/${encodeURIComponent(taskId)}/cancel`, + baseUrl, + ); + + const response = await this.fetchApi.fetch(url.toString(), { + method: 'POST', + }); + + if (!response.ok) { + if (response.status === 404) { + throw new NotFoundError(`Task ${taskId} not found`); + } else if (response.status === 409) { + throw new ConflictError(`Task ${taskId} is not running`); + } + throw await ResponseError.fromResponse(response); + } } public async getExternalDependencies(): Promise< diff --git a/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx b/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx index 7517cf8ea5..af86b9b237 100644 --- a/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx +++ b/plugins/devtools/src/components/Content/ScheduledTasksContent/ScheduledTasksContent.tsx @@ -29,10 +29,11 @@ import { TableColumn, } from '@backstage/core-components'; import Alert from '@material-ui/lab/Alert'; -import { useScheduledTasks, useTriggerScheduledTask } from '../../../hooks'; +import { useScheduledTasks, useScheduledTasksOperations } from '../../../hooks'; import { TaskApiTasksResponse } from '@backstage/plugin-devtools-common/alpha'; import { alertApiRef, configApiRef, useApi } from '@backstage/core-plugin-api'; import RefreshIcon from '@material-ui/icons/Refresh'; +import StopIcon from '@material-ui/icons/Stop'; import NightsStay from '@material-ui/icons/NightsStay'; import ErrorIcon from '@material-ui/icons/Error'; import BlockIcon from '@material-ui/icons/Block'; @@ -105,7 +106,7 @@ export const ScheduledTasksContent = () => { configApi.getOptionalStringArray('devTools.scheduledTasks.plugins') || []; const [selectedPlugin, setSelectedPlugin] = useState(plugins[0] || ''); const { scheduledTasks, loading, error } = useScheduledTasks(selectedPlugin); - const { triggerTask, isTriggering, triggerError } = useTriggerScheduledTask(); + const { triggerTask, cancelTask, isLoading } = useScheduledTasksOperations(); const [inputValue, setInputValue] = useState(''); @@ -209,28 +210,52 @@ export const ScheduledTasksContent = () => { permission={devToolsTaskSchedulerCreatePermission} errorPage={} > - - { - triggerTask(selectedPlugin, rowData.taskId); - if (triggerError) { - alertApi.post({ - message: `Error triggering task ${rowData.taskId}: ${error}`, - severity: 'error', - }); - } else { - alertApi.post({ - message: `Successfully triggered task ${rowData.taskId}`, - severity: 'success', - }); - } - }} - > - - - + + + { + try { + await triggerTask(selectedPlugin, rowData.taskId); + alertApi.post({ + message: `Successfully triggered task ${rowData.taskId}`, + severity: 'success', + }); + } catch (e) { + alertApi.post({ + message: `Error triggering task ${rowData.taskId}: ${e.message}`, + severity: 'error', + }); + } + }} + > + + + + + { + try { + await cancelTask(selectedPlugin, rowData.taskId); + alertApi.post({ + message: `Successfully cancelled task ${rowData.taskId}`, + severity: 'success', + }); + } catch (e) { + alertApi.post({ + message: `Error cancelling task ${rowData.taskId}: ${e.message}`, + severity: 'error', + }); + } + }} + > + + + + ), sorting: false, @@ -263,7 +288,7 @@ export const ScheduledTasksContent = () => { )} /> - {loading && } + {loading && !scheduledTasks && } {error && ( { )} - {!loading && !error && ( + {scheduledTasks && ( { search: true, sorting: true, searchFieldAlignment: 'right', + padding: 'dense', }} columns={columns} data={scheduledTasks || []} diff --git a/plugins/devtools/src/hooks/index.ts b/plugins/devtools/src/hooks/index.ts index 105d03fb68..9d212bc3af 100644 --- a/plugins/devtools/src/hooks/index.ts +++ b/plugins/devtools/src/hooks/index.ts @@ -18,4 +18,4 @@ export { useConfig } from './useConfig'; export { useExternalDependencies } from './useExternalDependencies'; export { useInfo } from './useInfo'; export { useScheduledTasks } from './useScheduledTasks'; -export { useTriggerScheduledTask } from './useTriggerScheduledTask'; +export { useScheduledTasksOperations } from './useScheduledTasksOperations'; diff --git a/plugins/devtools/src/hooks/useTriggerScheduledTask.ts b/plugins/devtools/src/hooks/useScheduledTasksOperations.ts similarity index 66% rename from plugins/devtools/src/hooks/useTriggerScheduledTask.ts rename to plugins/devtools/src/hooks/useScheduledTasksOperations.ts index d26b7a8025..ff9d02b675 100644 --- a/plugins/devtools/src/hooks/useTriggerScheduledTask.ts +++ b/plugins/devtools/src/hooks/useScheduledTasksOperations.ts @@ -17,22 +17,40 @@ import { useState, useCallback } from 'react'; import { devToolsApiRef } from '../api'; import { useApi } from '@backstage/core-plugin-api'; -export const useTriggerScheduledTask = () => { +export const useScheduledTasksOperations = () => { const api = useApi(devToolsApiRef); - const [isTriggering, setIsTriggering] = useState(false); + const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); const triggerTask = useCallback( async (plugin: string, taskId: string) => { - setIsTriggering(true); + setIsLoading(true); setError(undefined); try { await api.triggerScheduledTask(plugin, taskId); } catch (e) { setError(e); + throw e; } finally { - setIsTriggering(false); + setIsLoading(false); + } + }, + [api], + ); + + const cancelTask = useCallback( + async (plugin: string, taskId: string) => { + setIsLoading(true); + setError(undefined); + + try { + await api.cancelScheduledTask(plugin, taskId); + } catch (e) { + setError(e); + throw e; + } finally { + setIsLoading(false); } }, [api], @@ -40,7 +58,8 @@ export const useTriggerScheduledTask = () => { return { triggerTask, - isTriggering, - triggerError: error?.message, + cancelTask, + isLoading, + error: error?.message, }; };