Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions public/locales/de-DE/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
"deployments": "Deployments",
"batches": "Stapelverarbeitung",
"migrations": "Migrationen",
"operation-log": "Operationsprotokoll",
"admin": "Admin",
"help": "Hilfe",
"account": "Konto",
Expand Down Expand Up @@ -77,6 +78,7 @@
"decisions": "Entscheidungen",
"deployments": "Deployments",
"migrations": "Migrationen",
"operation-log": "Operationsprotokoll",
"admin": "Admin",
"admin-users": "Admin – Benutzer",
"admin-groups": "Admin – Gruppen",
Expand Down Expand Up @@ -761,6 +763,56 @@
"view-batch": "Batch anzeigen"
}
},
"operation_log": {
"title": "Operationsprotokoll",
"refresh": "Aktualisieren",
"matching-entries": "{{count}} passende Einträge",
"empty": "Keine Operationen entsprechen diesem Filter.",
"timestamp": "Zeitstempel",
"user": "Benutzer",
"operation": "Operation",
"entity": "Entität",
"annotation": "Anmerkung",
"annotate": "Anmerken",
"annotation-title": "Anmerkung bearbeiten",
"clear-annotation": "Anmerkung entfernen",
"details": "Eintragsdetails",
"property": "Eigenschaft",
"original-value": "Ursprünglicher Wert",
"new-value": "Neuer Wert",
"category": "Kategorie",
"apply-filters": "Filter anwenden",
"clear-filters": "Filter zurücksetzen",
"filters": {
"user": "Benutzer-ID",
"operation": "Operationstyp",
"after": "Nach",
"before": "Vor"
},
"sort": {
"timestamp": "Zeitstempel",
"operationId": "Operations-ID",
"userId": "Benutzer-ID",
"operationType": "Operationstyp",
"entityType": "Entitätstyp",
"category": "Kategorie"
},
"filter_keys": {
"userId": "userId",
"operationType": "operationType",
"entityType": "entityType",
"category": "category",
"timestampAfter": "timestampAfter",
"timestampBefore": "timestampBefore",
"processInstanceId": "processInstanceId",
"processDefinitionId": "processDefinitionId",
"taskId": "taskId",
"operationId": "operationId"
},
"filter": {
"manage_title": "Operationsprotokoll-Filter verwalten"
}
},
"list_filter": {
"saved_filter": "Filter",
"all": "Alle",
Expand Down
52 changes: 52 additions & 0 deletions public/locales/en-US/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"migrations": "Migrations",
"admin": "Admin",
"help": "Help",
"operation-log": "Operation Log",
"account": "Account",
"logout": "Logout",
"menu": "Menu",
Expand Down Expand Up @@ -77,6 +78,7 @@
"decisions": "Decisions",
"deployments": "Deployments",
"migrations": "Migrations",
"operation-log": "Operation Log",
"admin": "Admin",
"admin-users": "Admin – Users",
"admin-groups": "Admin – Groups",
Expand Down Expand Up @@ -761,6 +763,56 @@
"view-batch": "View batch"
}
},
"operation_log": {
"title": "Operation Log",
"refresh": "Refresh",
"matching-entries": "{{count}} matching entries",
"empty": "No operations match this filter.",
"timestamp": "Timestamp",
"user": "User",
"operation": "Operation",
"entity": "Entity",
"annotation": "Annotation",
"annotate": "Annotate",
"annotation-title": "Edit annotation",
"clear-annotation": "Clear annotation",
"details": "Entry details",
"property": "Property",
"original-value": "Original value",
"new-value": "New value",
"category": "Category",
"apply-filters": "Apply filters",
"clear-filters": "Clear filters",
"filters": {
"user": "User ID",
"operation": "Operation type",
"after": "After",
"before": "Before"
},
"sort": {
"timestamp": "Timestamp",
"operationId": "Operation ID",
"userId": "User ID",
"operationType": "Operation type",
"entityType": "Entity type",
"category": "Category"
},
"filter_keys": {
"userId": "userId",
"operationType": "operationType",
"entityType": "entityType",
"category": "category",
"timestampAfter": "timestampAfter",
"timestampBefore": "timestampBefore",
"processInstanceId": "processInstanceId",
"processDefinitionId": "processDefinitionId",
"taskId": "taskId",
"operationId": "operationId"
},
"filter": {
"manage_title": "Manage operation-log filters"
}
},
"list_filter": {
"saved_filter": "Filter",
"all": "All",
Expand Down
48 changes: 46 additions & 2 deletions src/api/resources/history.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { GET, PAGINATED_GET } from '../helper.jsx'
import { GET, PAGINATED_GET, PUT } from '../helper.jsx'

const INSTANCE_PAGE_SIZE = 20
const OPERATION_LOG_PAGE_SIZE = 30

const instance_url = (definition_id, params = {}, { unfinished = false } = {}) => {
const merged = {
Expand Down Expand Up @@ -55,6 +56,43 @@ const get_historic_called_instances = (state, instance_id) =>
const get_user_operation = (state, execution_id) =>
GET(`/history/user-operation?processInstanceId=${execution_id}`, state, state.api.history.user_operation)

const operation_log_url = (params = {}) => {
const merged = {
sortBy: 'timestamp',
sortOrder: 'desc',
...params,
}
return new URLSearchParams(merged).toString()
}

const get_operation_log = (state, params = {}, firstResult = 0) =>
PAGINATED_GET(
`/history/user-operation?${operation_log_url(params)}`,
state,
state.api.history.operation_log.list,
firstResult,
OPERATION_LOG_PAGE_SIZE,
)

const get_operation_log_count = (state, params = {}) =>
GET(`/history/user-operation/count?${operation_log_url(params)}`, state, state.api.history.operation_log.count)

const set_operation_log_annotation = (state, operation_id, annotation) =>
PUT(
`/history/user-operation/${operation_id}/set-annotation`,
{ annotation },
state,
state.api.history.operation_log.update,
)

const clear_operation_log_annotation = (state, operation_id) =>
PUT(
`/history/user-operation/${operation_id}/clear-annotation`,
{},
state,
state.api.history.operation_log.update,
)

const history = {
process_instance: {
all: get_process_instances,
Expand All @@ -72,7 +110,13 @@ const history = {
task: {
by_process_instance: get_historic_tasks_by_instance,
},
operation_log: {
all: get_operation_log,
count: get_operation_log_count,
set_annotation: set_operation_log_annotation,
clear_annotation: clear_operation_log_annotation,
},
get_user_operation
}

export default history
export default history
49 changes: 48 additions & 1 deletion src/api/resources/history.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { describe, it, vi, beforeEach, expect } from "vitest";
vi.mock("../helper.jsx", () => ({
GET: vi.fn(),
PAGINATED_GET: vi.fn(),
PUT: vi.fn(),
}));

import { GET, PAGINATED_GET } from "../helper.jsx";
import { GET, PAGINATED_GET, PUT } from "../helper.jsx";
import { create_mock_state, expect_api_call } from "../../test/helpers.js";
import history from "./history.js";

Expand Down Expand Up @@ -101,6 +102,52 @@ describe("api/resources/history", () => {
});
});

it("operation_log.all() PAGINATED_GETs the global operation log", () => {
history.operation_log.all(
state,
{ userId: "demo", operationType: "ModifyProcessInstance" },
30,
);
expect_api_call(PAGINATED_GET, {
url: "/history/user-operation?sortBy=timestamp&sortOrder=desc&userId=demo&operationType=ModifyProcessInstance",
state,
signal: state.api.history.operation_log.list,
});
expect(PAGINATED_GET.mock.lastCall[3]).toBe(30);
expect(PAGINATED_GET.mock.lastCall[4]).toBe(30);
});

it("operation_log.count() GETs the matching operation count", () => {
history.operation_log.count(state, {
timestampAfter: "2026-06-01T00:00:00.000+0000",
});
expect_api_call(GET, {
url: "/history/user-operation/count?sortBy=timestamp&sortOrder=desc&timestampAfter=2026-06-01T00%3A00%3A00.000%2B0000",
state,
signal: state.api.history.operation_log.count,
});
});

it("operation_log.set_annotation() PUTs the annotation body", () => {
history.operation_log.set_annotation(state, "op1", "Reviewed");
expect_api_call(PUT, {
url: "/history/user-operation/op1/set-annotation",
body: { annotation: "Reviewed" },
state,
signal: state.api.history.operation_log.update,
});
});

it("operation_log.clear_annotation() PUTs to the clear endpoint", () => {
history.operation_log.clear_annotation(state, "op1");
expect_api_call(PUT, {
url: "/history/user-operation/op1/clear-annotation",
body: {},
state,
signal: state.api.history.operation_log.update,
});
});

it("task.by_process_instance() GETs historic tasks filtered by instance", () => {
history.task.by_process_instance(state, "inst-1");
expect_api_call(GET, {
Expand Down
1 change: 1 addition & 0 deletions src/components/GoTo.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const PAGES = [
{ nameKey: "goto.pages.decisions", href: "/decisions" },
{ nameKey: "goto.pages.deployments", href: "/deployments" },
{ nameKey: "goto.pages.migrations", href: "/migrations" },
{ nameKey: "goto.pages.operation-log", href: "/operation-log" },
{ nameKey: "goto.pages.admin", href: "/admin" },
{ nameKey: "goto.pages.admin-users", href: "/admin/users" },
{ nameKey: "goto.pages.admin-groups", href: "/admin/groups" },
Expand Down
7 changes: 7 additions & 0 deletions src/components/GoTo.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ describe("GoTo", () => {
a.getAttribute("href"),
),
).not.toContain("/tasks");

type(input, "operation-log");
expect(
Array.from(container.querySelectorAll(".goto-item")).map((a) =>
a.getAttribute("href"),
),
).toContain("/operation-log");
});

it("navigates and closes when a result is clicked", () => {
Expand Down
9 changes: 9 additions & 0 deletions src/components/Header.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export function Header() {
<nav id="secondary-navigation">
<menu>
<li><a href="/help"> {t("nav.help")}</a></li>
<li><a href="/operation-log">{t("nav.operation-log")}</a></li>
<li><a href="/account">{t("nav.account")}</a></li>
</menu>
</nav>
Expand Down Expand Up @@ -146,6 +147,14 @@ export function Header() {
{t("nav.migrations")}
</a>
</li>
<li>
<a
href="/operation-log"
class={url.startsWith("/operation-log") && "active"}
>
{t("nav.operation-log")}
</a>
</li>
<li>
<a href="/admin" class={url.startsWith("/admin") && "active"}>
{t("nav.admin")}
Expand Down
53 changes: 53 additions & 0 deletions src/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -828,6 +828,59 @@
text-align: center;
}

.operation-log {
box-sizing: border-box;
display: flex;
flex-direction: column;
gap: var(--spacing-2);
height: 100%;
padding: var(--spacing-2);
}
.operation-log > header {
display: flex;
justify-content: space-between;
align-items: start;
gap: var(--spacing-2);
}
.operation-log > header button {
margin: 0;
}
.operation-log-filter {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
gap: var(--spacing-1);
align-items: end;
}
.operation-log-filter label,
.operation-log-annotation label {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.operation-log table {
width: 100%;
}
.operation-log td {
vertical-align: top;
}
.operation-log details {
padding: var(--spacing-1) 0;
}
.operation-log details table {
background: var(--background-2);
margin-top: var(--spacing-1);
}
.operation-log-entities {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.operation-log-annotation textarea {
box-sizing: border-box;
min-height: 8rem;
width: min(28rem, 100%);
}

#admin-page {
display: flex;
flex: 1;
Expand Down
Loading