forked from tsai/budibase
committed by
GitHub
48 changed files with 3249 additions and 2636 deletions
@ -1,42 +0,0 @@ |
|||
exports.SEPARATOR = "_" |
|||
exports.UNICODE_MAX = "\ufff0" |
|||
|
|||
const PRE_APP = "app" |
|||
const PRE_DEV = "dev" |
|||
|
|||
exports.DocumentTypes = { |
|||
USER: "us", |
|||
WORKSPACE: "workspace", |
|||
CONFIG: "config", |
|||
TEMPLATE: "template", |
|||
APP: PRE_APP, |
|||
DEV: PRE_DEV, |
|||
APP_DEV: `${PRE_APP}${exports.SEPARATOR}${PRE_DEV}`, |
|||
APP_METADATA: `${PRE_APP}${exports.SEPARATOR}metadata`, |
|||
ROLE: "role", |
|||
MIGRATIONS: "migrations", |
|||
DEV_INFO: "devinfo", |
|||
} |
|||
|
|||
exports.StaticDatabases = { |
|||
GLOBAL: { |
|||
name: "global-db", |
|||
docs: { |
|||
apiKeys: "apikeys", |
|||
usageQuota: "usage_quota", |
|||
licenseInfo: "license_info", |
|||
}, |
|||
}, |
|||
// contains information about tenancy and so on
|
|||
PLATFORM_INFO: { |
|||
name: "global-info", |
|||
docs: { |
|||
tenants: "tenants", |
|||
install: "install", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
exports.APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR |
|||
exports.APP_DEV = exports.APP_DEV_PREFIX = |
|||
exports.DocumentTypes.APP_DEV + exports.SEPARATOR |
|||
@ -0,0 +1,58 @@ |
|||
export const SEPARATOR = "_" |
|||
export const UNICODE_MAX = "\ufff0" |
|||
|
|||
/** |
|||
* Can be used to create a few different forms of querying a view. |
|||
*/ |
|||
export enum AutomationViewModes { |
|||
ALL = "all", |
|||
AUTOMATION = "automation", |
|||
STATUS = "status", |
|||
} |
|||
|
|||
export enum ViewNames { |
|||
USER_BY_EMAIL = "by_email", |
|||
BY_API_KEY = "by_api_key", |
|||
USER_BY_BUILDERS = "by_builders", |
|||
LINK = "by_link", |
|||
ROUTING = "screen_routes", |
|||
AUTOMATION_LOGS = "automation_logs", |
|||
} |
|||
|
|||
export enum DocumentTypes { |
|||
USER = "us", |
|||
WORKSPACE = "workspace", |
|||
CONFIG = "config", |
|||
TEMPLATE = "template", |
|||
APP = "app", |
|||
DEV = "dev", |
|||
APP_DEV = "app_dev", |
|||
APP_METADATA = "app_metadata", |
|||
ROLE = "role", |
|||
MIGRATIONS = "migrations", |
|||
DEV_INFO = "devinfo", |
|||
AUTOMATION_LOG = "log_au", |
|||
} |
|||
|
|||
export const StaticDatabases = { |
|||
GLOBAL: { |
|||
name: "global-db", |
|||
docs: { |
|||
apiKeys: "apikeys", |
|||
usageQuota: "usage_quota", |
|||
licenseInfo: "license_info", |
|||
}, |
|||
}, |
|||
// contains information about tenancy and so on
|
|||
PLATFORM_INFO: { |
|||
name: "global-info", |
|||
docs: { |
|||
tenants: "tenants", |
|||
install: "install", |
|||
}, |
|||
}, |
|||
} |
|||
|
|||
export const APP_PREFIX = exports.DocumentTypes.APP + exports.SEPARATOR |
|||
export const APP_DEV = exports.DocumentTypes.APP_DEV + exports.SEPARATOR |
|||
export const APP_DEV_PREFIX = APP_DEV |
|||
@ -0,0 +1,137 @@ |
|||
<script> |
|||
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui" |
|||
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte" |
|||
|
|||
export let automation |
|||
export let testResults |
|||
export let width = "400px" |
|||
|
|||
let showParameters |
|||
let blocks |
|||
|
|||
function prepTestResults(results) { |
|||
return results?.steps.filter(x => x.stepId !== "LOOP" || []) |
|||
} |
|||
|
|||
function textArea(results, message) { |
|||
if (!results) { |
|||
return message |
|||
} |
|||
return JSON.stringify(results, null, 2) |
|||
} |
|||
|
|||
$: filteredResults = prepTestResults(testResults) |
|||
|
|||
$: { |
|||
blocks = [] |
|||
if (automation) { |
|||
if (automation.definition.trigger) { |
|||
blocks.push(automation.definition.trigger) |
|||
} |
|||
blocks = blocks |
|||
.concat(automation.definition.steps || []) |
|||
.filter(x => x.stepId !== "LOOP") |
|||
} else if (filteredResults) { |
|||
blocks = filteredResults || [] |
|||
// make sure there is an ID for each block being displayed |
|||
let count = 0 |
|||
for (let block of blocks) { |
|||
block.id = count++ |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="container"> |
|||
{#each blocks as block, idx} |
|||
<div class="block" style={width ? `width: ${width}` : ""}> |
|||
{#if block.stepId !== "LOOP"} |
|||
<FlowItemHeader |
|||
showTestStatus={true} |
|||
bind:showParameters |
|||
{block} |
|||
isTrigger={idx === 0} |
|||
testResult={filteredResults?.[idx]} |
|||
/> |
|||
{#if showParameters && showParameters[block.id]} |
|||
<Divider noMargin /> |
|||
{#if filteredResults?.[idx]?.outputs.iterations} |
|||
<div style="display: flex; padding: 10px 10px 0px 12px;"> |
|||
<Icon name="Reuse" /> |
|||
<div style="margin-left: 10px;"> |
|||
<Label> |
|||
This loop ran {filteredResults?.[idx]?.outputs.iterations} times.</Label |
|||
> |
|||
</div> |
|||
</div> |
|||
{/if} |
|||
|
|||
<div class="tabs"> |
|||
<Tabs quiet noPadding selected="Input"> |
|||
<Tab title="Input"> |
|||
<div style="padding: 10px 10px 10px 10px;"> |
|||
<TextArea |
|||
minHeight="80px" |
|||
disabled |
|||
value={textArea(filteredResults?.[idx]?.inputs, "No input")} |
|||
/> |
|||
</div></Tab |
|||
> |
|||
<Tab title="Output"> |
|||
<div style="padding: 10px 10px 10px 10px;"> |
|||
<TextArea |
|||
minHeight="100px" |
|||
disabled |
|||
value={textArea( |
|||
filteredResults?.[idx]?.outputs, |
|||
"No output" |
|||
)} |
|||
/> |
|||
</div> |
|||
</Tab> |
|||
</Tabs> |
|||
</div> |
|||
{/if} |
|||
{/if} |
|||
</div> |
|||
{#if blocks.length - 1 !== idx} |
|||
<div class="separator" /> |
|||
{/if} |
|||
{/each} |
|||
</div> |
|||
|
|||
<style> |
|||
.container { |
|||
padding: 0 30px 0 30px; |
|||
height: 100%; |
|||
} |
|||
|
|||
.tabs { |
|||
display: flex; |
|||
flex-direction: column; |
|||
justify-content: flex-start; |
|||
align-items: stretch; |
|||
position: relative; |
|||
flex: 1 1 auto; |
|||
} |
|||
|
|||
.block { |
|||
display: inline-block; |
|||
width: 400px; |
|||
height: auto; |
|||
font-size: 16px; |
|||
background-color: var(--background); |
|||
border: 1px solid var(--spectrum-global-color-gray-300); |
|||
border-radius: 4px 4px 4px 4px; |
|||
} |
|||
|
|||
.separator { |
|||
width: 1px; |
|||
height: 40px; |
|||
border-left: 1px dashed var(--grey-4); |
|||
color: var(--grey-4); |
|||
/* center horizontally */ |
|||
text-align: center; |
|||
margin-left: 50%; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,6 @@ |
|||
<script> |
|||
import dayjs from "dayjs" |
|||
export let value |
|||
</script> |
|||
|
|||
{new dayjs(value).format("MMM D, YYYY HH:mm")} |
|||
@ -0,0 +1,91 @@ |
|||
<script> |
|||
import { Layout, Icon, ActionButton } from "@budibase/bbui" |
|||
import StatusRenderer from "./StatusRenderer.svelte" |
|||
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte" |
|||
import TestDisplay from "components/automation/AutomationBuilder/TestDisplay.svelte" |
|||
import { goto } from "@roxi/routify" |
|||
import { automationStore } from "builderStore" |
|||
|
|||
export let history |
|||
export let appId |
|||
export let close |
|||
|
|||
$: exists = $automationStore.automations?.find( |
|||
auto => auto._id === history?.automationId |
|||
) |
|||
</script> |
|||
|
|||
{#if history} |
|||
<div class="body"> |
|||
<div class="top"> |
|||
<div class="controls"> |
|||
<StatusRenderer value={history.status} /> |
|||
<ActionButton noPadding size="S" icon="Close" quiet on:click={close} /> |
|||
</div> |
|||
</div> |
|||
<Layout paddingX="XL" gap="S"> |
|||
<div class="icon"> |
|||
<Icon name="Clock" /> |
|||
<DateTimeRenderer value={history.createdAt} /> |
|||
</div> |
|||
<div class="icon"> |
|||
<Icon name="JourneyVoyager" /> |
|||
<div>{history.automationName}</div> |
|||
</div> |
|||
<div> |
|||
{#if exists} |
|||
<ActionButton |
|||
icon="Edit" |
|||
fullWidth={false} |
|||
on:click={() => |
|||
$goto(`../../../app/${appId}/automate/${history.automationId}`)} |
|||
>Edit automation</ActionButton |
|||
> |
|||
{/if} |
|||
</div> |
|||
</Layout> |
|||
<div class="bottom"> |
|||
{#key history} |
|||
<TestDisplay testResults={history} width="100%" /> |
|||
{/key} |
|||
</div> |
|||
</div> |
|||
{:else} |
|||
<div>No details found</div> |
|||
{/if} |
|||
|
|||
<style> |
|||
.body { |
|||
right: 0; |
|||
background-color: var(--background); |
|||
border-left: var(--border-light); |
|||
width: 420px; |
|||
height: calc(100vh - 240px); |
|||
position: fixed; |
|||
overflow: auto; |
|||
} |
|||
|
|||
.top { |
|||
padding: var(--spacing-m) 0 var(--spacing-m) 0; |
|||
border-bottom: var(--border-light); |
|||
} |
|||
|
|||
.bottom { |
|||
margin-top: var(--spacing-m); |
|||
border-top: var(--border-light); |
|||
padding-top: calc(var(--spacing-xl) * 2); |
|||
padding-bottom: calc(var(--spacing-xl) * 2); |
|||
} |
|||
|
|||
.icon { |
|||
display: flex; |
|||
gap: var(--spacing-m); |
|||
} |
|||
|
|||
.controls { |
|||
padding: 0 var(--spacing-l) 0 var(--spacing-l); |
|||
display: grid; |
|||
grid-template-columns: 1fr auto; |
|||
gap: var(--spacing-s); |
|||
} |
|||
</style> |
|||
@ -0,0 +1,218 @@ |
|||
<script> |
|||
import { Layout, Table, Select, Pagination } from "@budibase/bbui" |
|||
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte" |
|||
import StatusRenderer from "./StatusRenderer.svelte" |
|||
import HistoryDetailsPanel from "./HistoryDetailsPanel.svelte" |
|||
import { automationStore } from "builderStore" |
|||
import { createPaginationStore } from "helpers/pagination" |
|||
import { onMount } from "svelte" |
|||
import dayjs from "dayjs" |
|||
|
|||
const ERROR = "error", |
|||
SUCCESS = "success", |
|||
STOPPED = "stopped" |
|||
export let app |
|||
|
|||
let pageInfo = createPaginationStore() |
|||
let runHistory = null |
|||
let showPanel = false |
|||
let selectedHistory = null |
|||
let automationOptions = [] |
|||
let automationId = null |
|||
let status = null |
|||
let timeRange = null |
|||
|
|||
$: page = $pageInfo.page |
|||
$: fetchLogs(automationId, status, page, timeRange) |
|||
|
|||
const timeOptions = [ |
|||
{ value: "1-w", label: "Past week" }, |
|||
{ value: "1-d", label: "Past day" }, |
|||
{ value: "1-h", label: "Past 1 hour" }, |
|||
{ value: "15-m", label: "Past 15 mins" }, |
|||
{ value: "5-m", label: "Past 5 mins" }, |
|||
] |
|||
|
|||
const statusOptions = [ |
|||
{ value: SUCCESS, label: "Success" }, |
|||
{ value: ERROR, label: "Error" }, |
|||
{ value: STOPPED, label: "Stopped" }, |
|||
] |
|||
|
|||
const runHistorySchema = { |
|||
status: { displayName: "Status" }, |
|||
createdAt: { displayName: "Time" }, |
|||
automationName: { displayName: "Automation" }, |
|||
} |
|||
|
|||
const customRenderers = [ |
|||
{ column: "createdAt", component: DateTimeRenderer }, |
|||
{ column: "status", component: StatusRenderer }, |
|||
] |
|||
|
|||
async function fetchLogs(automationId, status, page, timeRange) { |
|||
let startDate = null |
|||
if (timeRange) { |
|||
const [length, units] = timeRange.split("-") |
|||
startDate = dayjs().subtract(length, units) |
|||
} |
|||
const response = await automationStore.actions.getLogs({ |
|||
automationId, |
|||
status, |
|||
page, |
|||
startDate, |
|||
}) |
|||
pageInfo.fetched(response.hasNextPage, response.nextPage) |
|||
runHistory = enrichHistory($automationStore.blockDefinitions, response.data) |
|||
} |
|||
|
|||
function enrichHistory(definitions, runHistory) { |
|||
if (!definitions) { |
|||
return [] |
|||
} |
|||
const finalHistory = [] |
|||
for (let history of runHistory) { |
|||
if (!history.steps) { |
|||
continue |
|||
} |
|||
let notFound = false |
|||
for (let step of history.steps) { |
|||
const trigger = definitions.TRIGGER[step.stepId], |
|||
action = definitions.ACTION[step.stepId] |
|||
if (!trigger && !action) { |
|||
notFound = true |
|||
break |
|||
} |
|||
step.icon = trigger ? trigger.icon : action.icon |
|||
step.name = trigger ? trigger.name : action.name |
|||
} |
|||
if (!notFound) { |
|||
finalHistory.push(history) |
|||
} |
|||
} |
|||
return finalHistory |
|||
} |
|||
|
|||
function viewDetails({ detail }) { |
|||
selectedHistory = detail |
|||
showPanel = true |
|||
} |
|||
|
|||
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(null, status) |
|||
if (shouldOpen) { |
|||
viewDetails({ detail: runHistory[0] }) |
|||
} |
|||
automationOptions = [] |
|||
for (let automation of $automationStore.automations) { |
|||
automationOptions.push({ value: automation._id, label: automation.name }) |
|||
} |
|||
}) |
|||
</script> |
|||
|
|||
<div class="root" class:panelOpen={showPanel}> |
|||
<Layout paddingX="XL" gap="S" alignContent="start"> |
|||
<div class="search"> |
|||
<div class="select"> |
|||
<Select |
|||
placeholder="All automations" |
|||
label="Automation" |
|||
bind:value={automationId} |
|||
options={automationOptions} |
|||
/> |
|||
</div> |
|||
<div class="select"> |
|||
<Select |
|||
placeholder="Past 30 days" |
|||
label="Date range" |
|||
bind:value={timeRange} |
|||
options={timeOptions} |
|||
/> |
|||
</div> |
|||
<div class="select"> |
|||
<Select |
|||
placeholder="All status" |
|||
label="Status" |
|||
bind:value={status} |
|||
options={statusOptions} |
|||
/> |
|||
</div> |
|||
</div> |
|||
{#if runHistory} |
|||
<Table |
|||
on:click={viewDetails} |
|||
schema={runHistorySchema} |
|||
allowSelectRows={false} |
|||
allowEditColumns={false} |
|||
allowEditRows={false} |
|||
data={runHistory} |
|||
{customRenderers} |
|||
placeholderText="No history found" |
|||
/> |
|||
{/if} |
|||
</Layout> |
|||
<div class="panel" class:panelShow={showPanel}> |
|||
<HistoryDetailsPanel |
|||
appId={app.devId} |
|||
bind:history={selectedHistory} |
|||
close={() => { |
|||
showPanel = false |
|||
}} |
|||
/> |
|||
</div> |
|||
</div> |
|||
<div class="pagination"> |
|||
<Pagination |
|||
page={$pageInfo.pageNumber} |
|||
hasPrevPage={$pageInfo.loading ? false : $pageInfo.hasPrevPage} |
|||
hasNextPage={$pageInfo.loading ? false : $pageInfo.hasNextPage} |
|||
goToPrevPage={pageInfo.prevPage} |
|||
goToNextPage={pageInfo.nextPage} |
|||
/> |
|||
</div> |
|||
|
|||
<style> |
|||
.root { |
|||
display: grid; |
|||
grid-template-columns: 1fr; |
|||
height: 100%; |
|||
} |
|||
|
|||
.search { |
|||
display: flex; |
|||
gap: var(--spacing-l); |
|||
width: 100%; |
|||
align-items: flex-end; |
|||
} |
|||
|
|||
.select { |
|||
flex-basis: 150px; |
|||
} |
|||
|
|||
.pagination { |
|||
position: absolute; |
|||
bottom: 0; |
|||
margin-bottom: var(--spacing-xl); |
|||
margin-left: var(--spacing-l); |
|||
} |
|||
|
|||
.panel { |
|||
display: none; |
|||
background-color: var(--background); |
|||
} |
|||
|
|||
.panelShow { |
|||
display: block; |
|||
} |
|||
|
|||
.panelOpen { |
|||
grid-template-columns: auto 420px; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,38 @@ |
|||
<script> |
|||
import { Icon } from "@budibase/bbui" |
|||
export let value |
|||
|
|||
$: isError = !value || value.toLowerCase() === "error" |
|||
$: isStopped = value?.toLowerCase() === "stopped" |
|||
$: status = getStatus(isError, isStopped) |
|||
|
|||
function getStatus(error, stopped) { |
|||
if (error) { |
|||
return { color: "var(--red)", message: "Error", icon: "Alert" } |
|||
} else if (stopped) { |
|||
return { color: "var(--yellow)", message: "Stopped", icon: "StopCircle" } |
|||
} else { |
|||
return { |
|||
color: "var(--green)", |
|||
message: "Success", |
|||
icon: "CheckmarkCircle", |
|||
} |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<div class="cell"> |
|||
<Icon color={status.color} name={status.icon} /> |
|||
<div style={`color: ${status.color};`}> |
|||
{status.message} |
|||
</div> |
|||
</div> |
|||
|
|||
<style> |
|||
.cell { |
|||
display: flex; |
|||
flex-direction: row; |
|||
gap: var(--spacing-m); |
|||
align-items: center; |
|||
} |
|||
</style> |
|||
@ -0,0 +1,35 @@ |
|||
import * as env from "../../environment" |
|||
import { AutomationResults, Automation, App } from "@budibase/types" |
|||
import { automations } from "@budibase/pro" |
|||
import { db as dbUtils } from "@budibase/backend-core" |
|||
|
|||
export async function storeLog( |
|||
automation: Automation, |
|||
results: AutomationResults |
|||
) { |
|||
// can disable this if un-needed in self-host, also only do this for prod apps
|
|||
if (env.DISABLE_AUTOMATION_LOGS) { |
|||
return |
|||
} |
|||
await automations.logs.storeLog(automation, results) |
|||
} |
|||
|
|||
export async function checkAppMetadata(apps: App[]) { |
|||
const maxStartDate = await automations.logs.oldestLogDate() |
|||
for (let metadata of apps) { |
|||
if (!metadata.automationErrors) { |
|||
continue |
|||
} |
|||
for (let [key, errors] of Object.entries(metadata.automationErrors)) { |
|||
const updated = [] |
|||
for (let error of errors) { |
|||
const startDate = error.split(dbUtils.SEPARATOR)[2] |
|||
if (startDate > maxStartDate) { |
|||
updated.push(error) |
|||
} |
|||
} |
|||
metadata.automationErrors[key] = updated |
|||
} |
|||
} |
|||
return apps |
|||
} |
|||
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue