Skip to content

Commit 1f6187e

Browse files
authored
Merge pull request #8671 from ProcessMaker/task/FOUR-28659
Task/FOUR-28659: Implement new Logs base UI section for Agents
2 parents c76e941 + 5f65a78 commit 1f6187e

21 files changed

Lines changed: 1034 additions & 0 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
<?php
2+
3+
namespace ProcessMaker\Http\Controllers\Admin;
4+
5+
use Illuminate\Http\Request;
6+
use ProcessMaker\Http\Controllers\Controller;
7+
8+
class LogsController extends Controller
9+
{
10+
/**
11+
* Display the logs index page.
12+
* This view loads log components from installed packages (package-email-start-event, package-ai).
13+
*
14+
* @return \Illuminate\Contracts\View\View
15+
*/
16+
public function index()
17+
{
18+
return view('admin.logs.index');
19+
}
20+
}

ProcessMaker/Http/Middleware/GenerateMenus.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,14 @@ public function handle(Request $request, Closure $next)
128128
'file' => "data:image/svg+xml;base64,{$devlinkIcon}",
129129
]);
130130
}
131+
if (\Auth::user()->canAny('view-settings|edit-settings') &&
132+
(hasPackage('package-email-start-event') || hasPackage('package-ai'))) {
133+
$submenu->add(__('Logs'), [
134+
'route' => 'admin.logs',
135+
'icon' => 'fa-bars',
136+
'id' => 'admin-logs',
137+
]);
138+
}
131139
});
132140
Menu::make('sidebar_task', function ($menu) {
133141
$submenu = $menu->add(__('Tasks'));
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
<template>
2+
<div
3+
class="
4+
tw-flex-1 tw-overflow-auto tw-rounded-xl tw-border tw-border-gray-200
5+
tw-shadow-md tw-shadow-zinc-200
6+
"
7+
>
8+
<table class="tw-w-full tw-text-left tw-text-sm">
9+
<thead class="tw-bg-gray-50 tw-text-zinc-400">
10+
<tr>
11+
<th
12+
v-for="column in columns"
13+
:key="column.key"
14+
class="tw-px-6 tw-py-4 tw-font-medium tw-whitespace-nowrap"
15+
>
16+
{{ column.label }}
17+
</th>
18+
</tr>
19+
</thead>
20+
<tbody>
21+
<tr
22+
v-for="(item, idx) in data"
23+
:key="idx"
24+
class="tw-border-t tw-border-zinc-200"
25+
>
26+
<td
27+
v-for="column in columns"
28+
:key="column.key"
29+
class="tw-px-6 tw-py-4 tw-text-gray-600 tw-border-b tw-border-gray-200 tw-whitespace-nowrap"
30+
>
31+
{{ getItemValue(item, column) }}
32+
</td>
33+
</tr>
34+
</tbody>
35+
</table>
36+
</div>
37+
</template>
38+
39+
<script>
40+
export default {
41+
props: {
42+
columns: { type: Array, required: true },
43+
data: { type: Array, required: true },
44+
},
45+
methods: {
46+
getItemValue(item, column) {
47+
// Get value - handle dot-separated keys for nested properties
48+
const value = column.key.includes(".")
49+
? column.key.split(".").reduce((val, part) => (val == null ? undefined : val[part]), item)
50+
: item[column.key];
51+
52+
// Apply format function if provided
53+
if (typeof column.format === "function") {
54+
return column.format(value);
55+
}
56+
57+
return value;
58+
},
59+
},
60+
};
61+
</script>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// eslint-disable-next-line import/prefer-default-export
2+
export { default as BaseTable } from './BaseTable.vue';
3+
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<template>
2+
<div class="tw-flex tw-flex-row tw-gap-x-8 tw-justify-between tw-w-full sm:tw-flex-row">
3+
<!-- Email log type tabs - only shown for email category -->
4+
<div
5+
v-if="isEmailCategory"
6+
class="tw-flex tw-items-center tw-gap-2 tw-bg-gray-100 tw-rounded-lg tw-p-1"
7+
>
8+
<RouterLink
9+
to="/email/errors"
10+
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
11+
:class="tabClasses('errors')"
12+
>
13+
{{ $t('Error Logs') }}
14+
</RouterLink>
15+
<RouterLink
16+
to="/email/matched"
17+
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
18+
:class="tabClasses('matched')"
19+
>
20+
{{ $t('Matched Logs') }}
21+
</RouterLink>
22+
<RouterLink
23+
to="/email/total"
24+
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
25+
:class="tabClasses('total')"
26+
>
27+
{{ $t('Total Logs') }}
28+
</RouterLink>
29+
</div>
30+
31+
<!-- Agents category tabs -->
32+
<div
33+
v-else-if="isAgentsCategory"
34+
class="tw-flex tw-items-center tw-gap-2 tw-bg-gray-100 tw-rounded-lg tw-p-1"
35+
>
36+
<RouterLink
37+
to="/agents/design"
38+
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
39+
:class="tabClasses('design')"
40+
>
41+
{{ $t('Design Mode Logs') }}
42+
</RouterLink>
43+
<RouterLink
44+
to="/agents/execution"
45+
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
46+
:class="tabClasses('execution')"
47+
>
48+
{{ $t('Execution Logs') }}
49+
</RouterLink>
50+
</div>
51+
52+
<!-- Empty placeholder for other categories -->
53+
<div v-else />
54+
55+
<div class="tw-flex tw-flex-1 tw-items-center tw-gap-1 tw-w-auto tw-border tw-border-zinc-200 tw-rounded-lg tw-p-1 tw-px-3">
56+
<div class="tw-relative tw-w-full tw-flex tw-items-center tw-gap-1">
57+
<i class="fas fa-search" />
58+
<input
59+
ref="searchInput"
60+
type="text"
61+
class="
62+
tw-h-8
63+
tw-w-full
64+
tw-pl-3
65+
tw-pr-3
66+
tw-text-sm
67+
tw-outline-none
68+
tw-ring-0
69+
placeholder:tw-text-zinc-400
70+
"
71+
:placeholder="$t('Search here')"
72+
:value="value"
73+
@input="onInput"
74+
@keypress="onKeypress"
75+
>
76+
</div>
77+
</div>
78+
</div>
79+
</template>
80+
81+
<script>
82+
export default {
83+
props: {
84+
value: { type: String, default: '' },
85+
},
86+
computed: {
87+
isEmailCategory() {
88+
return this.$route.path.startsWith('/email');
89+
},
90+
isAgentsCategory() {
91+
return this.$route.path.startsWith('/agents');
92+
},
93+
},
94+
watch: {
95+
'$route.path': {
96+
handler() {
97+
// reset input value in search when route changes
98+
this.$emit('input', '');
99+
},
100+
immediate: true,
101+
},
102+
},
103+
methods: {
104+
tabClasses(tab) {
105+
const currentRoute = this.$route.params.logType;
106+
107+
return currentRoute === tab
108+
? 'tw-bg-white tw-font-semibold tw-text-zinc-900'
109+
: 'tw-text-zinc-700 hover:tw-bg-zinc-50';
110+
},
111+
onInput(event) {
112+
this.$emit('input', event.target.value);
113+
},
114+
onKeypress(event) {
115+
if (event.charCode === 13) {
116+
this.$emit('search');
117+
}
118+
},
119+
},
120+
};
121+
</script>
122+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// eslint-disable-next-line import/prefer-default-export
2+
export { default as HeaderBar } from './HeaderBar.vue';
3+
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
<template>
2+
<div class="tw-flex tw-gap-6">
3+
<sidebar />
4+
5+
<section class="tw-flex-1 tw-overflow-hidden">
6+
<div class="tw-flex tw-flex-col tw-rounded-xl tw-border tw-border-zinc-200 tw-p-4 tw-bg-white tw-h-screen">
7+
<header-bar
8+
v-model="search"
9+
@search="onHandleSearch"
10+
/>
11+
12+
<div class="tw-flex tw-flex-col tw-flex-1 tw-min-h-0">
13+
<div class="tw-flex tw-items-center tw-justify-between tw-my-8 tw-shrink-0">
14+
<h2 class="tw-text-2xl tw-font-semibold tw-text-zinc-900">
15+
{{ $t(title) }}
16+
</h2>
17+
<div v-if="showExportButton">
18+
<a
19+
:href="getExportUrl"
20+
target="_blank"
21+
class="
22+
tw-inline-flex
23+
tw-items-center
24+
tw-gap-2
25+
tw-rounded-lg
26+
tw-bg-blue-500
27+
tw-px-3
28+
tw-py-2
29+
tw-text-sm
30+
tw-font-normal
31+
tw-text-white
32+
"
33+
>
34+
<i class="fas fa-download" />
35+
<span>{{ $t('Export to CSV') }}</span>
36+
</a>
37+
</div>
38+
</div>
39+
40+
<RouterView ref="routerView" class="tw-flex tw-flex-col tw-flex-1 tw-min-h-0" />
41+
</div>
42+
</div>
43+
</section>
44+
</div>
45+
</template>
46+
47+
<script>
48+
import { Sidebar } from '../Sidebar';
49+
import { HeaderBar } from '../HeaderBar';
50+
51+
export default {
52+
components: {
53+
Sidebar,
54+
HeaderBar,
55+
},
56+
data() {
57+
return {
58+
search: '',
59+
};
60+
},
61+
computed: {
62+
isEmailCategory() {
63+
return this.$route.path.startsWith('/email');
64+
},
65+
isAgentsCategory() {
66+
return this.$route.path.startsWith('/agents');
67+
},
68+
logType() {
69+
return this.$route.params.logType;
70+
},
71+
title() {
72+
if (this.isAgentsCategory) {
73+
const agentTitles = {
74+
design: 'Design Mode Logs',
75+
execution: 'Execution Logs',
76+
};
77+
return agentTitles[this.logType] ?? 'FlowGenie Agents Logs';
78+
}
79+
80+
const titles = {
81+
errors: 'Error Logs',
82+
matched: 'Matched Logs',
83+
total: 'Total Logs',
84+
};
85+
return titles[this.logType] ?? '';
86+
},
87+
showExportButton() {
88+
// Only show export button for email category (has export endpoint)
89+
return this.isEmailCategory;
90+
},
91+
getExportUrl() {
92+
return `/admin/logs/export/csv?type=${this.logType}&search=${this.search}`;
93+
},
94+
},
95+
methods: {
96+
onHandleSearch() {
97+
if (this.$refs.routerView) {
98+
this.$refs.routerView.refresh({ search: this.search });
99+
}
100+
},
101+
},
102+
};
103+
</script>
104+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
// eslint-disable-next-line import/prefer-default-export
2+
export { default as LogContainer } from './LogContainer.vue';
3+

0 commit comments

Comments
 (0)