Getting notifications working correctly, linking to errors in a better way, generally improving UI, getting some final touches here and there.

This commit is contained in:
mike12345567 2022-06-22 20:23:18 +01:00
parent 4aba13ac39
commit a2dc3dc3b1
12 changed files with 167 additions and 69 deletions

View File

@ -37,6 +37,7 @@
export let autoSortColumns = true export let autoSortColumns = true
export let compact = false export let compact = false
export let customPlaceholder = false export let customPlaceholder = false
export let placeholderText = "No rows found"
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
@ -405,7 +406,7 @@
> >
<use xlink:href="#spectrum-icon-18-Table" /> <use xlink:href="#spectrum-icon-18-Table" />
</svg> </svg>
<div>No rows found</div> <div>{placeholderText}</div>
</div> </div>
{/if} {/if}
</div> </div>

View File

@ -129,7 +129,12 @@ const automationActions = store => ({
page, page,
}) })
}, },
clearLogErrors: async () => {}, clearLogErrors: async ({ automationId, appId } = {}) => {
return await API.clearAutomationLogErrors({
automationId,
appId,
})
},
addTestDataToAutomation: data => { addTestDataToAutomation: data => {
store.update(state => { store.update(state => {
state.selectedAutomation.addTestData(data) state.selectedAutomation.addTestData(data)
@ -138,11 +143,10 @@ const automationActions = store => ({
}, },
addBlockToAutomation: (block, blockIdx) => { addBlockToAutomation: (block, blockIdx) => {
store.update(state => { store.update(state => {
const newBlock = state.selectedAutomation.addBlock( state.selectedBlock = state.selectedAutomation.addBlock(
cloneDeep(block), cloneDeep(block),
blockIdx blockIdx
) )
state.selectedBlock = newBlock
return state return state
}) })
}, },

View File

@ -103,6 +103,7 @@
<style> <style>
.container { .container {
padding: 0 30px 0 30px; padding: 0 30px 0 30px;
height: 100%;
} }
.tabs { .tabs {
@ -117,6 +118,7 @@
.block { .block {
display: inline-block; display: inline-block;
width: 400px; width: 400px;
height: auto;
font-size: 16px; font-size: 16px;
background-color: var(--background); background-color: var(--background);
border: 1px solid var(--spectrum-global-color-gray-300); border: 1px solid var(--spectrum-global-color-gray-300);

View File

@ -26,7 +26,7 @@
<Layout paddingX="XL" gap="S"> <Layout paddingX="XL" gap="S">
<div class="icon"> <div class="icon">
<Icon name="Clock" /> <Icon name="Clock" />
<DateTimeRenderer value={history.timestamp} /> <DateTimeRenderer value={history.createdAt} />
</div> </div>
<div class="icon"> <div class="icon">
<Icon name="JourneyVoyager" /> <Icon name="JourneyVoyager" />
@ -45,7 +45,9 @@
</div> </div>
</Layout> </Layout>
<div class="bottom"> <div class="bottom">
<TestDisplay testResults={history} width="100%" /> {#key history}
<TestDisplay testResults={history} width="100%" />
{/key}
</div> </div>
</div> </div>
{:else} {:else}
@ -54,10 +56,13 @@
<style> <style>
.body { .body {
right: 0;
background-color: var(--background); background-color: var(--background);
border-left: var(--border-light); border-left: var(--border-light);
height: 100%; width: 420px;
width: 100%; height: calc(100vh - 240px);
position: fixed;
overflow: auto;
} }
.top { .top {
@ -69,7 +74,7 @@
margin-top: var(--spacing-m); margin-top: var(--spacing-m);
border-top: var(--border-light); border-top: var(--border-light);
padding-top: calc(var(--spacing-xl) * 2); padding-top: calc(var(--spacing-xl) * 2);
height: 100%; padding-bottom: calc(var(--spacing-xl) * 2);
} }
.icon { .icon {

View File

@ -6,14 +6,17 @@
import { automationStore } from "builderStore" import { automationStore } from "builderStore"
import { onMount } from "svelte" import { onMount } from "svelte"
const ERROR = "error",
SUCCESS = "success"
export let app export let app
let runHistory = [] let runHistory = null
let showPanel = false let showPanel = false
let selectedHistory = null let selectedHistory = null
let automationOptions = [] let automationOptions = []
let automationId = null let automationId = null
let status = null let status = null
let timeRange = null
let prevPage, let prevPage,
nextPage, nextPage,
page, page,
@ -22,9 +25,17 @@
$: fetchLogs(automationId, status, page) $: fetchLogs(automationId, status, page)
const timeOptions = [
{ value: "1w", label: "Past week" },
{ value: "1d", label: "Past day" },
{ value: "1h", label: "Past 1 hour" },
{ value: "15m", label: "Past 15 mins" },
{ value: "5m", label: "Past 5 mins" },
]
const statusOptions = [ const statusOptions = [
{ value: "success", label: "Success" }, { value: SUCCESS, label: "Success" },
{ value: "error", label: "Error" }, { value: ERROR, label: "Error" },
] ]
const runHistorySchema = { const runHistorySchema = {
@ -94,8 +105,17 @@
} }
onMount(async () => { onMount(async () => {
const params = new URLSearchParams(window.location.search)
const shouldOpen = params.get("open") === ERROR
// open with errors, open panel for latest
if (shouldOpen) {
status = ERROR
}
await automationStore.actions.fetch() await automationStore.actions.fetch()
await fetchLogs() await fetchLogs(null, status)
if (shouldOpen) {
viewDetails({ detail: runHistory[0] })
}
automationOptions = [] automationOptions = []
for (let automation of $automationStore.automations) { for (let automation of $automationStore.automations) {
automationOptions.push({ value: automation._id, label: automation.name }) automationOptions.push({ value: automation._id, label: automation.name })
@ -115,7 +135,12 @@
/> />
</div> </div>
<div class="select"> <div class="select">
<Select placeholder="Past 30 days" label="Date range" /> <Select
placeholder="Past 30 days"
label="Date range"
bind:value={timeRange}
options={timeOptions}
/>
</div> </div>
<div class="select"> <div class="select">
<Select <Select
@ -126,7 +151,7 @@
/> />
</div> </div>
</div> </div>
{#if runHistory && runHistory.length} {#if runHistory}
<Table <Table
on:click={viewDetails} on:click={viewDetails}
schema={runHistorySchema} schema={runHistorySchema}
@ -135,6 +160,7 @@
allowEditRows={false} allowEditRows={false}
data={runHistory} data={runHistory}
{customRenderers} {customRenderers}
placeholderText="No history found"
/> />
{/if} {/if}
</Layout> </Layout>
@ -165,10 +191,6 @@
height: 100%; height: 100%;
} }
.panelOpen {
grid-template-columns: auto 420px;
}
.search { .search {
display: flex; display: flex;
gap: var(--spacing-l); gap: var(--spacing-l);
@ -189,15 +211,14 @@
.panel { .panel {
display: none; display: none;
position: absolute;
right: 0;
height: 100%;
width: 420px;
overflow: hidden;
background-color: var(--background); background-color: var(--background);
} }
.panelShow { .panelShow {
display: block; display: block;
} }
.panelOpen {
grid-template-columns: auto 420px;
}
</style> </style>

View File

@ -96,27 +96,30 @@
const automationErrors = {} const automationErrors = {}
for (let app of apps) { for (let app of apps) {
if (app.automationErrors) { if (app.automationErrors) {
automationErrors[app.devId] = app.automationErrors if (errorCount(app.automationErrors) > 0) {
automationErrors[app.devId] = app.automationErrors
}
} }
} }
return automationErrors return automationErrors
} }
const goToAutomationError = appId => { const goToAutomationError = appId => {
const params = new URLSearchParams({ tab: "Automation History" }) const params = new URLSearchParams({
tab: "Automation History",
open: "error",
})
$goto(`../overview/${appId}?${params.toString()}`) $goto(`../overview/${appId}?${params.toString()}`)
} }
const errorCount = appId => { const errorCount = errors => {
return Object.values(automationErrors[appId]).reduce( return Object.values(errors).reduce((acc, next) => acc + next.length, 0)
(prev, next) => prev + next,
0
)
} }
const automationErrorMessage = appId => { const automationErrorMessage = appId => {
const app = enrichedApps.find(app => app.devId === appId) const app = enrichedApps.find(app => app.devId === appId)
return `${app.name} - Automation error (${errorCount(appId)})` const errors = automationErrors[appId]
return `${app.name} - Automation error (${errorCount(errors)})`
} }
const initiateAppCreation = () => { const initiateAppCreation = () => {
@ -238,19 +241,23 @@
<Page wide> <Page wide>
<Layout noPadding gap="M"> <Layout noPadding gap="M">
{#if loaded} {#if loaded}
{#if automationErrors} {#each Object.keys(automationErrors || {}) as appId}
{#each Object.keys(automationErrors) as appId} <Notification
<Notification wide
wide dismissable
dismissable action={() => goToAutomationError(appId)}
action={() => goToAutomationError(appId)} type="error"
type="error" icon="Alert"
icon="Alert" actionMessage={errorCount(automationErrors[appId]) > 1
actionMessage={errorCount(appId) > 1 ? "View errors" : "View error"} ? "View errors"
message={automationErrorMessage(appId)} : "View error"}
/> on:dismiss={async () => {
{/each} await automationStore.actions.clearLogErrors({ appId })
{/if} await apps.load()
}}
message={automationErrorMessage(appId)}
/>
{/each}
<div class="title"> <div class="title">
<div class="welcome"> <div class="welcome">
<Layout noPadding gap="XS"> <Layout noPadding gap="XS">

View File

@ -78,6 +78,8 @@ export const buildAutomationEndpoints = API => ({
* Get the logs for the app, or by automation ID. * Get the logs for the app, or by automation ID.
* @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.
* @param status The status, error or success.
* @param page The page to retrieve.
*/ */
getAutomationLogs: async ({ automationId, startDate, status, page }) => { getAutomationLogs: async ({ automationId, startDate, status, page }) => {
return await API.post({ return await API.post({
@ -90,4 +92,20 @@ export const buildAutomationEndpoints = API => ({
}, },
}) })
}, },
/**
* Clears automation log errors (which are creating notification) for
* automation or the app.
* @param automationId optional - the ID of the automation to clear errors for.
* @param appId The app ID to clear errors for.
*/
clearAutomationLogErrors: async ({ automationId, appId }) => {
return await API.delete({
url: "/api/automations/logs",
body: {
appId,
automationId,
},
})
},
}) })

View File

@ -1,7 +1,11 @@
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/logging") const { getLogs, oneDayAgo } = require("../../automations/logging")
const { getAutomationParams, generateAutomationID } = require("../../db/utils") const {
getAutomationParams,
generateAutomationID,
DocumentTypes,
} = require("../../db/utils")
const { const {
checkForWebhooks, checkForWebhooks,
updateTestHistory, updateTestHistory,
@ -10,8 +14,13 @@ const {
const { deleteEntityMetadata } = require("../../utilities") const { deleteEntityMetadata } = require("../../utilities")
const { MetadataTypes } = require("../../constants") const { MetadataTypes } = require("../../constants")
const { setTestFlag, clearTestFlag } = require("../../utilities/redis") const { setTestFlag, clearTestFlag } = require("../../utilities/redis")
const { getAppDB } = require("@budibase/backend-core/context") const {
getAppDB,
getProdAppDB,
doInAppContext,
} = require("@budibase/backend-core/context")
const { events } = require("@budibase/backend-core") const { events } = require("@budibase/backend-core")
const { app } = require("@budibase/backend-core/cache")
const ACTION_DEFS = removeDeprecated(actions.ACTION_DEFINITIONS) const ACTION_DEFS = removeDeprecated(actions.ACTION_DEFINITIONS)
const TRIGGER_DEFS = removeDeprecated(triggers.TRIGGER_DEFINITIONS) const TRIGGER_DEFS = removeDeprecated(triggers.TRIGGER_DEFINITIONS)
@ -192,6 +201,25 @@ exports.logSearch = async function (ctx) {
ctx.body = await getLogs(startDate, status, automationId, page) ctx.body = await getLogs(startDate, status, automationId, page)
} }
exports.clearLogError = async function (ctx) {
const { automationId, appId } = ctx.request.body
await doInAppContext(appId, async () => {
const db = getProdAppDB()
const metadata = await db.get(DocumentTypes.APP_METADATA)
if (!automationId) {
delete metadata.automationErrors
} else if (
metadata.automationErrors &&
metadata.automationErrors[automationId]
) {
delete metadata.automationErrors[automationId]
}
await db.put(metadata)
await app.invalidateAppMetadata(metadata.appId, metadata)
ctx.body = { message: `Error logs cleared.` }
})
}
exports.getActionList = async function (ctx) { exports.getActionList = async function (ctx) {
ctx.body = ACTION_DEFS ctx.body = ACTION_DEFS
} }

View File

@ -51,17 +51,22 @@ router
automationValidator(false), automationValidator(false),
controller.create controller.create
) )
.post(
"/api/automations/logs/search",
authorized(BUILDER),
controller.logSearch
)
.delete(
"/api/automations/logs",
authorized(BUILDER),
controller.clearLogError
)
.delete( .delete(
"/api/automations/:id/:rev", "/api/automations/:id/:rev",
paramResource("id"), paramResource("id"),
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

@ -69,13 +69,10 @@ async function clearOldHistory() {
const status = parts[parts.length - 1] const status = parts[parts.length - 1]
return status === AutomationStatus.ERROR return status === AutomationStatus.ERROR
}) })
.map((doc: any) => { .map((doc: any) => doc.id)
const parts = doc.id.split(SEPARATOR)
return `${parts[parts.length - 3]}${SEPARATOR}${parts[parts.length - 2]}`
})
await db.bulkDocs(toDelete) await db.bulkDocs(toDelete)
if (errorLogIds.length) { if (errorLogIds.length) {
await updateAppMetadataWithErrors(errorLogIds) await updateAppMetadataWithErrors(errorLogIds, { clearing: true })
} }
} }
@ -150,25 +147,34 @@ async function getLogsByView(
} }
async function updateAppMetadataWithErrors( async function updateAppMetadataWithErrors(
automationIds: string[], logIds: string[],
{ clearing } = { clearing: false } { clearing } = { clearing: false }
) { ) {
const db = getProdAppDB() const db = getProdAppDB()
// this will try multiple times with a delay between to update the metadata // this will try multiple times with a delay between to update the metadata
await backOff(async () => { await backOff(async () => {
const metadata = await db.get(DocumentTypes.APP_METADATA) const metadata = await db.get(DocumentTypes.APP_METADATA)
for (let automationId of automationIds) { for (let logId of logIds) {
const parts = logId.split(SEPARATOR)
const autoId = `${parts[parts.length - 3]}${SEPARATOR}${
parts[parts.length - 2]
}`
let errors: MetadataErrors = {} let errors: MetadataErrors = {}
if (metadata.automationErrors) { if (metadata.automationErrors) {
errors = metadata.automationErrors as MetadataErrors errors = metadata.automationErrors as MetadataErrors
} }
const change = clearing ? -1 : 1 if (!Array.isArray(errors[autoId])) {
errors[automationId] = errors[automationId] errors[autoId] = []
? errors[automationId] + change }
: 1 const idx = errors[autoId].indexOf(logId)
if (clearing && idx !== -1) {
errors[autoId].splice(idx, 1)
} else {
errors[autoId].push(logId)
}
// if clearing and reach zero, this will pass and will remove the element // if clearing and reach zero, this will pass and will remove the element
if (!errors[automationId]) { if (errors[autoId].length === 0) {
delete errors[automationId] delete errors[autoId]
} }
metadata.automationErrors = errors metadata.automationErrors = errors
} }
@ -204,7 +210,7 @@ export async function storeLog(
// need to note on the app metadata that there is an error, store what the error is // need to note on the app metadata that there is an error, store what the error is
if (status === AutomationStatus.ERROR) { if (status === AutomationStatus.ERROR) {
await updateAppMetadataWithErrors([automation._id as string]) await updateAppMetadataWithErrors([id])
} }
// clear up old logging for app // clear up old logging for app

View File

@ -396,8 +396,9 @@ exports.getAutomationLogParams = (
} }
return { return {
...otherProps, ...otherProps,
startkey: `${base}${startDate}`, descending: true,
endkey: `${base}${endDate}${UNICODE_MAX}`, startkey: `${base}${endDate}${UNICODE_MAX}`,
endkey: `${base}${startDate}`,
} }
} }

View File

@ -105,4 +105,4 @@ export interface Automation extends Base {
} }
} }
export type MetadataErrors = { [key: string]: number } export type MetadataErrors = { [key: string]: string[] }