-
Notifications
You must be signed in to change notification settings - Fork 242
Task/FOUR-28659: Implement new Logs base UI section for Agents #8671
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a495541
7be12fb
1022975
d43dc69
5f65a78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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'); | ||
| } | ||
| } |
| 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> |
| 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'; | ||
|
|
| 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> | ||
|
|
| 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'; | ||
|
|
| 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}`; | ||
| }, | ||
| }, | ||
| methods: { | ||
| onHandleSearch() { | ||
| if (this.$refs.routerView) { | ||
| this.$refs.routerView.refresh({ search: this.search }); | ||
| } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Search functionality broken due to incorrect RouterView refHigh Severity The |
||
| }, | ||
| }, | ||
| }; | ||
| </script> | ||
|
|
||
| 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'; | ||
|
|
There was a problem hiding this comment.
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
getExportUrlcomputed property directly interpolatesthis.searchinto the URL query string without usingencodeURIComponent(). If a user's search term contains special characters like&,=,?, or#, the URL will be malformed. For example, searching fortest&foo=barwould produce a URL wherefoois incorrectly parsed as a separate query parameter rather than part of the search value, causing the export to return wrong results.