Filter Pipelines page to only show what the user thinks as pipelines (#7495)

# Description of Changes
Currently, the Pipelines page shows all backend Policies, which was the
desired behaviour when we first designed this, but as it's come along,
it doesn't feel right anymore. This adds a filter so the Pipelines table
only shows things that have been defined by the user as a New Pipeline,
so not Policies etc.

## Before
<img width="1510" height="789" alt="image"
src="https://github.com/user-attachments/assets/5aebb065-3d42-4483-be3c-253fd8918d49"
/>

## After
<img width="1512" height="790" alt="image"
src="https://github.com/user-attachments/assets/2966a0de-ec9f-43e6-bb1d-c4aeb30dfbf8"
/>
This commit is contained in:
James Brunton
2026-08-14 13:11:02 +00:00
committed by GitHub
parent 588afb6306
commit 6f2b829f72
3 changed files with 86 additions and 14 deletions
@@ -20,23 +20,28 @@ import stirling.software.proprietary.policy.source.SourceStore;
import stirling.software.proprietary.policy.store.PolicyStore;
/**
* Builds the Pipelines overview: every policy the caller's team owns, each annotated with its
* referenced sources (resolved to display names), its pipeline steps, and a trigger/output summary.
* Source names are resolved from the team's sources in memory rather than persisted on the policy,
* so the view always reflects the live source set. This is the "all pipelines" admin surface; the
* user-facing Policies page builds only a friendly subset of the same backend policies.
* Builds the Pipelines overview: one row per policy the caller's team built on the Pipelines page,
* with its sources resolved to live display names, its steps, and a trigger/output summary.
* Frontend/catalogue policies (marked by a {@code categoryId} in their output options) belong to
* the user-facing Policies page and are excluded; a folder-watch trigger is not a signal.
*/
@Service
@RequiredArgsConstructor
public class PolicyOverviewService {
// Output-options key marking a frontend/catalogue policy (set by the Policies page and seeder).
private static final String CATEGORY_OPTION = "categoryId";
private final PolicyStore policyStore;
private final SourceStore sourceStore;
private final PolicyAccessGuard policyAccessGuard;
private final SourceAccessGuard sourceAccessGuard;
public PoliciesOverviewResponse overview() {
List<Policy> policies = policyAccessGuard.visibleFrom(policyStore);
List<Policy> policies =
policyAccessGuard.visibleFrom(policyStore).stream()
.filter(PolicyOverviewService::isPipeline)
.toList();
Map<String, String> sourceNames = sourceNames();
List<PolicyView> views =
@@ -50,6 +55,18 @@ public class PolicyOverviewService {
return new PoliciesOverviewResponse(buildKpis(policies), views);
}
private static boolean isPipeline(Policy policy) {
return !isCataloguePolicy(policy);
}
/** A frontend/catalogue policy, marked by a {@code categoryId} in its output options. */
private static boolean isCataloguePolicy(Policy policy) {
OutputSpec output = policy.output();
return output != null
&& output.options().get(CATEGORY_OPTION) instanceof String category
&& !category.isBlank();
}
/** Display names for every source the caller's team can see, keyed by source id. */
private Map<String, String> sourceNames() {
Map<String, String> names = new HashMap<>();
@@ -28,9 +28,11 @@ import stirling.software.proprietary.policy.store.InProcessPolicyStore;
import stirling.software.proprietary.policy.store.PolicyStore;
/**
* Tests for {@link PolicyOverviewService}: every policy appears once with its sources resolved to
* names, its steps and trigger/output summarised, and the KPI strip counting active vs paused.
* Login is disabled so the team guards pass everything through.
* Tests for {@link PolicyOverviewService}: every Pipelines-page policy appears once with its
* sources resolved to names, its steps and trigger/output summarised, and the KPI strip counting
* active vs paused. Frontend/catalogue policies (owned by the Policies page) are excluded, while a
* pipeline that uses a folder-watch trigger stays. Login is disabled so the team guards pass
* everything through.
*/
class PolicyOverviewServiceTest {
@@ -95,6 +97,51 @@ class PolicyOverviewServiceTest {
assertEquals(List.of(2L, 1L, 1L), response.kpis().stream().map(PolicyKpi::value).toList());
}
@Test
void excludesCataloguePoliciesButKeepsFolderWatchPipelines() {
Source inbox = source("Inbox", "/inbox");
// A hand-built pipeline: shows.
policyStore.save(
new Policy(
null,
"Compress pipeline",
"owner",
true,
List.of(),
List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())),
OutputSpec.inline()));
// A folder-watch pipeline is still a pipeline: shows.
policyStore.save(
new Policy(
null,
"Inbox watcher",
"owner",
true,
List.of(
new PipelineInput(
inbox.id(), new TriggerConfig("folder-watch", Map.of()))),
List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())),
OutputSpec.inline()));
// A frontend/catalogue policy (categoryId in output options): hidden.
policyStore.save(
new Policy(
null,
"Classification Policy",
"system",
true,
List.of(),
List.of(new PipelineStep("/api/v1/ai/tools/classify-and-label", Map.of())),
new OutputSpec("inline", Map.of("categoryId", "classification"))));
PoliciesOverviewResponse response = service.overview();
assertEquals(
List.of("Compress pipeline", "Inbox watcher"),
response.pipelines().stream().map(PolicyView::name).toList());
// KPIs count both visible pipelines, not the hidden catalogue policy.
assertEquals(List.of(2L, 2L, 0L), response.kpis().stream().map(PolicyKpi::value).toList());
}
@Test
void anUnresolvedSourceFallsBackToItsId() {
policyStore.save(
@@ -152,9 +152,16 @@ function toView(policy: StoredPolicy): PipelineView {
};
}
function buildKpis(): PipelineKpi[] {
const total = store.length;
const active = store.filter((p) => p.enabled).length;
// Mirrors the backend PolicyOverviewService: hide frontend/catalogue policies (a categoryId in
// output options). A folder-watch trigger is still a normal pipeline and stays.
function isPipeline(policy: StoredPolicy): boolean {
const categoryId = policy.output?.options?.categoryId;
return !(typeof categoryId === "string" && categoryId.length > 0);
}
function buildKpis(policies: StoredPolicy[]): PipelineKpi[] {
const total = policies.length;
const active = policies.filter((p) => p.enabled).length;
return [
{ value: total, description: "pipelines" },
{ value: active, description: "running automatically" },
@@ -163,10 +170,11 @@ function buildKpis(): PipelineKpi[] {
}
function buildOverview(): PipelinesOverviewResponse {
const pipelines = store
const visible = store.filter(isPipeline);
const pipelines = visible
.map(toView)
.sort((a, b) => a.name.localeCompare(b.name));
return { kpis: buildKpis(), pipelines };
return { kpis: buildKpis(visible), pipelines };
}
export const pipelinesHandlers = [