Adding pagination control to the API and to the frontend, as well as getting view working as expected, emitting different key combinations to be able to search by any pattern.

This commit is contained in:
mike12345567 2022-06-01 22:39:51 +01:00
parent 34759c7916
commit fe84c0f21c
12 changed files with 316 additions and 180 deletions

View File

@ -122,10 +122,12 @@ const automationActions = store => ({
return state return state
}) })
}, },
getLogs: async (automationId, startDate) => { getLogs: async ({ automationId, startDate, status, page } = {}) => {
return await API.getAutomationLogs({ return await API.getAutomationLogs({
automationId, automationId,
startDate, startDate,
status,
page,
}) })
}, },
addTestDataToAutomation: data => { addTestDataToAutomation: data => {

View File

@ -1,5 +1,5 @@
<script> <script>
import { Layout, Table, Select } from "@budibase/bbui" import { Layout, Table, Select, Pagination } from "@budibase/bbui"
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte" import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
import StatusRenderer from "./StatusRenderer.svelte" import StatusRenderer from "./StatusRenderer.svelte"
import HistoryDetailsPanel from "./HistoryDetailsPanel.svelte" import HistoryDetailsPanel from "./HistoryDetailsPanel.svelte"
@ -8,9 +8,24 @@
export let appId export let appId
let runHistory = []
let showPanel = false let showPanel = false
let selectedHistory = null let selectedHistory = null
let runHistory = [] let automationOptions = []
let automationId = null
let status = null
let prevPage,
nextPage,
page,
hasNextPage,
pageNumber = 1
$: fetchLogs(automationId, status, page)
const statusOptions = [
{ value: "success", label: "Success" },
{ value: "error", label: "Error" },
]
const runHistorySchema = { const runHistorySchema = {
status: { displayName: "Status" }, status: { displayName: "Status" },
@ -23,7 +38,33 @@
{ column: "status", component: StatusRenderer }, { column: "status", component: StatusRenderer },
] ]
async function fetchLogs(automationId, status, page) {
const response = await automationStore.actions.getLogs({
automationId,
status,
page,
})
nextPage = response.nextPage
hasNextPage = response.hasNextPage
runHistory = enrichHistory($automationStore.blockDefinitions, response.data)
}
function goToNextPage() {
pageNumber++
prevPage = page
page = nextPage
}
function goToPrevPage() {
pageNumber--
nextPage = page
page = prevPage
}
function enrichHistory(definitions, runHistory) { function enrichHistory(definitions, runHistory) {
if (!definitions) {
return []
}
const finalHistory = [] const finalHistory = []
for (let history of runHistory) { for (let history of runHistory) {
if (!history.steps) { if (!history.steps) {
@ -31,8 +72,8 @@
} }
let notFound = false let notFound = false
for (let step of history.steps) { for (let step of history.steps) {
const trigger = definitions.trigger[step.stepId], const trigger = definitions.TRIGGER[step.stepId],
action = definitions.action[step.stepId] action = definitions.ACTION[step.stepId]
if (!trigger && !action) { if (!trigger && !action) {
notFound = true notFound = true
break break
@ -53,11 +94,12 @@
} }
onMount(async () => { onMount(async () => {
let definitions = await automationStore.actions.definitions() await automationStore.actions.fetch()
runHistory = enrichHistory( await fetchLogs()
definitions, automationOptions = []
await automationStore.actions.getLogs() for (let automation of $automationStore.automations) {
) automationOptions.push({ value: automation._id, label: automation.name })
}
}) })
</script> </script>
@ -65,13 +107,23 @@
<Layout paddingX="XL" gap="S" alignContent="start"> <Layout paddingX="XL" gap="S" alignContent="start">
<div class="search"> <div class="search">
<div class="select"> <div class="select">
<Select placeholder="All automations" label="Automation" /> <Select
placeholder="All automations"
label="Automation"
bind:value={automationId}
options={automationOptions}
/>
</div> </div>
<div class="select"> <div class="select">
<Select placeholder="Past 30 days" label="Date range" /> <Select placeholder="Past 30 days" label="Date range" />
</div> </div>
<div class="select"> <div class="select">
<Select placeholder="All status" label="Status" /> <Select
placeholder="All status"
label="Status"
bind:value={status}
options={statusOptions}
/>
</div> </div>
</div> </div>
{#if runHistory} {#if runHistory}
@ -95,6 +147,15 @@
/> />
</div> </div>
</div> </div>
<div class="pagination">
<Pagination
page={pageNumber}
hasPrevPage={pageNumber > 1}
{hasNextPage}
{goToPrevPage}
{goToNextPage}
/>
</div>
<style> <style>
.root { .root {
@ -118,12 +179,11 @@
flex-basis: 150px; flex-basis: 150px;
} }
.separator { .pagination {
flex-grow: 1; position: absolute;
} bottom: 0;
margin-bottom: var(--spacing-xl);
.searchInput { margin-left: var(--spacing-l);
margin-top: auto;
} }
.panel { .panel {

View File

@ -10,7 +10,9 @@
<div class="cell"> <div class="cell">
<Icon {color} name={isError ? "Alert" : "CheckmarkCircle"} /> <Icon {color} name={isError ? "Alert" : "CheckmarkCircle"} />
<div class:green={!isError} class:red={isError}>{value}</div> <div class:green={!isError} class:red={isError}>
{isError ? "Error" : "Success"}
</div>
</div> </div>
<style> <style>

View File

@ -5,6 +5,7 @@
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte" import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte" import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte" import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
import { onMount } from "svelte"
$: automation = $: automation =
$automationStore.selectedAutomation?.automation || $automationStore.selectedAutomation?.automation ||
@ -12,6 +13,9 @@
let modal let modal
let webhookModal let webhookModal
onMount(() => {
$automationStore.showTestPanel = false
})
</script> </script>
<!-- routify:options index=3 --> <!-- routify:options index=3 -->

View File

@ -79,12 +79,14 @@ export const buildAutomationEndpoints = API => ({
* @param automationId The ID of the automation to get logs for. * @param automationId The ID of the automation to get logs for.
* @param startDate An ISO date string to state the start of the date range. * @param startDate An ISO date string to state the start of the date range.
*/ */
getAutomationLogs: async ({ automationId, startDate }) => { getAutomationLogs: async ({ automationId, startDate, status, page }) => {
return await API.post({ return await API.post({
url: "/api/automations/logs/search", url: "/api/automations/logs/search",
body: { body: {
automationId, automationId,
startDate, startDate,
status,
page,
}, },
}) })
}, },

View File

@ -1,6 +1,6 @@
const actions = require("../../automations/actions") const actions = require("../../automations/actions")
const triggers = require("../../automations/triggers") const triggers = require("../../automations/triggers")
const { getLogs, oneDayAgo } = require("../../automations/history") const { getLogs, oneDayAgo } = require("../../automations/logging")
const { getAutomationParams, generateAutomationID } = require("../../db/utils") const { getAutomationParams, generateAutomationID } = require("../../db/utils")
const { const {
checkForWebhooks, checkForWebhooks,
@ -152,11 +152,11 @@ exports.destroy = async function (ctx) {
} }
exports.logSearch = async function (ctx) { exports.logSearch = async function (ctx) {
const { automationId } = ctx.request.body const { automationId, status, page } = ctx.request.body
// TODO: check if there is a date range in the search params // TODO: check if there is a date range in the search params
// also check the date range vs their license, see if it is allowed // also check the date range vs their license, see if it is allowed
const startDate = oneDayAgo() const startDate = oneDayAgo()
ctx.body = await getLogs(startDate, automationId) ctx.body = await getLogs(startDate, status, automationId, page)
} }
exports.getActionList = async function (ctx) { exports.getActionList = async function (ctx) {

View File

@ -1,140 +0,0 @@
import {
AutomationLog,
AutomationResults,
AutomationStatus,
} from "../../definitions/automation"
import { getAppDB } from "@budibase/backend-core/context"
import {
generateAutomationLogID,
getAutomationLogParams,
getQueryIndex,
ViewNames,
} from "../../db/utils"
import { createLogByAutomationView } from "../../db/views/staticViews"
import { Automation } from "../../definitions/common"
const EARLIEST_DATE = new Date(0).toISOString()
const FREE_EXPIRY_SEC = 86400
const PRO_EXPIRY_SEC = FREE_EXPIRY_SEC * 30
function getStatus(results: AutomationResults) {
let status = AutomationStatus.SUCCESS
let first = true
for (let step of results.steps) {
// skip the trigger, its always successful if automation ran
if (first) {
first = false
continue
}
if (!step.outputs?.success) {
status = AutomationStatus.ERROR
}
}
return status
}
// export function oneMonthAgo() {
// return new Date(
// new Date().getTime() - PRO_EXPIRY_SEC * 1000
// ).toISOString()
// }
export function oneDayAgo() {
return new Date(new Date().getTime() - FREE_EXPIRY_SEC * 1000).toISOString()
}
async function clearOldHistory() {
const db = getAppDB()
// TODO: handle license lookup for deletion
const expiredEnd = oneDayAgo()
const results = await getAllLogs(EARLIEST_DATE, expiredEnd, {
include_docs: false,
})
const toDelete = results.map((doc: any) => ({
_id: doc.id,
_rev: doc.rev,
_deleted: true,
}))
await db.bulkDocs(toDelete)
}
async function getAllLogs(
startDate: string,
endDate: string,
opts: any = { include_docs: true }
) {
const db = getAppDB()
const queryParams: any = {
endDate,
startDate,
}
let response = (await db.allDocs(getAutomationLogParams(queryParams, opts)))
.rows
if (opts?.include_docs) {
response = response.map((row: any) => row.doc)
}
return response
}
async function getLogsByAutomationID(
automationId: string,
opts: { startDate?: string; endDate?: string } = {}
): Promise<AutomationLog[]> {
const db = getAppDB()
try {
const queryParams = {
startDate: opts?.startDate,
endDate: opts?.startDate,
automationId,
}
return (
await db.query(
getQueryIndex(ViewNames.LOGS_BY_AUTOMATION),
getAutomationLogParams(queryParams, { include_docs: true })
)
).rows.map((row: any) => row.doc)
} catch (err: any) {
if (err != null && err.name === "not_found") {
await createLogByAutomationView()
return getLogsByAutomationID(automationId, opts)
}
}
return []
}
export async function storeLog(
automation: Automation,
results: AutomationResults
) {
const automationId = automation._id
const name = automation.name
const db = getAppDB()
const isoDate = new Date().toISOString()
const id = generateAutomationLogID(automationId, isoDate)
await db.put({
// results contain automationId and status for view
...results,
automationId,
automationName: name,
status: getStatus(results),
createdAt: isoDate,
_id: id,
})
// clear up old history for app
await clearOldHistory()
}
export async function getLogs(startDate: string, automationId?: string) {
let logs: AutomationLog[]
let endDate = new Date().toISOString()
if (automationId) {
logs = await getLogsByAutomationID(automationId, {
startDate,
endDate,
})
} else {
logs = await getAllLogs(startDate, endDate)
}
return logs
}

View File

@ -0,0 +1,180 @@
import {
AutomationLog,
AutomationLogPage,
AutomationResults,
AutomationStatus,
} from "../../definitions/automation"
import { getAppDB } from "@budibase/backend-core/context"
import {
generateAutomationLogID,
getAutomationLogParams,
getQueryIndex,
ViewNames,
} from "../../db/utils"
import { createLogByAutomationView } from "../../db/views/staticViews"
import { Automation } from "../../definitions/common"
const PAGE_SIZE = 9
const EARLIEST_DATE = new Date(0).toISOString()
const FREE_EXPIRY_SEC = 86400
// const PRO_EXPIRY_SEC = FREE_EXPIRY_SEC * 30
function getStatus(results: AutomationResults) {
let status = AutomationStatus.SUCCESS
let first = true
for (let step of results.steps) {
// skip the trigger, its always successful if automation ran
if (first) {
first = false
continue
}
if (!step.outputs?.success) {
status = AutomationStatus.ERROR
}
}
return status
}
// export function oneMonthAgo() {
// return new Date(
// new Date().getTime() - PRO_EXPIRY_SEC * 1000
// ).toISOString()
// }
export function oneDayAgo() {
return new Date(new Date().getTime() - FREE_EXPIRY_SEC * 1000).toISOString()
}
async function clearOldHistory() {
const db = getAppDB()
// TODO: handle license lookup for deletion
const expiredEnd = oneDayAgo()
const results = await getAllLogs(EARLIEST_DATE, expiredEnd, {
docs: false,
})
const toDelete = results.data.map((doc: any) => ({
_id: doc.id,
_rev: doc.rev,
_deleted: true,
}))
await db.bulkDocs(toDelete)
}
function pagination(
response: any,
paginate: boolean = true
): AutomationLogPage {
const data = response.rows.map((row: any) => {
return row.doc ? row.doc : row
})
if (!paginate) {
return { data, hasNextPage: false }
}
const hasNextPage = data.length > PAGE_SIZE
return {
data: data.slice(0, PAGE_SIZE),
hasNextPage,
nextPage: hasNextPage ? data[PAGE_SIZE]?._id : undefined,
}
}
async function getAllLogs(
startDate: string,
endDate: string,
opts: {
docs: boolean
status?: string
paginate?: boolean
page?: string
} = { docs: true }
): Promise<AutomationLogPage> {
const db = getAppDB()
let optional: any = { status: opts.status }
const params = getAutomationLogParams(startDate, endDate, optional, {
include_docs: opts.docs,
limit: opts?.paginate ? PAGE_SIZE + 1 : undefined,
})
if (opts?.page) {
params.startkey = opts.page
}
let response = await db.allDocs(params)
return pagination(response, opts?.paginate)
}
async function getLogsByView(
startDate: string,
endDate: string,
viewParams: { automationId?: string; status?: string; page?: string } = {}
): Promise<AutomationLogPage> {
const db = getAppDB()
let response
try {
let optional = {
automationId: viewParams?.automationId,
status: viewParams?.status,
}
const params = getAutomationLogParams(startDate, endDate, optional, {
include_docs: true,
limit: PAGE_SIZE,
})
if (viewParams?.page) {
params.startkey = viewParams.page
}
response = await db.query(getQueryIndex(ViewNames.AUTO_LOGS), params)
} catch (err: any) {
if (err != null && err.name === "not_found") {
await createLogByAutomationView()
return getLogsByView(startDate, endDate, viewParams)
}
}
return pagination(response)
}
export async function storeLog(
automation: Automation,
results: AutomationResults
) {
const db = getAppDB()
const automationId = automation._id
const name = automation.name
const status = getStatus(results)
const isoDate = new Date().toISOString()
const id = generateAutomationLogID(isoDate, status, automationId)
await db.put({
// results contain automationId and status for view
...results,
automationId,
status,
automationName: name,
createdAt: isoDate,
_id: id,
})
// clear up old logging for app
await clearOldHistory()
}
export async function getLogs(
startDate: string,
status?: string,
automationId?: string,
page?: string
): Promise<AutomationLogPage> {
let response: AutomationLogPage
let endDate = new Date().toISOString()
if (automationId || status) {
response = await getLogsByView(startDate, endDate, {
automationId,
status,
page,
})
} else {
response = await getAllLogs(startDate, endDate, {
status,
page,
docs: true,
paginate: true,
})
}
return response
}

View File

@ -49,7 +49,13 @@ const DocumentTypes = {
const ViewNames = { const ViewNames = {
LINK: "by_link", LINK: "by_link",
ROUTING: "screen_routes", ROUTING: "screen_routes",
LOGS_BY_AUTOMATION: "log_by_auto", AUTO_LOGS: "auto_log",
}
const ViewModes = {
ALL: "all",
AUTOMATION: "auto",
STATUS: "status",
} }
const InternalTables = { const InternalTables = {
@ -77,6 +83,7 @@ exports.isProdAppID = isProdAppID
exports.USER_METDATA_PREFIX = `${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` exports.USER_METDATA_PREFIX = `${DocumentTypes.ROW}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}`
exports.LINK_USER_METADATA_PREFIX = `${DocumentTypes.LINK}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}` exports.LINK_USER_METADATA_PREFIX = `${DocumentTypes.LINK}${SEPARATOR}${InternalTables.USER_METADATA}${SEPARATOR}`
exports.ViewNames = ViewNames exports.ViewNames = ViewNames
exports.ViewModes = ViewModes
exports.InternalTables = InternalTables exports.InternalTables = InternalTables
exports.DocumentTypes = DocumentTypes exports.DocumentTypes = DocumentTypes
exports.SEPARATOR = SEPARATOR exports.SEPARATOR = SEPARATOR
@ -364,26 +371,32 @@ exports.getMemoryViewParams = (otherProps = {}) => {
return getDocParams(DocumentTypes.MEM_VIEW, null, otherProps) return getDocParams(DocumentTypes.MEM_VIEW, null, otherProps)
} }
exports.generateAutomationLogID = (automationId, isoDate) => { exports.generateAutomationLogID = (isoDate, status, automationId) => {
return `${DocumentTypes.AUTOMATION_LOG}${SEPARATOR}${isoDate}${SEPARATOR}${automationId}` return `${DocumentTypes.AUTOMATION_LOG}${SEPARATOR}${isoDate}${SEPARATOR}${automationId}`
} }
exports.getAutomationLogParams = ( exports.getAutomationLogParams = (
{ startDate, endDate, automationId } = {}, startDate,
endDate,
{ status, automationId } = {},
otherProps = {} otherProps = {}
) => { ) => {
const base = `${DocumentTypes.AUTOMATION_LOG}${SEPARATOR}` const automationBase = automationId ? `${automationId}${SEPARATOR}` : ""
let start = startDate || "", const statusBase = status ? `${status}${SEPARATOR}` : ""
end = endDate || "" let base
// reverse for view if (status && automationId) {
if (automationId) { base = `${ViewModes.ALL}${SEPARATOR}${statusBase}${automationBase}`
start = `${automationId}${SEPARATOR}${start}` } else if (status) {
end = `${automationId}${SEPARATOR}${end}` base = `${ViewModes.STATUS}${SEPARATOR}${statusBase}`
} else if (automationId) {
base = `${ViewModes.AUTOMATION}${SEPARATOR}${automationBase}`
} else {
base = `${DocumentTypes.AUTOMATION_LOG}${SEPARATOR}`
} }
return { return {
...otherProps, ...otherProps,
startkey: `${base}${start}`, startkey: `${base}${startDate}`,
endkey: `${base}${end}${UNICODE_MAX}`, endkey: `${base}${endDate}${UNICODE_MAX}`,
} }
} }

View File

@ -3,6 +3,7 @@ const {
DocumentTypes, DocumentTypes,
SEPARATOR, SEPARATOR,
ViewNames, ViewNames,
ViewModes,
SearchIndexes, SearchIndexes,
} = require("../utils") } = require("../utils")
const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR const SCREEN_PREFIX = DocumentTypes.SCREEN + SEPARATOR
@ -69,14 +70,20 @@ exports.createLogByAutomationView = async () => {
const view = { const view = {
map: `function(doc) { map: `function(doc) {
if (doc._id.startsWith("${LOG_PREFIX}")) { if (doc._id.startsWith("${LOG_PREFIX}")) {
let key = doc.automationId + ${SEPARATOR} + doc.createdAt let autoId = doc.automationId + "${SEPARATOR}"
emit(key, doc._id) let status = doc.status + "${SEPARATOR}"
let autoKey = "${ViewModes.AUTOMATION}${SEPARATOR}" + autoId + doc.createdAt
let statusKey = "${ViewModes.STATUS}${SEPARATOR}" + status + doc.createdAt
let allKey = "${ViewModes.ALL}${SEPARATOR}" + status + autoId + doc.createdAt
emit(statusKey)
emit(autoKey)
emit(allKey)
} }
}`, }`,
} }
designDoc.views = { designDoc.views = {
...designDoc.views, ...designDoc.views,
[ViewNames.LOGS_BY_AUTOMATION]: view, [ViewNames.AUTO_LOGS]: view,
} }
await db.put(designDoc) await db.put(designDoc)
} }

View File

@ -23,3 +23,9 @@ export interface AutomationLog extends AutomationResults {
_id: string _id: string
_rev: string _rev: string
} }
export interface AutomationLogPage {
data: AutomationLog[]
hasNextPage: boolean
nextPage?: string
}

View File

@ -9,7 +9,7 @@ const { doInTenant } = require("@budibase/backend-core/tenancy")
const { definitions: triggerDefs } = require("../automations/triggerInfo") const { definitions: triggerDefs } = require("../automations/triggerInfo")
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context") const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
const { AutomationErrors, LoopStepTypes } = require("../constants") const { AutomationErrors, LoopStepTypes } = require("../constants")
const { storeLog } = require("../automations/history") const { storeLog } = require("../automations/logging")
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
@ -326,7 +326,7 @@ class Orchestrator {
} }
} }
// store the history for the automation run // store the logs for the automation run
await storeLog(this._automation, this.executionOutput) await storeLog(this._automation, this.executionOutput)
return this.executionOutput return this.executionOutput
} }