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

View File

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

View File

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

View File

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

View File

@ -6,14 +6,17 @@
import { automationStore } from "builderStore"
import { onMount } from "svelte"
const ERROR = "error",
SUCCESS = "success"
export let app
let runHistory = []
let runHistory = null
let showPanel = false
let selectedHistory = null
let automationOptions = []
let automationId = null
let status = null
let timeRange = null
let prevPage,
nextPage,
page,
@ -22,9 +25,17 @@
$: 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 = [
{ value: "success", label: "Success" },
{ value: "error", label: "Error" },
{ value: SUCCESS, label: "Success" },
{ value: ERROR, label: "Error" },
]
const runHistorySchema = {
@ -94,8 +105,17 @@
}
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 fetchLogs()
await fetchLogs(null, status)
if (shouldOpen) {
viewDetails({ detail: runHistory[0] })
}
automationOptions = []
for (let automation of $automationStore.automations) {
automationOptions.push({ value: automation._id, label: automation.name })
@ -115,7 +135,12 @@
/>
</div>
<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 class="select">
<Select
@ -126,7 +151,7 @@
/>
</div>
</div>
{#if runHistory && runHistory.length}
{#if runHistory}
<Table
on:click={viewDetails}
schema={runHistorySchema}
@ -135,6 +160,7 @@
allowEditRows={false}
data={runHistory}
{customRenderers}
placeholderText="No history found"
/>
{/if}
</Layout>
@ -165,10 +191,6 @@
height: 100%;
}
.panelOpen {
grid-template-columns: auto 420px;
}
.search {
display: flex;
gap: var(--spacing-l);
@ -189,15 +211,14 @@
.panel {
display: none;
position: absolute;
right: 0;
height: 100%;
width: 420px;
overflow: hidden;
background-color: var(--background);
}
.panelShow {
display: block;
}
.panelOpen {
grid-template-columns: auto 420px;
}
</style>

View File

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

View File

@ -78,6 +78,8 @@ export const buildAutomationEndpoints = API => ({
* 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.
* @param status The status, error or success.
* @param page The page to retrieve.
*/
getAutomationLogs: async ({ automationId, startDate, status, page }) => {
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 triggers = require("../../automations/triggers")
const { getLogs, oneDayAgo } = require("../../automations/logging")
const { getAutomationParams, generateAutomationID } = require("../../db/utils")
const {
getAutomationParams,
generateAutomationID,
DocumentTypes,
} = require("../../db/utils")
const {
checkForWebhooks,
updateTestHistory,
@ -10,8 +14,13 @@ const {
const { deleteEntityMetadata } = require("../../utilities")
const { MetadataTypes } = require("../../constants")
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 { app } = require("@budibase/backend-core/cache")
const ACTION_DEFS = removeDeprecated(actions.ACTION_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)
}
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) {
ctx.body = ACTION_DEFS
}

View File

@ -51,17 +51,22 @@ router
automationValidator(false),
controller.create
)
.post(
"/api/automations/logs/search",
authorized(BUILDER),
controller.logSearch
)
.delete(
"/api/automations/logs",
authorized(BUILDER),
controller.clearLogError
)
.delete(
"/api/automations/:id/:rev",
paramResource("id"),
authorized(BUILDER),
controller.destroy
)
.post(
"/api/automations/logs/search",
authorized(BUILDER),
controller.logSearch
)
.post(
"/api/automations/:id/trigger",
appInfoMiddleware({ appType: AppType.PROD }),

View File

@ -69,13 +69,10 @@ async function clearOldHistory() {
const status = parts[parts.length - 1]
return status === AutomationStatus.ERROR
})
.map((doc: any) => {
const parts = doc.id.split(SEPARATOR)
return `${parts[parts.length - 3]}${SEPARATOR}${parts[parts.length - 2]}`
})
.map((doc: any) => doc.id)
await db.bulkDocs(toDelete)
if (errorLogIds.length) {
await updateAppMetadataWithErrors(errorLogIds)
await updateAppMetadataWithErrors(errorLogIds, { clearing: true })
}
}
@ -150,25 +147,34 @@ async function getLogsByView(
}
async function updateAppMetadataWithErrors(
automationIds: string[],
logIds: string[],
{ clearing } = { clearing: false }
) {
const db = getProdAppDB()
// this will try multiple times with a delay between to update the metadata
await backOff(async () => {
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 = {}
if (metadata.automationErrors) {
errors = metadata.automationErrors as MetadataErrors
}
const change = clearing ? -1 : 1
errors[automationId] = errors[automationId]
? errors[automationId] + change
: 1
if (!Array.isArray(errors[autoId])) {
errors[autoId] = []
}
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 (!errors[automationId]) {
delete errors[automationId]
if (errors[autoId].length === 0) {
delete errors[autoId]
}
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
if (status === AutomationStatus.ERROR) {
await updateAppMetadataWithErrors([automation._id as string])
await updateAppMetadataWithErrors([id])
}
// clear up old logging for app

View File

@ -396,8 +396,9 @@ exports.getAutomationLogParams = (
}
return {
...otherProps,
startkey: `${base}${startDate}`,
endkey: `${base}${endDate}${UNICODE_MAX}`,
descending: true,
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[] }