Skip to content
Merged
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
20 changes: 20 additions & 0 deletions ProcessMaker/Http/Controllers/Admin/LogsController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

namespace ProcessMaker\Http\Controllers\Admin;

use Illuminate\Http\Request;
use ProcessMaker\Http\Controllers\Controller;

class LogsController extends Controller
{
/**
* Display the logs index page.
* This view loads log components from installed packages (package-email-start-event, package-ai).
*
* @return \Illuminate\Contracts\View\View
*/
public function index()
{
return view('admin.logs.index');
}
}
8 changes: 8 additions & 0 deletions ProcessMaker/Http/Middleware/GenerateMenus.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ public function handle(Request $request, Closure $next)
'file' => "data:image/svg+xml;base64,{$devlinkIcon}",
]);
}
if (\Auth::user()->canAny('view-settings|edit-settings') &&
(hasPackage('package-email-start-event') || hasPackage('package-ai'))) {
$submenu->add(__('Logs'), [
'route' => 'admin.logs',
'icon' => 'fa-bars',
'id' => 'admin-logs',
]);
}
});
Menu::make('sidebar_task', function ($menu) {
$submenu = $menu->add(__('Tasks'));
Expand Down
61 changes: 61 additions & 0 deletions resources/js/admin/logs/components/Logs/BaseTable/BaseTable.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<template>
<div
class="
tw-flex-1 tw-overflow-auto tw-rounded-xl tw-border tw-border-gray-200
tw-shadow-md tw-shadow-zinc-200
"
>
<table class="tw-w-full tw-text-left tw-text-sm">
<thead class="tw-bg-gray-50 tw-text-zinc-400">
<tr>
<th
v-for="column in columns"
:key="column.key"
class="tw-px-6 tw-py-4 tw-font-medium tw-whitespace-nowrap"
>
{{ column.label }}
</th>
</tr>
</thead>
<tbody>
<tr
v-for="(item, idx) in data"
:key="idx"
class="tw-border-t tw-border-zinc-200"
>
<td
v-for="column in columns"
:key="column.key"
class="tw-px-6 tw-py-4 tw-text-gray-600 tw-border-b tw-border-gray-200 tw-whitespace-nowrap"
>
{{ getItemValue(item, column) }}
</td>
</tr>
</tbody>
</table>
</div>
</template>

<script>
export default {
props: {
columns: { type: Array, required: true },
data: { type: Array, required: true },
},
methods: {
getItemValue(item, column) {
// Get value - handle dot-separated keys for nested properties
const value = column.key.includes(".")
? column.key.split(".").reduce((val, part) => (val == null ? undefined : val[part]), item)
: item[column.key];

// Apply format function if provided
if (typeof column.format === "function") {
return column.format(value);
}

return value;
},
},
};
</script>
3 changes: 3 additions & 0 deletions resources/js/admin/logs/components/Logs/BaseTable/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export { default as BaseTable } from './BaseTable.vue';

122 changes: 122 additions & 0 deletions resources/js/admin/logs/components/Logs/HeaderBar/HeaderBar.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
<template>
<div class="tw-flex tw-flex-row tw-gap-x-8 tw-justify-between tw-w-full sm:tw-flex-row">
<!-- Email log type tabs - only shown for email category -->
<div
v-if="isEmailCategory"
class="tw-flex tw-items-center tw-gap-2 tw-bg-gray-100 tw-rounded-lg tw-p-1"
>
<RouterLink
to="/email/errors"
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
:class="tabClasses('errors')"
>
{{ $t('Error Logs') }}
</RouterLink>
<RouterLink
to="/email/matched"
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
:class="tabClasses('matched')"
>
{{ $t('Matched Logs') }}
</RouterLink>
<RouterLink
to="/email/total"
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
:class="tabClasses('total')"
>
{{ $t('Total Logs') }}
</RouterLink>
</div>

<!-- Agents category tabs -->
<div
v-else-if="isAgentsCategory"
class="tw-flex tw-items-center tw-gap-2 tw-bg-gray-100 tw-rounded-lg tw-p-1"
>
<RouterLink
to="/agents/design"
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
:class="tabClasses('design')"
>
{{ $t('Design Mode Logs') }}
</RouterLink>
<RouterLink
to="/agents/execution"
class="tw-rounded-lg tw-px-3 tw-py-2 tw-text-base"
:class="tabClasses('execution')"
>
{{ $t('Execution Logs') }}
</RouterLink>
</div>

<!-- Empty placeholder for other categories -->
<div v-else />

<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">
<div class="tw-relative tw-w-full tw-flex tw-items-center tw-gap-1">
<i class="fas fa-search" />
<input
ref="searchInput"
type="text"
class="
tw-h-8
tw-w-full
tw-pl-3
tw-pr-3
tw-text-sm
tw-outline-none
tw-ring-0
placeholder:tw-text-zinc-400
"
:placeholder="$t('Search here')"
:value="value"
@input="onInput"
@keypress="onKeypress"
>
</div>
</div>
</div>
</template>

<script>
export default {
props: {
value: { type: String, default: '' },
},
computed: {
isEmailCategory() {
return this.$route.path.startsWith('/email');
},
isAgentsCategory() {
return this.$route.path.startsWith('/agents');
},
},
watch: {
'$route.path': {
handler() {
// reset input value in search when route changes
this.$emit('input', '');
},
immediate: true,
},
},
methods: {
tabClasses(tab) {
const currentRoute = this.$route.params.logType;

return currentRoute === tab
? 'tw-bg-white tw-font-semibold tw-text-zinc-900'
: 'tw-text-zinc-700 hover:tw-bg-zinc-50';
},
onInput(event) {
this.$emit('input', event.target.value);
},
onKeypress(event) {
if (event.charCode === 13) {
this.$emit('search');
}
},
},
};
</script>

3 changes: 3 additions & 0 deletions resources/js/admin/logs/components/Logs/HeaderBar/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export { default as HeaderBar } from './HeaderBar.vue';

104 changes: 104 additions & 0 deletions resources/js/admin/logs/components/Logs/LogContainer/LogContainer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<template>
<div class="tw-flex tw-gap-6">
<sidebar />

<section class="tw-flex-1 tw-overflow-hidden">
<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">
<header-bar
v-model="search"
@search="onHandleSearch"
/>

<div class="tw-flex tw-flex-col tw-flex-1 tw-min-h-0">
<div class="tw-flex tw-items-center tw-justify-between tw-my-8 tw-shrink-0">
<h2 class="tw-text-2xl tw-font-semibold tw-text-zinc-900">
{{ $t(title) }}
</h2>
<div v-if="showExportButton">
<a
:href="getExportUrl"
target="_blank"
class="
tw-inline-flex
tw-items-center
tw-gap-2
tw-rounded-lg
tw-bg-blue-500
tw-px-3
tw-py-2
tw-text-sm
tw-font-normal
tw-text-white
"
>
<i class="fas fa-download" />
<span>{{ $t('Export to CSV') }}</span>
</a>
</div>
</div>

<RouterView ref="routerView" class="tw-flex tw-flex-col tw-flex-1 tw-min-h-0" />
</div>
</div>
</section>
</div>
</template>

<script>
import { Sidebar } from '../Sidebar';
import { HeaderBar } from '../HeaderBar';

export default {
components: {
Sidebar,
HeaderBar,
},
data() {
return {
search: '',
};
},
computed: {
isEmailCategory() {
return this.$route.path.startsWith('/email');
},
isAgentsCategory() {
return this.$route.path.startsWith('/agents');
},
logType() {
return this.$route.params.logType;
},
title() {
if (this.isAgentsCategory) {
const agentTitles = {
design: 'Design Mode Logs',
execution: 'Execution Logs',
};
return agentTitles[this.logType] ?? 'FlowGenie Agents Logs';
}

const titles = {
errors: 'Error Logs',
matched: 'Matched Logs',
total: 'Total Logs',
};
return titles[this.logType] ?? '';
},
showExportButton() {
// Only show export button for email category (has export endpoint)
return this.isEmailCategory;
},
getExportUrl() {
return `/admin/logs/export/csv?type=${this.logType}&search=${this.search}`;
},
Copy link

Choose a reason for hiding this comment

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

Search parameter not URL-encoded in export URL

Medium Severity

The getExportUrl computed property directly interpolates this.search into the URL query string without using encodeURIComponent(). If a user's search term contains special characters like &, =, ?, or #, the URL will be malformed. For example, searching for test&foo=bar would produce a URL where foo is incorrectly parsed as a separate query parameter rather than part of the search value, causing the export to return wrong results.

Fix in Cursor Fix in Web

},
methods: {
onHandleSearch() {
if (this.$refs.routerView) {
this.$refs.routerView.refresh({ search: this.search });
}
Copy link

Choose a reason for hiding this comment

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

Search functionality broken due to incorrect RouterView ref

High Severity

The onHandleSearch method calls this.$refs.routerView.refresh(), but in Vue 2 the ref on <RouterView> points to the RouterView component wrapper, not the rendered child component (LogTable). Since RouterView doesn't have a refresh method, the search functionality will silently fail and users won't be able to filter log data. The refresh method exists on LogTable but is inaccessible through this ref pattern.

Fix in Cursor Fix in Web

},
},
};
</script>

3 changes: 3 additions & 0 deletions resources/js/admin/logs/components/Logs/LogContainer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// eslint-disable-next-line import/prefer-default-export
export { default as LogContainer } from './LogContainer.vue';

Loading
Loading