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:
parent
4aba13ac39
commit
a2dc3dc3b1
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
})
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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">
|
||||
{#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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -96,27 +96,30 @@
|
|||
const automationErrors = {}
|
||||
for (let app of apps) {
|
||||
if (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}
|
||||
{#each Object.keys(automationErrors || {}) as appId}
|
||||
<Notification
|
||||
wide
|
||||
dismissable
|
||||
action={() => goToAutomationError(appId)}
|
||||
type="error"
|
||||
icon="Alert"
|
||||
actionMessage={errorCount(appId) > 1 ? "View errors" : "View error"}
|
||||
actionMessage={errorCount(automationErrors[appId]) > 1
|
||||
? "View errors"
|
||||
: "View error"}
|
||||
on:dismiss={async () => {
|
||||
await automationStore.actions.clearLogErrors({ appId })
|
||||
await apps.load()
|
||||
}}
|
||||
message={automationErrorMessage(appId)}
|
||||
/>
|
||||
{/each}
|
||||
{/if}
|
||||
<div class="title">
|
||||
<div class="welcome">
|
||||
<Layout noPadding gap="XS">
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 }),
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}`,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -105,4 +105,4 @@ export interface Automation extends Base {
|
|||
}
|
||||
}
|
||||
|
||||
export type MetadataErrors = { [key: string]: number }
|
||||
export type MetadataErrors = { [key: string]: string[] }
|
||||
|
|
Loading…
Reference in New Issue