Adding real history to the history tab, getting storage to couch and retrieval + api working.

This commit is contained in:
mike12345567 2022-06-01 16:01:06 +01:00
parent 702f4057bd
commit 34759c7916
15 changed files with 175 additions and 98 deletions

View File

@ -6,6 +6,7 @@ import analytics, { Events } from "analytics"
const initialAutomationState = { const initialAutomationState = {
automations: [], automations: [],
showTestPanel: false,
blockDefinitions: { blockDefinitions: {
TRIGGER: [], TRIGGER: [],
ACTION: [], ACTION: [],
@ -121,6 +122,12 @@ const automationActions = store => ({
return state return state
}) })
}, },
getLogs: async (automationId, startDate) => {
return await API.getAutomationLogs({
automationId,
startDate,
})
},
addTestDataToAutomation: data => { addTestDataToAutomation: data => {
store.update(state => { store.update(state => {
state.selectedAutomation.addTestData(data) state.selectedAutomation.addTestData(data)

View File

@ -65,7 +65,7 @@
<ActionButton <ActionButton
disabled={!$automationStore.selectedAutomation?.testResults} disabled={!$automationStore.selectedAutomation?.testResults}
on:click={() => { on:click={() => {
$automationStore.selectedAutomation.automation.showTestPanel = true $automationStore.showTestPanel = true
}} }}
size="M">Test Details</ActionButton size="M">Test Details</ActionButton
> >

View File

@ -51,7 +51,7 @@
$automationStore.selectedAutomation?.automation, $automationStore.selectedAutomation?.automation,
testData testData
) )
$automationStore.selectedAutomation.automation.showTestPanel = true $automationStore.showTestPanel = true
} catch (error) { } catch (error) {
notifications.error("Error testing notification") notifications.error("Error testing notification")
} }

View File

@ -10,7 +10,7 @@
let blocks let blocks
function prepTestResults(results) { function prepTestResults(results) {
return results.steps.filter(x => x.stepId !== "LOOP" || []) return results?.steps.filter(x => x.stepId !== "LOOP" || [])
} }
function textArea(results, message) { function textArea(results, message) {

View File

@ -36,7 +36,7 @@
<div style="padding-right: var(--spacing-xl)"> <div style="padding-right: var(--spacing-xl)">
<Icon <Icon
on:click={async () => { on:click={async () => {
$automationStore.selectedAutomation.automation.showTestPanel = false $automationStore.showTestPanel = false
}} }}
hoverable hoverable
name="Close" name="Close"

View File

@ -14,12 +14,12 @@
const runHistorySchema = { const runHistorySchema = {
status: { displayName: "Status" }, status: { displayName: "Status" },
timestamp: { displayName: "Time" }, createdAt: { displayName: "Time" },
name: { displayName: "Automation" }, automationName: { displayName: "Automation" },
} }
const customRenderers = [ const customRenderers = [
{ column: "time", component: DateTimeRenderer }, { column: "createdAt", component: DateTimeRenderer },
{ column: "status", component: StatusRenderer }, { column: "status", component: StatusRenderer },
] ]
@ -54,57 +54,10 @@
onMount(async () => { onMount(async () => {
let definitions = await automationStore.actions.definitions() let definitions = await automationStore.actions.definitions()
runHistory = enrichHistory(definitions, [ runHistory = enrichHistory(
{ definitions,
status: "Success", await automationStore.actions.getLogs()
timestamp: "2022-05-11T16:06:14.438Z", )
name: "automation name",
steps: [
{
stepId: "ROW_SAVED",
inputs: null,
outputs: {
id: "awd",
revision: "awd",
row: {
tableId: "ta_240cfde36405479fa814b8a2c46655b5",
name: "",
suppliers: [],
"supplier name": "",
_id: "awd",
_rev: "awd",
},
},
},
{
stepId: "SERVER_LOG",
inputs: {
text: "awdawdawd",
},
outputs: {
success: true,
},
},
],
},
{
status: "Error",
timestamp: "2022-05-11T16:03:14.438Z",
name: "automation name",
steps: [
{
stepId: "ROW_SAVED",
inputs: {},
outputs: {},
},
{
stepId: "SEND_EMAIL_SMTP",
inputs: {},
outputs: {},
},
],
},
])
}) })
</script> </script>

View File

@ -2,7 +2,7 @@
import { Icon } from "@budibase/bbui" import { Icon } from "@budibase/bbui"
export let value export let value
$: isError = value === "Error" $: isError = !value || value.toLowerCase() === "error"
$: color = isError $: color = isError
? "var(--spectrum-semantic-negative-color-background)" ? "var(--spectrum-semantic-negative-color-background)"
: "var(--green)" : "var(--green)"

View File

@ -45,7 +45,7 @@
{/if} {/if}
</div> </div>
{#if automation?.showTestPanel} {#if $automationStore.showTestPanel}
<div class="setup"> <div class="setup">
<TestPanel {automation} /> <TestPanel {automation} />
</div> </div>

View File

@ -73,4 +73,19 @@ export const buildAutomationEndpoints = API => ({
url: `/api/automations/${automationId}/${automationRev}`, url: `/api/automations/${automationId}/${automationRev}`,
}) })
}, },
/**
* Get the logs for the app, or by automation ID.
* @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.
*/
getAutomationLogs: async ({ automationId, startDate }) => {
return await API.post({
url: "/api/automations/logs/search",
body: {
automationId,
startDate,
},
})
},
}) })

View File

