Merge pull request #33298 from backstage/freben/devtools-cancel-task

devtools: Add cancel task operation to scheduled tasks
This commit is contained in:
Fredrik Adelöw
2026-03-12 10:02:54 +01:00
committed by GitHub
7 changed files with 112 additions and 35 deletions
@@ -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.
+1
View File
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
devToolsTaskSchedulerReadPermission,
devToolsTaskSchedulerCreatePermission,
+1
View File
@@ -38,4 +38,5 @@ export interface DevToolsApi {
plugin: string,
taskId: string,
): Promise<TriggerScheduledTask>;
cancelScheduledTask(plugin: string, taskId: string): Promise<void>;
}
+26 -2
View File
@@ -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<TriggerScheduledTask>;
return {};
}
public async cancelScheduledTask(
plugin: string,
taskId: string,
): Promise<void> {
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<
@@ -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,
@@ -263,7 +288,7 @@ export const ScheduledTasksContent = () => {
)}
/>
{loading && <Progress />}
{loading && !scheduledTasks && <Progress />}
{error && (
<ErrorPanel
@@ -292,7 +317,7 @@ export const ScheduledTasksContent = () => {
</ErrorPanel>
)}
{!loading && !error && (
{scheduledTasks && (
<Table
title={`Scheduled Tasks (${selectedPlugin})`}
options={{
@@ -300,6 +325,7 @@ export const ScheduledTasksContent = () => {
search: true,
sorting: true,
searchFieldAlignment: 'right',
padding: 'dense',
}}
columns={columns}
data={scheduledTasks || []}
+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,
};
};