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