@ -1,5 +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 { getAutomationParams, generateAutomationID } = require("../../db/utils") const { getAutomationParams, generateAutomationID } = require("../../db/utils")
const { const {
checkForWebhooks, checkForWebhooks,
@ -150,6 +151,14 @@ exports.destroy = async function (ctx) {
ctx.body = await db.remove(automationId, ctx.params.rev) ctx.body = await db.remove(automationId, ctx.params.rev)
} }
exports.logSearch = async function (ctx) {
const { automationId } = ctx.request.body
// 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
const startDate = oneDayAgo()
ctx.body = await getLogs(startDate, automationId)
}
exports.getActionList = async function (ctx) { exports.getActionList = async function (ctx) {
ctx.body = ACTION_DEFS ctx.body = ACTION_DEFS
} }

View File

@ -57,6 +57,11 @@ router
authorized(BUILDER), authorized(BUILDER),
controller.destroy controller.destroy
) )
.post(
"/api/automations/logs/search",
authorized(BUILDER),
controller.logSearch
)
.post( .post(
"/api/automations/:id/trigger", "/api/automations/:id/trigger",
appInfoMiddleware({ appType: AppType.PROD }), appInfoMiddleware({ appType: AppType.PROD }),

View File

@ -1,24 +1,55 @@
import { AutomationResults } from "../../definitions/automation" import {
AutomationLog,
AutomationResults,
AutomationStatus,
} from "../../definitions/automation"
import { getAppDB } from "@budibase/backend-core/context" import { getAppDB } from "@budibase/backend-core/context"
import { import {
generateAutomationLogID, generateAutomationLogID,
getAutomationLogParams, getAutomationLogParams,
// getQueryIndex, getQueryIndex,
// ViewNames, ViewNames,
} from "../../db/utils" } from "../../db/utils"
// import { createLogByAutomationView } from "../../db/views/staticViews" import { createLogByAutomationView } from "../../db/views/staticViews"
import { Automation } from "../../definitions/common"
const EARLIEST_DATE = new Date(0).toISOString()
const FREE_EXPIRY_SEC = 86400 const FREE_EXPIRY_SEC = 86400
// const PRO_EXPIRY_SEC = FREE_EXPIRY_SEC * 30 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() { async function clearOldHistory() {
const db = getAppDB() const db = getAppDB()
// TODO: handle license lookup for deletion // TODO: handle license lookup for deletion
const time = new Date(new Date().getTime() - FREE_EXPIRY_SEC * 1000) const expiredEnd = oneDayAgo()
const queryParams: any = { const results = await getAllLogs(EARLIEST_DATE, expiredEnd, {
endIso: time, include_docs: false,
} })
const results = await db.allDocs(getAutomationLogParams(queryParams))
const toDelete = results.map((doc: any) => ({ const toDelete = results.map((doc: any) => ({
_id: doc.id, _id: doc.id,
_rev: doc.rev, _rev: doc.rev,
@ -27,36 +58,66 @@ async function clearOldHistory() {
await db.bulkDocs(toDelete) await db.bulkDocs(toDelete)
} }
// async function getByAutomationID(automationId: string) { async function getAllLogs(
// const db = getAppDB() startDate: string,
// try { endDate: string,
// const queryParams: any = { opts: any = { include_docs: true }
// 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 getByAutomationID(automationId)
// }
// }
// }
export async function storeHistory(
automationId: string,
results: AutomationResults
) { ) {
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 db = getAppDB()
const isoDate = new Date().toISOString() const isoDate = new Date().toISOString()
const id = generateAutomationLogID(isoDate, automationId) const id = generateAutomationLogID(automationId, isoDate)
await db.put({ await db.put({
// results contain automationId and status for view // results contain automationId and status for view
...results, ...results,
automationId,
automationName: name,
status: getStatus(results),
createdAt: isoDate, createdAt: isoDate,
_id: id, _id: id,
}) })
@ -64,4 +125,16 @@ export async function storeHistory(
await clearOldHistory() await clearOldHistory()
} }
export async function retrieve() {} 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

@ -369,12 +369,12 @@ exports.generateAutomationLogID = (automationId, isoDate) => {
} }
exports.getAutomationLogParams = ( exports.getAutomationLogParams = (
{ startIso, endIso, automationId } = {}, { startDate, endDate, automationId } = {},
otherProps = {} otherProps = {}
) => { ) => {
const base = `${DocumentTypes.AUTOMATION_LOG}${SEPARATOR}` const base = `${DocumentTypes.AUTOMATION_LOG}${SEPARATOR}`
let start = startIso || "", let start = startDate || "",
end = endIso || "" end = endDate || ""
// reverse for view // reverse for view
if (automationId) { if (automationId) {
start = `${automationId}${SEPARATOR}${start}` start = `${automationId}${SEPARATOR}${start}`

View File

@ -1,6 +1,12 @@
export enum AutomationStatus {
SUCCESS = "success",
ERROR = "error",
}
export interface AutomationResults { export interface AutomationResults {
automationId: string automationId: string
status: string status: string
trigger?: any
steps: { steps: {
stepId: string stepId: string
inputs: { inputs: {
@ -11,3 +17,9 @@ export interface AutomationResults {
} }
}[] }[]
} }
export interface AutomationLog extends AutomationResults {
createdAt: string
_id: string
_rev: string
}

View File

@ -9,6 +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 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
@ -325,6 +326,8 @@ class Orchestrator {
} }
} }
// store the history for the automation run
await storeLog(this._automation, this.executionOutput)
return this.executionOutput return this.executionOutput
} }
} }