mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-03 05:10:16 +03:00
Combine Policies and Pipelines pages (#7681)
# Description of Changes Combine the Policies and Pipelines pages into one, so we have the new concept of Policies as Pipelines that always run which the user cannot disable. What used to be Policies are now referred to as Templates, and they allow you to create a new Pipeline more easily with the simple UI. There's followup work to be done here to improve the template UIs because they've not been touched in a long time, but I've considered that beyond the scope of this merge. The only real changes I've made to them in this PR is that they have a toggle for whether they're policies, they now have a "Customise" button to kick you into the full Pipeline editor, and I've removed the source selection. Previously, they supported selecting as many sources as you liked, but that feature never worked and is incompatible with the backend as it stands now, which only allows for one source. Because of that, I've made it so that they can only run in editor unless you open them in the custom pipeline editor, where you can switch out which source it will use. There's also another bit of followup to rename and remove all the previous Policies code. Now that they've been combined into one, we don't need a lot of the Policies code anymore, but also there's about 300 files in the frontend referencing policies in text/comments which need to be updated to say pipelines. This is way more work than is reasonable to do in this PR so I'll just do it in a new PR. ## Limitations This PR is about the merging of the old Policies and Pipelines and I'm considering enforcing the new definition of a Policy where it's only modifiable by admins beyond the scope of this PR. <img width="756" height="395" alt="image" src="https://github.com/user-attachments/assets/d31be5ce-f1c9-46b3-8e8d-866e63f89a81" /> <img width="1507" height="793" alt="image" src="https://github.com/user-attachments/assets/9ba8875f-8be5-4881-91cf-40e0bc1076dc" /> <img width="1508" height="787" alt="image" src="https://github.com/user-attachments/assets/3e1da77b-a0c0-4262-aad3-16650098db81" /> --------- Co-authored-by: EthanHealy01 <80844253+EthanHealy01@users.noreply.github.com>
This commit is contained in:
co-authored by
EthanHealy01
parent
1b2a3118a6
commit
aca0e40c37
+2
@@ -398,6 +398,8 @@ public class PolicyController {
|
||||
policy.name(),
|
||||
owner,
|
||||
policy.enabled(),
|
||||
policy.required(),
|
||||
policy.icon(),
|
||||
policy.inputs(),
|
||||
policy.steps(),
|
||||
policy.output(),
|
||||
|
||||
+44
-5
@@ -21,6 +21,8 @@ public record Policy(
|
||||
String name,
|
||||
String owner,
|
||||
boolean enabled,
|
||||
boolean required,
|
||||
String icon,
|
||||
List<PipelineInput> inputs,
|
||||
List<PipelineStep> steps,
|
||||
OutputSpec output,
|
||||
@@ -29,6 +31,7 @@ public record Policy(
|
||||
EditorConfig editor) {
|
||||
|
||||
public Policy {
|
||||
icon = icon == null ? "" : icon;
|
||||
inputs = inputs == null ? List.of() : List.copyOf(inputs);
|
||||
steps = steps == null ? List.of() : steps;
|
||||
output = output == null ? OutputSpec.inline() : output;
|
||||
@@ -36,7 +39,11 @@ public record Policy(
|
||||
editor = editor == null ? EditorConfig.disabled() : editor;
|
||||
}
|
||||
|
||||
/** Without editor participation: a swept or on-demand policy. */
|
||||
/**
|
||||
* Without the {@code required} flag, {@code icon}, or editor participation: defaults to not
|
||||
* org-required, no icon, and a swept/on-demand policy. Kept for the many callers and tests
|
||||
* written before those fields; the frontend and stores that care use the full constructor.
|
||||
*/
|
||||
public Policy(
|
||||
String id,
|
||||
String name,
|
||||
@@ -47,7 +54,26 @@ public record Policy(
|
||||
OutputSpec output,
|
||||
List<String> outputIds,
|
||||
Long teamId) {
|
||||
this(id, name, owner, enabled, inputs, steps, output, outputIds, teamId, null);
|
||||
this(id, name, owner, enabled, false, "", inputs, steps, output, outputIds, teamId, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Without the {@code required} flag or {@code icon} but with explicit editor participation: the
|
||||
* seeded Classification policy runs on the editor, so it must set {@link EditorConfig} even
|
||||
* though it predates the org-required and icon fields.
|
||||
*/
|
||||
public Policy(
|
||||
String id,
|
||||
String name,
|
||||
String owner,
|
||||
boolean enabled,
|
||||
List<PipelineInput> inputs,
|
||||
List<PipelineStep> steps,
|
||||
OutputSpec output,
|
||||
List<String> outputIds,
|
||||
Long teamId,
|
||||
EditorConfig editor) {
|
||||
this(id, name, owner, enabled, false, "", inputs, steps, output, outputIds, teamId, editor);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,19 +134,32 @@ public record Policy(
|
||||
/** A copy with the inline output replaced (e.g. resolved for the engine, or migrated). */
|
||||
public Policy withOutput(OutputSpec resolved) {
|
||||
return new Policy(
|
||||
id, name, owner, enabled, inputs, steps, resolved, outputIds, teamId, editor);
|
||||
id, name, owner, enabled, required, icon, inputs, steps, resolved, outputIds,
|
||||
teamId, editor);
|
||||
}
|
||||
|
||||
/** A copy under a different owner (e.g. moving a seed off a placeholder name). */
|
||||
public Policy withOwner(String newOwner) {
|
||||
return new Policy(
|
||||
id, name, newOwner, enabled, inputs, steps, output, outputIds, teamId, editor);
|
||||
id, name, newOwner, enabled, required, icon, inputs, steps, output, outputIds,
|
||||
teamId, editor);
|
||||
}
|
||||
|
||||
/** A copy referencing the given saved output destinations. */
|
||||
public Policy withOutputIds(List<String> newOutputIds) {
|
||||
return new Policy(
|
||||
id, name, owner, enabled, inputs, steps, output, newOutputIds, teamId, editor);
|
||||
id,
|
||||
name,
|
||||
owner,
|
||||
enabled,
|
||||
required,
|
||||
icon,
|
||||
inputs,
|
||||
steps,
|
||||
output,
|
||||
newOutputIds,
|
||||
teamId,
|
||||
editor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+27
-23
@@ -20,28 +20,23 @@ import stirling.software.proprietary.policy.source.SourceStore;
|
||||
import stirling.software.proprietary.policy.store.PolicyStore;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Builds the unified Pipelines overview: one row per policy the caller's team owns, with its
|
||||
* sources resolved to live display names, its steps, and a trigger/output summary. This lists EVERY
|
||||
* policy - both pipelines built in the full builder and the friendly "suggested" policies - since
|
||||
* the two surfaces were merged (a policy is a pipeline the org requires). No catalogue filter any
|
||||
* more.
|
||||
*/
|
||||
@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).stream()
|
||||
.filter(PolicyOverviewService::isPipeline)
|
||||
.toList();
|
||||
List<Policy> policies = policyAccessGuard.visibleFrom(policyStore).stream().toList();
|
||||
Map<String, String> sourceNames = sourceNames();
|
||||
|
||||
List<PolicyView> views =
|
||||
@@ -55,18 +50,6 @@ 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<>();
|
||||
@@ -88,6 +71,8 @@ public class PolicyOverviewService {
|
||||
policy.id(),
|
||||
policy.name(),
|
||||
policy.enabled(),
|
||||
policy.required(),
|
||||
iconKey(policy),
|
||||
policy.enabled() ? "active" : "paused",
|
||||
triggerSummary(policy),
|
||||
sources,
|
||||
@@ -111,6 +96,25 @@ public class PolicyOverviewService {
|
||||
return outputSummary(policy.output());
|
||||
}
|
||||
|
||||
/**
|
||||
* The list-row icon key. The policy's first-class {@code icon} wins; otherwise a
|
||||
* template-derived policy falls back to its {@code categoryId} (the template-identity marker
|
||||
* the frontend maps to the category glyph). Empty when neither is set, so the frontend shows
|
||||
* its default.
|
||||
*/
|
||||
private static String iconKey(Policy policy) {
|
||||
if (!policy.icon().isBlank()) {
|
||||
return policy.icon();
|
||||
}
|
||||
OutputSpec output = policy.output();
|
||||
if (output != null
|
||||
&& output.options().get("categoryId") instanceof String category
|
||||
&& !category.isBlank()) {
|
||||
return category;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Summarise a policy's triggers for the overview row: "manual" when no input is triggered,
|
||||
* otherwise the distinct trigger types across its inputs (e.g. "folder-watch, schedule").
|
||||
|
||||
+7
-4
@@ -3,15 +3,18 @@ package stirling.software.proprietary.policy.overview;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* One row in the Pipelines overview: a stored policy shown for the admin portal, with its
|
||||
* referenced sources resolved to names and its pipeline summarised. The portal's "all pipelines"
|
||||
* surface lists every backend policy (the user-facing Policies page builds only a friendly subset
|
||||
* of these).
|
||||
* One row in the unified Pipelines overview: a stored policy shown for the admin portal, with its
|
||||
* referenced sources resolved to names and its pipeline summarised. This surface lists every
|
||||
* backend policy - both the pipelines built in the full builder and the friendly "suggested"
|
||||
* policies - so a {@code required} policy (one the org mandates) reads the same as any other
|
||||
* pipeline here.
|
||||
*/
|
||||
public record PolicyView(
|
||||
String id,
|
||||
String name,
|
||||
boolean enabled,
|
||||
boolean required,
|
||||
String icon,
|
||||
String status,
|
||||
String trigger,
|
||||
List<SourceRef> sources,
|
||||
|
||||
+2
@@ -34,6 +34,8 @@ public class InProcessPolicyStore implements PolicyStore {
|
||||
policy.name(),
|
||||
policy.owner(),
|
||||
policy.enabled(),
|
||||
policy.required(),
|
||||
policy.icon(),
|
||||
policy.inputs(),
|
||||
policy.steps(),
|
||||
policy.output(),
|
||||
|
||||
+11
-1
@@ -17,6 +17,7 @@ import stirling.software.proprietary.policy.model.Policy;
|
||||
import stirling.software.proprietary.policy.model.PolicyBinding;
|
||||
import stirling.software.proprietary.policy.source.EditorSource;
|
||||
|
||||
import tools.jackson.databind.DeserializationFeature;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.ObjectMapper;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
@@ -47,6 +48,8 @@ public class JpaPolicyStore implements PolicyStore {
|
||||
policy.name(),
|
||||
policy.owner(),
|
||||
policy.enabled(),
|
||||
policy.required(),
|
||||
policy.icon(),
|
||||
policy.inputs(),
|
||||
policy.steps(),
|
||||
policy.output(),
|
||||
@@ -155,7 +158,14 @@ public class JpaPolicyStore implements PolicyStore {
|
||||
JsonNode node =
|
||||
liftEditorConfig(
|
||||
upgradeLegacyShape(objectMapper.readTree(entity.getPolicyJson())));
|
||||
return Optional.of(objectMapper.treeToValue(node, Policy.class));
|
||||
// A blob written by an older version won't carry fields added since (e.g. required,
|
||||
// icon). Default absent primitives rather than rejecting the whole policy, so upgrades
|
||||
// don't drop existing pipelines.
|
||||
return Optional.of(
|
||||
objectMapper
|
||||
.readerFor(Policy.class)
|
||||
.without(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES)
|
||||
.readValue(node));
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Skipping unreadable policy id={} name={}: stored JSON could not be parsed"
|
||||
|
||||
+70
-12
@@ -29,11 +29,11 @@ import stirling.software.proprietary.policy.store.InProcessPolicyStore;
|
||||
import stirling.software.proprietary.policy.store.PolicyStore;
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Tests for {@link PolicyOverviewService}: every policy the caller's team owns appears once with
|
||||
* its sources resolved to names, its steps and trigger/output summarised, and the KPI strip
|
||||
* counting active vs paused. Since Policies were merged into Pipelines, the suggested ("catalogue")
|
||||
* policies are listed alongside hand-built pipelines - nothing is filtered. Login is disabled so
|
||||
* the team guards pass everything through.
|
||||
*/
|
||||
class PolicyOverviewServiceTest {
|
||||
|
||||
@@ -99,9 +99,9 @@ class PolicyOverviewServiceTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void excludesCataloguePoliciesButKeepsFolderWatchPipelines() {
|
||||
void listsEveryPolicyIncludingSuggestedOnes() {
|
||||
Source inbox = source("Inbox", "/inbox");
|
||||
// A hand-built pipeline: shows.
|
||||
// A hand-built pipeline.
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
null,
|
||||
@@ -111,7 +111,7 @@ class PolicyOverviewServiceTest {
|
||||
List.of(),
|
||||
List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())),
|
||||
OutputSpec.inline()));
|
||||
// A folder-watch pipeline is still a pipeline: shows.
|
||||
// A folder-watch pipeline.
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
null,
|
||||
@@ -123,7 +123,7 @@ class PolicyOverviewServiceTest {
|
||||
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.
|
||||
// A suggested ("catalogue") policy (categoryId in output options): now listed too.
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
null,
|
||||
@@ -137,10 +137,68 @@ class PolicyOverviewServiceTest {
|
||||
PoliciesOverviewResponse response = service.overview();
|
||||
|
||||
assertEquals(
|
||||
List.of("Compress pipeline", "Inbox watcher"),
|
||||
List.of("Classification Policy", "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());
|
||||
// KPIs count all three.
|
||||
assertEquals(List.of(3L, 3L, 0L), response.kpis().stream().map(PolicyKpi::value).toList());
|
||||
}
|
||||
|
||||
@Test
|
||||
void requiredFlagSurfacesInTheView() {
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
null,
|
||||
"Mandatory redaction",
|
||||
"owner",
|
||||
true,
|
||||
true,
|
||||
"",
|
||||
List.of(),
|
||||
List.of(new PipelineStep("/api/v1/security/auto-redact", Map.of())),
|
||||
OutputSpec.inline(),
|
||||
List.of(),
|
||||
null,
|
||||
EditorConfig.disabled()));
|
||||
|
||||
PolicyView view = find(service.overview(), "Mandatory redaction");
|
||||
assertTrue(view.required());
|
||||
}
|
||||
|
||||
@Test
|
||||
void iconIsExplicitOtherwiseFallsBackToCategory() {
|
||||
// The policy's first-class icon wins.
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
null,
|
||||
"Custom with icon",
|
||||
"owner",
|
||||
true,
|
||||
false,
|
||||
"shield",
|
||||
List.of(),
|
||||
List.of(new PipelineStep("/api/v1/misc/compress-pdf", Map.of())),
|
||||
OutputSpec.inline(),
|
||||
List.of(),
|
||||
null,
|
||||
EditorConfig.disabled()));
|
||||
// No explicit icon: a template-derived policy falls back to its categoryId marker.
|
||||
policyStore.save(
|
||||
new Policy(
|
||||
null,
|
||||
"Template derived",
|
||||
"owner",
|
||||
true,
|
||||
false,
|
||||
"",
|
||||
List.of(),
|
||||
List.of(new PipelineStep("/api/v1/security/auto-redact", Map.of())),
|
||||
new OutputSpec("inline", Map.of("categoryId", "security")),
|
||||
List.of(),
|
||||
null,
|
||||
EditorConfig.disabled()));
|
||||
|
||||
assertEquals("shield", find(service.overview(), "Custom with icon").icon());
|
||||
assertEquals("security", find(service.overview(), "Template derived").icon());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user