devtools: Add cancel task operation to scheduled tasks

Renames useTriggerScheduledTask to useScheduledTasksOperations and adds
cancelTask alongside triggerTask, with shared isLoading/error state. Adds
CancelScheduledTask type to devtools-common and cancelScheduledTask to the
DevToolsApi interface and client, hitting the existing scheduler cancel
endpoint. The ScheduledTasksContent UI now shows both trigger and cancel
buttons per task row.

Signed-off-by: Fredrik Adelöw <freben@spotify.com>
Made-with: Cursor
This commit is contained in:
Fredrik Adelöw
2026-03-11 16:47:24 +01:00
parent 57fee86920
commit 85f38c29d1
7 changed files with 108 additions and 31 deletions
+1
View File
@@ -18,6 +18,7 @@ export {
devToolsTaskSchedulerCreatePermission,
} from './permissions';
export type {
CancelScheduledTask,
ScheduledTasks,
TaskApiTasksResponse,
TriggerScheduledTask,
+5
View File
@@ -133,3 +133,8 @@ export type ScheduledTasks = {
export type TriggerScheduledTask = {
error?: string;
};
/** @alpha */
export type CancelScheduledTask = {
error?: string;
};
+5
View File
@@ -21,6 +21,7 @@ import {
ExternalDependency,
} from '@backstage/plugin-devtools-common';
import {
CancelScheduledTask,
ScheduledTasks,
TriggerScheduledTask,
} from '@backstage/plugin-devtools-common/alpha';
@@ -38,4 +39,8 @@ export interface DevToolsApi {
plugin: string,
taskId: string,
): Promise<TriggerScheduledTask>;
cancelScheduledTask(
plugin: string,
taskId: string,
): Promise<CancelScheduledTask>;
}
@@ -21,6 +21,7 @@ import {
ExternalDependency,
} from '@backstage/plugin-devtools-common';
import {
CancelScheduledTask,
ScheduledTasks,
TriggerScheduledTask,
} from '@backstage/plugin-devtools-common/alpha';
@@ -85,6 +86,27 @@ export class DevToolsClient implements DevToolsApi {
return response.json() as Promise<TriggerScheduledTask>;
}
public async cancelScheduledTask(
plugin: string,
taskId: string,
): Promise<CancelScheduledTask> {
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) {
throw await ResponseError.fromResponse(response);
}
return response.json() as Promise<CancelScheduledTask>;
}
public async getExternalDependencies(): Promise<
ExternalDependency[] | undefined
> {
@@ -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={<CreateNotAllowed />}
>
<Tooltip title="Run Task">
<IconButton
aria-label="Trigger"
disabled={isTriggering}
onClick={() => {
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',
});
}
}}
>
<RefreshIcon />
</IconButton>
</Tooltip>
<Box display="flex" justifyContent="center">
<Tooltip title="Run Task">
<IconButton
aria-label="Trigger"
disabled={isLoading}
onClick={async () => {
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',
});
}
}}
>
<RefreshIcon />
</IconButton>
</Tooltip>
<Tooltip title="Cancel Task">
<IconButton
aria-label="Cancel"
disabled={isLoading}
onClick={async () => {
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',
});
}
}}
>
<StopIcon />
</IconButton>
</Tooltip>
</Box>
</RequirePermission>
),
sorting: false,
+1 -1
View File
@@ -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';
@@ -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<Error | undefined>();
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,
};
};