Merge branch 'master' into devtools-bui-config

This commit is contained in:
Aditya Kumar
2026-04-01 08:30:32 +05:30
2246 changed files with 88302 additions and 19645 deletions
+94 -30
View File
@@ -16,18 +16,25 @@
import {
createFrontendPlugin,
coreExtensionData,
discoveryApiRef,
fetchApiRef,
ApiBlueprint,
PageBlueprint,
NavItemBlueprint,
createExtensionInput,
coreExtensionData,
SubPageBlueprint,
} from '@backstage/frontend-plugin-api';
import { devToolsApiRef, DevToolsClient } from '../api';
import BuildIcon from '@material-ui/icons/Build';
import { Content } from '@backstage/core-components';
import { rootRouteRef } from '../routes';
import {
devToolsConfigReadPermission,
devToolsInfoReadPermission,
} from '@backstage/plugin-devtools-common';
import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha';
import { RequirePermission } from '@backstage/plugin-permission-react';
/** @alpha */
export const devToolsApi = ApiBlueprint.make({
@@ -45,34 +52,84 @@ export const devToolsApi = ApiBlueprint.make({
/** @alpha */
export const devToolsPage = PageBlueprint.makeWithOverrides({
inputs: {
contents: createExtensionInput(
[
coreExtensionData.reactElement,
coreExtensionData.routePath,
coreExtensionData.routeRef.optional(),
coreExtensionData.title,
],
{
optional: true,
},
),
},
factory(originalFactory, { inputs }) {
return originalFactory({
path: '/devtools',
routeRef: rootRouteRef,
loader: () => {
const contents = inputs.contents.map(content => ({
path: content.get(coreExtensionData.routePath),
title: content.get(coreExtensionData.title),
children: content.get(coreExtensionData.reactElement),
}));
return import('../components/DevToolsPage').then(m => (
<m.DevToolsPage contents={contents} />
));
},
const pages = [...inputs.pages].sort((left, right) => {
const leftPath = left.get(coreExtensionData.routePath);
const rightPath = right.get(coreExtensionData.routePath);
if (leftPath === 'info' && rightPath !== 'info') {
return -1;
}
if (leftPath !== 'info' && rightPath === 'info') {
return 1;
}
return 0;
});
return originalFactory(
{
path: '/devtools',
routeRef: rootRouteRef,
title: 'DevTools',
},
{
inputs: {
pages,
},
},
);
},
});
/** @alpha */
export const devToolsInfoPage = SubPageBlueprint.make({
name: 'info',
params: {
path: 'info',
title: 'Info',
loader: () =>
import('../components/Content').then(m => (
<Content>
<RequirePermission permission={devToolsInfoReadPermission}>
<m.InfoContent />
</RequirePermission>
</Content>
)),
},
});
/** @alpha */
export const devToolsConfigPage = SubPageBlueprint.make({
name: 'config',
params: {
path: 'config',
title: 'Config',
loader: () =>
import('../components/Content').then(m => (
<Content>
<RequirePermission permission={devToolsConfigReadPermission}>
<m.ConfigContent />
</RequirePermission>
</Content>
)),
},
});
/** @alpha */
export const devToolsScheduledTasksPage = SubPageBlueprint.make({
name: 'scheduled-tasks',
params: {
path: 'scheduled-tasks',
title: 'Scheduled Tasks',
loader: () =>
import('../components/Content').then(m => (
<Content>
<RequirePermission permission={devToolsTaskSchedulerReadPermission}>
<m.ScheduledTasksContent />
</RequirePermission>
</Content>
)),
},
});
@@ -89,10 +146,17 @@ export const devToolsNavItem = NavItemBlueprint.make({
export default createFrontendPlugin({
pluginId: 'devtools',
title: 'DevTools',
icon: <BuildIcon />,
icon: <BuildIcon fontSize="inherit" />,
info: { packageJson: () => import('../../package.json') },
routes: {
root: rootRouteRef,
},
extensions: [devToolsApi, devToolsPage, devToolsNavItem],
extensions: [
devToolsApi,
devToolsPage,
devToolsInfoPage,
devToolsConfigPage,
devToolsScheduledTasksPage,
devToolsNavItem,
],
});
+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 || []}
@@ -21,7 +21,7 @@ import {
import { ConfigContent } from '../Content';
import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha';
import { DevToolsLayout } from '../DevToolsLayout';
import { DevToolsLayout } from '../DevToolsLayout/DevToolsLayout';
import { InfoContent } from '../Content';
import { RequirePermission } from '@backstage/plugin-permission-react';
import { ScheduledTasksContent } from '../Content/ScheduledTasksContent';
+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,
};
};