@@ -20,6 +20,10 @@ import {
|
||||
DevToolsInfo,
|
||||
ExternalDependency,
|
||||
} from '@backstage/plugin-devtools-common';
|
||||
import {
|
||||
ScheduledTasks,
|
||||
TriggerScheduledTask,
|
||||
} from '@backstage/plugin-devtools-common/alpha';
|
||||
|
||||
export const devToolsApiRef = createApiRef<DevToolsApi>({
|
||||
id: 'plugin.devtools.service',
|
||||
@@ -29,4 +33,9 @@ export interface DevToolsApi {
|
||||
getConfig(): Promise<ConfigInfo | undefined>;
|
||||
getExternalDependencies(): Promise<ExternalDependency[] | undefined>;
|
||||
getInfo(): Promise<DevToolsInfo | undefined>;
|
||||
getScheduledTasksByPlugin(plugin: string): Promise<ScheduledTasks>;
|
||||
triggerScheduledTask(
|
||||
plugin: string,
|
||||
taskId: string,
|
||||
): Promise<TriggerScheduledTask>;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ import {
|
||||
DevToolsInfo,
|
||||
ExternalDependency,
|
||||
} from '@backstage/plugin-devtools-common';
|
||||
import {
|
||||
ScheduledTasks,
|
||||
TriggerScheduledTask,
|
||||
} from '@backstage/plugin-devtools-common/alpha';
|
||||
import { ResponseError } from '@backstage/errors';
|
||||
import { DevToolsApi } from './DevToolsApi';
|
||||
|
||||
@@ -42,6 +46,45 @@ export class DevToolsClient implements DevToolsApi {
|
||||
return configInfo;
|
||||
}
|
||||
|
||||
public async getScheduledTasksByPlugin(
|
||||
plugin: string,
|
||||
): Promise<ScheduledTasks> {
|
||||
const baseUrl = `${await this.discoveryApi.getBaseUrl(plugin)}/`;
|
||||
const url = new URL('.backstage/scheduler/v1/tasks', baseUrl);
|
||||
|
||||
const response = await this.fetchApi.fetch(url.toString());
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
const scheduledTasks = await response.json();
|
||||
return {
|
||||
scheduledTasks: scheduledTasks.tasks,
|
||||
};
|
||||
}
|
||||
|
||||
public async triggerScheduledTask(
|
||||
plugin: string,
|
||||
taskId: string,
|
||||
): Promise<TriggerScheduledTask> {
|
||||
const baseUrl = `${await this.discoveryApi.getBaseUrl(plugin)}/`;
|
||||
const url = new URL(
|
||||
`.backstage/scheduler/v1/tasks/${taskId}/trigger`,
|
||||
baseUrl,
|
||||
);
|
||||
|
||||
const response = await this.fetchApi.fetch(url.toString(), {
|
||||
method: 'POST',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
}
|
||||
|
||||
return response.json() as Promise<TriggerScheduledTask>;
|
||||
}
|
||||
|
||||
public async getExternalDependencies(): Promise<
|
||||
ExternalDependency[] | undefined
|
||||
> {
|
||||
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { TaskApiTasksResponse } from '@backstage/plugin-devtools-common/alpha';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
detailPanel: {
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
detailLabel: {
|
||||
fontWeight: 'bold',
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.palette.error.main,
|
||||
marginRight: theme.spacing(1),
|
||||
fontSize: '1.2rem',
|
||||
},
|
||||
detailPanelAlert: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const ScheduledTaskDetailPanel = ({
|
||||
rowData,
|
||||
}: {
|
||||
rowData: TaskApiTasksResponse;
|
||||
}) => {
|
||||
const classes = useStyles();
|
||||
const lastRunError = rowData.taskState?.lastRunError;
|
||||
|
||||
const DetailItem = ({ title, value }: { title: string; value: any }) => (
|
||||
<>
|
||||
<Grid item xs={3}>
|
||||
<Typography variant="subtitle2" className={classes.detailLabel}>
|
||||
{title}:
|
||||
</Typography>
|
||||
</Grid>
|
||||
<Grid item xs={9}>
|
||||
<Typography variant="body2" component="code">
|
||||
{value || 'N/A'}
|
||||
</Typography>
|
||||
</Grid>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box className={classes.detailPanel}>
|
||||
{lastRunError && (
|
||||
<Alert severity="error" className={classes.detailPanelAlert}>
|
||||
<strong>Last Run Error:</strong> {lastRunError}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Grid container spacing={1}>
|
||||
<DetailItem title="Worker State" value={rowData.workerState?.status} />
|
||||
<DetailItem
|
||||
title="Frequency (Cadence)"
|
||||
value={rowData.settings?.cadence?.toString()}
|
||||
/>
|
||||
<DetailItem title="Scope" value={rowData.scope} />
|
||||
<DetailItem
|
||||
title="Started At"
|
||||
value={
|
||||
rowData.taskState?.status === 'running' &&
|
||||
rowData.taskState.startedAt
|
||||
? new Date(rowData.taskState.startedAt).toLocaleString()
|
||||
: 'N/A'
|
||||
}
|
||||
/>
|
||||
<DetailItem
|
||||
title="Times Out At"
|
||||
value={
|
||||
rowData.taskState?.status === 'running' &&
|
||||
rowData.taskState.timesOutAt
|
||||
? new Date(rowData.taskState.timesOutAt).toLocaleString()
|
||||
: 'N/A'
|
||||
}
|
||||
/>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
+307
@@ -0,0 +1,307 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import Box from '@material-ui/core/Box';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Autocomplete from '@material-ui/lab/Autocomplete';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import { makeStyles, createStyles, Theme } from '@material-ui/core/styles';
|
||||
import {
|
||||
ErrorPanel,
|
||||
Progress,
|
||||
Table,
|
||||
TableColumn,
|
||||
} from '@backstage/core-components';
|
||||
import Alert from '@material-ui/lab/Alert';
|
||||
import { useScheduledTasks, useTriggerScheduledTask } 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 NightsStay from '@material-ui/icons/NightsStay';
|
||||
import ErrorIcon from '@material-ui/icons/Error';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import { ScheduledTaskDetailPanel } from './ScheduledTaskDetailedPanel';
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { devToolsTaskSchedulerCreatePermission } from '@backstage/plugin-devtools-common/alpha';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) =>
|
||||
createStyles({
|
||||
paperStyle: {
|
||||
display: 'flex',
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
flexContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
padding: 0,
|
||||
},
|
||||
formControl: {
|
||||
minWidth: 240,
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
detailPanel: {
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
detailLabel: {
|
||||
fontWeight: 'bold',
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
errorIcon: {
|
||||
color: theme.palette.error.main,
|
||||
marginRight: theme.spacing(1),
|
||||
fontSize: '1.2rem',
|
||||
},
|
||||
detailPanelAlert: {
|
||||
marginBottom: theme.spacing(2),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
const StatusDisplay = ({
|
||||
icon,
|
||||
text,
|
||||
}: {
|
||||
icon: React.ReactNode;
|
||||
text: string;
|
||||
}) => (
|
||||
<Box display="flex" alignItems="center">
|
||||
{icon}
|
||||
<Typography variant="body2" style={{ marginLeft: 8 }}>
|
||||
{text}
|
||||
</Typography>
|
||||
</Box>
|
||||
);
|
||||
|
||||
/** @public */
|
||||
export const ScheduledTasksContent = () => {
|
||||
const classes = useStyles();
|
||||
const configApi = useApi(configApiRef);
|
||||
const alertApi = useApi(alertApiRef);
|
||||
const plugins =
|
||||
configApi.getOptionalStringArray('devTools.scheduledTasks.plugins') || [];
|
||||
const [selectedPlugin, setSelectedPlugin] = useState(plugins[0] || '');
|
||||
const { scheduledTasks, loading, error } = useScheduledTasks(selectedPlugin);
|
||||
const { triggerTask, isTriggering, triggerError } = useTriggerScheduledTask();
|
||||
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
|
||||
const handleAutocompleteChange = (_event: any, newValue: string | null) => {
|
||||
setSelectedPlugin(newValue || '');
|
||||
};
|
||||
|
||||
const handleCommitChange = () => {
|
||||
if (inputValue !== selectedPlugin) {
|
||||
setSelectedPlugin(inputValue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleCommitChange();
|
||||
// Prevent Autocomplete's default behavior (which might select a filtered item)
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
if (!plugins || plugins.length === 0) {
|
||||
return (
|
||||
<Alert severity="info">
|
||||
No plugins configured for scheduled tasks. Please configure
|
||||
`devTools.scheduledTasks.plugins` in app-config.yaml.
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
const columns: TableColumn<TaskApiTasksResponse>[] = [
|
||||
{
|
||||
title: 'Task ID',
|
||||
field: 'taskId',
|
||||
width: '35%',
|
||||
render: (rowData: TaskApiTasksResponse) => {
|
||||
const errorIconStyle: React.CSSProperties = {
|
||||
color: '#f44336',
|
||||
marginRight: '8px',
|
||||
fontSize: '1.2rem',
|
||||
verticalAlign: 'middle',
|
||||
};
|
||||
|
||||
return (
|
||||
<Box display="flex" alignItems="center">
|
||||
{rowData.taskState?.lastRunError && (
|
||||
<ErrorIcon style={errorIconStyle} />
|
||||
)}
|
||||
<Typography>{rowData.taskId}</Typography>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Status',
|
||||
field: 'taskState.status',
|
||||
width: '15%',
|
||||
render: (rowData: TaskApiTasksResponse) => {
|
||||
const status = rowData.taskState?.status;
|
||||
|
||||
if (status === 'idle') {
|
||||
return (
|
||||
<StatusDisplay icon={<NightsStay fontSize="small" />} text="Idle" />
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'running') {
|
||||
return (
|
||||
<StatusDisplay
|
||||
icon={<CircularProgress color="inherit" size="30px" />}
|
||||
text="Running"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <Typography variant="body2">{status || 'N/A'}</Typography>;
|
||||
},
|
||||
},
|
||||
{
|
||||
title: 'Last Run',
|
||||
field: 'taskState.lastRunEndedAt',
|
||||
width: '25%',
|
||||
render: (rowData: TaskApiTasksResponse) =>
|
||||
rowData.taskState?.lastRunEndedAt
|
||||
? new Date(rowData.taskState.lastRunEndedAt).toLocaleString()
|
||||
: 'N/A',
|
||||
},
|
||||
{
|
||||
title: 'Next Run',
|
||||
width: '15%',
|
||||
render: (rowData: TaskApiTasksResponse) =>
|
||||
rowData.taskState?.status === 'idle' && rowData.taskState.startsAt
|
||||
? new Date(rowData.taskState.startsAt).toLocaleString()
|
||||
: 'N/A',
|
||||
},
|
||||
{
|
||||
title: 'Actions',
|
||||
render: (rowData: TaskApiTasksResponse) => (
|
||||
<RequirePermission permission={devToolsTaskSchedulerCreatePermission}>
|
||||
<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>
|
||||
</RequirePermission>
|
||||
),
|
||||
sorting: false,
|
||||
width: '10%',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Autocomplete
|
||||
className={classes.formControl}
|
||||
classes={{ root: classes.formControl }}
|
||||
freeSolo
|
||||
options={plugins}
|
||||
value={selectedPlugin}
|
||||
inputValue={inputValue}
|
||||
onChange={handleAutocompleteChange}
|
||||
onInputChange={(_event, newInputValue) => {
|
||||
setInputValue(newInputValue);
|
||||
}}
|
||||
renderInput={params => (
|
||||
<TextField
|
||||
{...params}
|
||||
label="Select Plugin"
|
||||
variant="outlined"
|
||||
onKeyDown={handleKeyDown}
|
||||
onBlur={handleCommitChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
{loading && <Progress />}
|
||||
|
||||
{error && (
|
||||
<ErrorPanel
|
||||
error={new Error(`No scheduled tasks found for "${selectedPlugin}"`)}
|
||||
title="No Scheduled Tasks Found"
|
||||
>
|
||||
<Typography variant="body2">
|
||||
The plugin ID "{selectedPlugin}" doesn't have any scheduled tasks or
|
||||
may contain a typo.
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ marginTop: 8 }}>
|
||||
Please verify:
|
||||
</Typography>
|
||||
<ul>
|
||||
<li>
|
||||
<Typography variant="body2">
|
||||
The plugin ID is spelled correctly
|
||||
</Typography>
|
||||
</li>
|
||||
<li>
|
||||
<Typography variant="body2">
|
||||
The plugin has registered scheduled tasks
|
||||
</Typography>
|
||||
</li>
|
||||
</ul>
|
||||
</ErrorPanel>
|
||||
)}
|
||||
|
||||
{!loading && !error && (
|
||||
<Table
|
||||
title={`Scheduled Tasks (${selectedPlugin})`}
|
||||
options={{
|
||||
paging: true,
|
||||
search: true,
|
||||
sorting: true,
|
||||
searchFieldAlignment: 'right',
|
||||
}}
|
||||
columns={columns}
|
||||
data={scheduledTasks || []}
|
||||
emptyContent={
|
||||
<Alert severity="info">
|
||||
No scheduled tasks found for {selectedPlugin}.
|
||||
</Alert>
|
||||
}
|
||||
detailPanel={({ rowData }) => {
|
||||
return <ScheduledTaskDetailPanel rowData={rowData} />;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
+205
@@ -0,0 +1,205 @@
|
||||
{
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "cool-provider",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT24H",
|
||||
"timeoutAfterDuration": "PT60M"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "running",
|
||||
"startedAt": "2025-10-31T21:02:36.461+00:00",
|
||||
"timesOutAt": "2025-10-31T22:02:36.461+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T19:57:15.674+00:00",
|
||||
"lastRunError": "{\"name\":\"error\",\"message\":\"insert into \\\"refresh_state\\\" (\\\"entity_id\\\", \\\"entity_ref\\\", \\\"errors\\\", \\\"last_discovery_at\\\", \\\"location_key\\\", \\\"next_update_at\\\", \\\"unprocessed_entity\\\", \\\"unprocessed_hash\\\") values ($1, $2, $3, CURRENT_TIMESTAMP, $4, CURRENT_TIMESTAMP, $5, $6), ($7, $8, $9, CURRENT_TIMESTAMP, $10, CURRENT_TIMESTAMP, $11, $12), ($13, $14, $15, CURRENT_TIMESTAMP, $16, CURRENT_TIMESTAMP, $17, $18), ($19, $20, $21, CURRENT_TIMESTAMP, $22, CURRENT_TIMESTAMP, $23, $24), ($25, $26, $27, CURRENT_TIMESTAMP, $28, CURRENT_TIMESTAMP, $29, $30), ($31, $32, $33, CURRENT_TIMESTAMP, $34, CURRENT_TIMESTAMP, $35, $36), ($37, $38, $39, CURRENT_TIMESTAMP, $40, CURRENT_TIMESTAMP, $41, $42), ($43, $44, $45, CURRENT_TIMESTAMP, $46, CURRENT_TIMESTAMP, $47, $48), ($49, $50, $51, CURRENT_TIMESTAMP, $52, CURRENT_TIMESTAMP, $53, $54), ($55, $56, $57, CURRENT_TIMESTAMP, $58, CURRENT_TIMESTAMP, $59, $60), ($61, $62, $63, CURRENT_TIMESTAMP, $64, CURRENT_TIMESTAMP, $65, $66), ($67, $68, $69, CURRENT_TIMESTAMP, $70, CURRENT_TIMESTAMP, $71, $72), ($73, $74, $75, CURRENT_TIMESTAMP, $76, CURRENT_TIMESTAMP, $77, $78), ($79, $80, $81, CURRENT_TIMESTAMP, $82, CURRENT_TIMESTAMP, $83, $84), ($85, $86, $87, CURRENT_TIMESTAMP, $88, CURRENT_TIMESTAMP, $89, $90), ($91, $92, $93, CURRENT_TIMESTAMP, $94, CURRENT_TIMESTAMP, $95, $96), ($97, $98, $99, CURRENT_TIMESTAMP, $100, CURRENT_TIMESTAMP, $101, $102), ($103, $104, $105, CURRENT_TIMESTAMP, $106, CURRENT_TIMESTAMP, $107, $108), ($109, $110, $111, CURRENT_TIMESTAMP, $112, CURRENT_TIMESTAMP, $113, $114), ($115, $116, $117, CURRENT_TIMESTAMP, $118, CURRENT_TIMESTAMP, $119, $120), ($121, $122, $123, CURRENT_TIMESTAMP, $124, CURRENT_TIMESTAMP, $125, $126), ($127, $128, $129, CURRENT_TIMESTAMP, $130, CURRENT_TIMESTAMP, $131, $132), ($133, $134, $135, CURRENT_TIMESTAMP, $136, CURRENT_TIMESTAMP, $137, $138), ($139, $140, $141, CURRENT_TIMESTAMP, $142, CURRENT_TIMESTAMP, $143, $144), ($145, $146, $147, CURRENT_TIMESTAMP, $148, CURRENT_TIMESTAMP, $149, $150), ($151, $152, $153, CURRENT_TIMESTAMP, $154, CURRENT_TIMESTAMP, $155, $156), ($157, $158, $159, CURRENT_TIMESTAMP, $160, CURRENT_TIMESTAMP, $161, $162), ($163, $164, $165, CURRENT_TIMESTAMP, $166, CURRENT_TIMESTAMP, $167, $168), ($169, $170, $171, CURRENT_TIMESTAMP, $172, CURRENT_TIMESTAMP, $173, $174), ($175, $176, $177, CURRENT_TIMESTAMP, $178, CURRENT_TIMESTAMP, $179, $180), ($181, $182, $183, CURRENT_TIMESTAMP, $184, CURRENT_TIMESTAMP, $185, $186), ($187, $188, $189, CURRENT_TIMESTAMP, $190, CURRENT_TIMESTAMP, $191, $192), ($193, $194, $195, CURRENT_TIMESTAMP, $196, CURRENT_TIMESTAMP, $197, $198), ($199, $200, $201, CURRENT_TIMESTAMP, $202, CURRENT_TIMESTAMP, $203, $204), ($205, $206, $207, CURRENT_TIMESTAMP, $208, CURRENT_TIMESTAMP, $209, $210), ($211, $212, $213, CURRENT_TIMESTAMP, $214, CURRENT_TIMESTAMP, $215, $216), ($217, $218, $219, CURRENT_TIMESTAMP, $220, CURRENT_TIMESTAMP, $221, $222), ($223, $224, $225, CURRENT_TIMESTAMP, $226, CURRENT_TIMESTAMP, $227, $228), ($229, $230, $231, CURRENT_TIMESTAMP, $232, CURRENT_TIMESTAMP, $233, $234), ($235, $236, $237, CURRENT_TIMESTAMP, $238, CURRENT_TIMESTAMP, $239, $240), ($241, $242, $243, CURRENT_TIMESTAMP, $244, CURRENT_TIMESTAMP, $245, $246), ($247, $248, $249, CURRENT_TIMESTAMP, $250, CURRENT_TIMESTAMP, $251, $252), ($253, $254, $255, CURRENT_TIMESTAMP, $256, CURRENT_TIMESTAMP, $257, $258), ($259, $260, $261, CURRENT_TIMESTAMP, $262, CURRENT_TIMESTAMP, $263, $264), ($265, $266, $267, CURRENT_TIMESTAMP, $268, CURRENT_TIMESTAMP, $269, $270), ($271, $272, $273, CURRENT_TIMESTAMP, $274, CURRENT_TIMESTAMP, $275, $276), ($277, $278, $279, CURRENT_TIMESTAMP, $280, CURRENT_TIMESTAMP, $281, $282), ($283, $284, $285, CURRENT_TIMESTAMP, $286, CURRENT_TIMESTAMP, $287, $288), ($289, $290, $291, CURRENT_TIMESTAMP, $292, CURRENT_TIMESTAMP, $293, $294), ($295, $296, $297, CURRENT_TIMESTAMP, $298, CURRENT_TIMESTAMP, $299, $300) - value too long for type character varying(255)\",\"length\":99,\"severity\":\"ERROR\",\"code\":\"22001\",\"file\":\"varchar.c\",\"line\":\"637\",\"routine\":\"varchar\"}"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "running"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "some-provider",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT24H",
|
||||
"timeoutAfterDuration": "PT24H"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-11-01T21:02:34.850+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T21:02:35.556+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "three-provider",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT30M",
|
||||
"timeoutAfterDuration": "PT2M"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-10-31T21:16:04.145+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T20:46:09.184+00:00",
|
||||
"lastRunError": "{\"name\":\"TypeError\",\"message\":\"String.prototype.replaceAll called with a non-global RegExp argument\"}"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "a-provider",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT60M",
|
||||
"timeoutAfterDuration": "PT24H"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-10-31T21:16:04.152+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T20:16:08.713+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "github-provider:provider123:refresh",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT24H",
|
||||
"timeoutAfterDuration": "PT1H"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-11-01T18:16:04.153+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T18:18:56.176+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "github-provider:provider123:refresh",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT24H",
|
||||
"timeoutAfterDuration": "PT1H"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-11-01T18:16:04.156+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T18:16:04.264+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "github-provider:provider234:refresh",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT24H",
|
||||
"timeoutAfterDuration": "PT1H"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-11-01T18:16:04.157+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T18:16:08.564+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "github-provider:provider567:refresh",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT24H",
|
||||
"timeoutAfterDuration": "PT1H"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-11-01T18:16:04.154+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T18:16:06.978+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "github-provider:provider910:refresh",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT24H",
|
||||
"timeoutAfterDuration": "PT1H"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-11-01T18:16:04.160+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T18:16:08.573+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "github-provider:provider000:refresh",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT24H",
|
||||
"timeoutAfterDuration": "PT1H"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-11-01T18:16:04.161+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T18:18:54.339+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
},
|
||||
{
|
||||
"taskId": "catalog_orphan_cleanup",
|
||||
"pluginId": "catalog",
|
||||
"scope": "global",
|
||||
"settings": {
|
||||
"version": 2,
|
||||
"cadence": "PT30S",
|
||||
"timeoutAfterDuration": "PT24S"
|
||||
},
|
||||
"taskState": {
|
||||
"status": "idle",
|
||||
"startsAt": "2025-10-31T21:06:35.672+00:00",
|
||||
"lastRunEndedAt": "2025-10-31T21:06:09.106+00:00"
|
||||
},
|
||||
"workerState": {
|
||||
"status": "idle"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export { ScheduledTasksContent } from './ScheduledTasksContent';
|
||||
export { ScheduledTaskDetailPanel } from './ScheduledTaskDetailedPanel';
|
||||
@@ -17,3 +17,4 @@
|
||||
export * from './ConfigContent';
|
||||
export * from './InfoContent';
|
||||
export * from './ExternalDependenciesContent';
|
||||
export * from './ScheduledTasksContent';
|
||||
|
||||
@@ -20,9 +20,11 @@ import {
|
||||
} from '@backstage/plugin-devtools-common';
|
||||
|
||||
import { ConfigContent } from '../Content';
|
||||
import { devToolsTaskSchedulerReadPermission } from '@backstage/plugin-devtools-common/alpha';
|
||||
import { DevToolsLayout } from '../DevToolsLayout';
|
||||
import { InfoContent } from '../Content';
|
||||
import { RequirePermission } from '@backstage/plugin-permission-react';
|
||||
import { ScheduledTasksContent } from '../Content/ScheduledTasksContent';
|
||||
import { DevToolsPageProps } from '../DevToolsPage';
|
||||
|
||||
/** @public */
|
||||
@@ -38,6 +40,11 @@ export const DefaultDevToolsPage = ({ extensions }: DevToolsPageProps) => (
|
||||
<ConfigContent />
|
||||
</RequirePermission>
|
||||
</DevToolsLayout.Route>
|
||||
<DevToolsLayout.Route path="scheduled-tasks" title="Scheduled Tasks">
|
||||
<RequirePermission permission={devToolsTaskSchedulerReadPermission}>
|
||||
<ScheduledTasksContent />
|
||||
</RequirePermission>
|
||||
</DevToolsLayout.Route>
|
||||
{extensions?.map((extension, index) => (
|
||||
<DevToolsLayout.Route
|
||||
key={`extension-${extension.path}-${index}`}
|
||||
|
||||
@@ -17,3 +17,5 @@
|
||||
export { useConfig } from './useConfig';
|
||||
export { useExternalDependencies } from './useExternalDependencies';
|
||||
export { useInfo } from './useInfo';
|
||||
export { useScheduledTasks } from './useScheduledTasks';
|
||||
export { useTriggerScheduledTask } from './useTriggerScheduledTask';
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { devToolsApiRef } from '../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
|
||||
export const useScheduledTasks = (plugin: string) => {
|
||||
const api = useApi(devToolsApiRef);
|
||||
|
||||
const {
|
||||
value,
|
||||
loading,
|
||||
error: asyncError,
|
||||
} = useAsync(async () => {
|
||||
return api.getScheduledTasksByPlugin(plugin);
|
||||
}, [api, plugin]);
|
||||
|
||||
if (asyncError) {
|
||||
return {
|
||||
scheduledTasks: undefined,
|
||||
loading: false,
|
||||
error: asyncError.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
scheduledTasks: value?.scheduledTasks,
|
||||
loading,
|
||||
error: value?.error,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2025 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { useState, useCallback } from 'react';
|
||||
import { devToolsApiRef } from '../api';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
|
||||
export const useTriggerScheduledTask = () => {
|
||||
const api = useApi(devToolsApiRef);
|
||||
const [isTriggering, setIsTriggering] = useState(false);
|
||||
const [error, setError] = useState<Error | undefined>();
|
||||
|
||||
const triggerTask = useCallback(
|
||||
async (plugin: string, taskId: string) => {
|
||||
setIsTriggering(true);
|
||||
setError(undefined);
|
||||
|
||||
try {
|
||||
await api.triggerScheduledTask(plugin, taskId);
|
||||
} catch (e) {
|
||||
setError(e);
|
||||
} finally {
|
||||
setIsTriggering(false);
|
||||
}
|
||||
},
|
||||
[api],
|
||||
);
|
||||
|
||||
return {
|
||||
triggerTask,
|
||||
isTriggering,
|
||||
triggerError: error?.message,
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user