Skip to content
8 changes: 8 additions & 0 deletions .features/pending/14809-sensor-previous-runs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Component: UI
Issues: 14809
Description: Add Previous Runs section to Sensor details page to display workflows triggered by sensors
Author: [puretension](https://github.com/puretension)

- Added Previous Runs section below Sensor editor tabs using `workflows.argoproj.io/sensor` label filtering
- Implemented identical UI pattern as CronWorkflow with WorkflowDetailsList component for consistency
- Fixed empty state handling with proper array length check to display triggered workflows correctly
26 changes: 25 additions & 1 deletion ui/src/sensors/sensor-details.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import {uiUrl} from '../shared/base';
import {ErrorNotice} from '../shared/components/error-notice';
import {Node} from '../shared/components/graph/types';
import {Loading} from '../shared/components/loading';
import {ZeroState} from '../shared/components/zero-state';
import {Context} from '../shared/context';
import {historyUrl} from '../shared/history';
import {Sensor} from '../shared/models';
import * as models from '../shared/models';
import {Sensor, Workflow} from '../shared/models';
import {services} from '../shared/services';
import {useCollectEvent} from '../shared/use-collect-event';
import {useEditableObject} from '../shared/use-editable-object';
import {useQueryParams} from '../shared/use-query-params';
import {WorkflowDetailsList} from '../workflows/components/workflow-details-list/workflow-details-list';
import {SensorEditor} from './sensor-editor';
import {SensorSidePanel} from './sensor-side-panel';

Expand All @@ -30,6 +33,8 @@ export function SensorDetails({match, location, history}: RouteComponentProps<an
const [namespace] = useState(match.params.namespace);
const [name] = useState(match.params.name);
const [tab, setTab] = useState<string>(queryParams.get('tab'));
const [workflows, setWorkflows] = useState<Workflow[]>([]);
const [columns, setColumns] = useState<models.Column[]>([]);

const {object: sensor, setObject: setSensor, resetObject: resetSensor, serialization, edited, lang, setLang} = useEditableObject<Sensor>();
const [selectedLogNode, setSelectedLogNode] = useState<Node>(queryParams.get('selectedLogNode'));
Expand Down Expand Up @@ -66,6 +71,16 @@ export function SensorDetails({match, location, history}: RouteComponentProps<an
.catch(setError);
}, [namespace, name]);

useEffect(() => {
(async () => {
const workflowList = await services.workflows.list(namespace, null, [`${models.labels.sensor}=${name}`], {limit: 50});
const workflowsInfo = await services.info.getInfo();

setWorkflows(workflowList.items);
setColumns(workflowsInfo.columns);
Comment on lines +76 to +80
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing error handling for the async workflow fetching. If services.workflows.list() or services.info.getInfo() fails, the error is not caught or displayed to the user. Add .catch(setError) or wrap in try-catch to handle potential errors, similar to the sensor fetching useEffect on line 66-72.

Suggested change
const workflowList = await services.workflows.list(namespace, null, [`${models.labels.sensor}=${name}`], {limit: 50});
const workflowsInfo = await services.info.getInfo();
setWorkflows(workflowList.items);
setColumns(workflowsInfo.columns);
try {
const workflowList = await services.workflows.list(namespace, null, [`${models.labels.sensor}=${name}`], {limit: 50});
const workflowsInfo = await services.info.getInfo();
setWorkflows(workflowList.items);
setColumns(workflowsInfo.columns);
setError(null);
} catch (err) {
setError(err);
}

Copilot uses AI. Check for mistakes.
})();
}, [namespace, name]);

useCollectEvent('openedSensorDetails');

const selected = (() => {
Expand Down Expand Up @@ -142,6 +157,15 @@ export function SensorDetails({match, location, history}: RouteComponentProps<an
onTabSelected={setTab}
/>
)}
<>
{!workflows || workflows.length === 0 ? (
Copy link

Copilot AI Nov 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition !workflows will always be false since workflows is initialized to [] on line 36. The check should be workflows.length === 0 only, matching the pattern in cron-workflow-details.tsx which checks !workflows where workflows is initialized to undefined.

Suggested change
{!workflows || workflows.length === 0 ? (
{workflows.length === 0 ? (

Copilot uses AI. Check for mistakes.
<ZeroState title='No previous runs'>
<p>No workflows have been triggered by this sensor yet.</p>
</ZeroState>
) : (
<WorkflowDetailsList workflows={workflows} columns={columns} />
)}
</>
</>
{!!selectedLogNode && (
<SensorSidePanel
Expand Down
1 change: 1 addition & 0 deletions ui/src/shared/models/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const labels = {
creatorEmail: 'workflows.argoproj.io/creator-email',
creatorPreferredUsername: 'workflows.argoproj.io/creator-preferred-username',
cronWorkflow: 'workflows.argoproj.io/cron-workflow',
sensor: 'workflows.argoproj.io/sensor',
workflowTemplate: 'workflows.argoproj.io/workflow-template'
};

Expand Down
19 changes: 15 additions & 4 deletions ui/src/workflow-templates/workflow-template-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ export function WorkflowTemplateList({match, location, history}: RouteComponentP
const [namespace, setNamespace] = useState(nsUtils.getNamespace(match.params.namespace) || '');
const [sidePanel, setSidePanel] = useState(queryParams.get('sidePanel') === 'true');
const [namePattern, setNamePattern] = useState('');
const [labels, setLabels] = useState([]);
const [labels, setLabels] = useState<string[]>(() => {
const savedOptions = storage.getItem('options', {});
const savedLabels = savedOptions.labels || [];
const labelQueryParam = queryParams.getAll('label');
return labelQueryParam.length > 0 ? labelQueryParam : savedLabels;
});

const [pagination, setPagination] = useState<Pagination>({
offset: queryParams.get('offset'),
limit: parseLimit(queryParams.get('limit')) || savedOptions.paginationLimit || 500
Expand All @@ -62,14 +68,19 @@ export function WorkflowTemplateList({match, location, history}: RouteComponentP
isFirstRender.current = false;
return;
}
storage.setItem('options', {labels}, {});
const params = new URLSearchParams();
labels?.forEach(label => params.append('label', label));
if (sidePanel) {
params.append('sidePanel', 'true');
}
history.push(
historyUrl('workflow-templates' + (nsUtils.getManagedNamespace() ? '' : '/{namespace}'), {
namespace,
sidePanel
extraSearchParams: params
})
);
}, [namespace, sidePanel]);

}, [namespace, sidePanel, labels.toString()]);
// internal state
const [error, setError] = useState<Error>();
const [templates, setTemplates] = useState<WorkflowTemplate[]>();
Expand Down
Loading