Merge pull request #6170 from Budibase/feature/automation-logs
Automation logs
This commit is contained in:
commit
bc4b099da5
|
@ -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
|
|
@ -1,7 +1,7 @@
|
||||||
import { newid } from "../hashing"
|
import { newid } from "../hashing"
|
||||||
import { DEFAULT_TENANT_ID, Configs } from "../constants"
|
import { DEFAULT_TENANT_ID, Configs } from "../constants"
|
||||||
import env from "../environment"
|
import env from "../environment"
|
||||||
import { SEPARATOR, DocumentTypes, UNICODE_MAX } from "./constants"
|
import { SEPARATOR, DocumentTypes, UNICODE_MAX, ViewNames } from "./constants"
|
||||||
import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy"
|
import { getTenantId, getGlobalDBName, getGlobalDB } from "../tenancy"
|
||||||
import fetch from "node-fetch"
|
import fetch from "node-fetch"
|
||||||
import { doWithDB, allDbs } from "./index"
|
import { doWithDB, allDbs } from "./index"
|
||||||
|
@ -12,12 +12,6 @@ import { isDevApp, isDevAppID } from "./conversions"
|
||||||
import { APP_PREFIX } from "./constants"
|
import { APP_PREFIX } from "./constants"
|
||||||
import * as events from "../events"
|
import * as events from "../events"
|
||||||
|
|
||||||
export const ViewNames = {
|
|
||||||
USER_BY_EMAIL: "by_email",
|
|
||||||
BY_API_KEY: "by_api_key",
|
|
||||||
USER_BY_BUILDERS: "by_builders",
|
|
||||||
}
|
|
||||||
|
|
||||||
export * from "./constants"
|
export * from "./constants"
|
||||||
export * from "./conversions"
|
export * from "./conversions"
|
||||||
export { default as Replication } from "./Replication"
|
export { default as Replication } from "./Replication"
|
||||||
|
@ -61,6 +55,13 @@ export function getDocParams(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the correct index for a view based on default design DB.
|
||||||
|
*/
|
||||||
|
export function getQueryIndex(viewName: ViewNames) {
|
||||||
|
return `database/${viewName}`
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generates a new workspace ID.
|
* Generates a new workspace ID.
|
||||||
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
* @returns {string} The new workspace ID which the workspace doc can be stored under.
|
||||||
|
|
|
@ -13,6 +13,7 @@ import deprovisioning from "./context/deprovision"
|
||||||
import auth from "./auth"
|
import auth from "./auth"
|
||||||
import constants from "./constants"
|
import constants from "./constants"
|
||||||
import * as dbConstants from "./db/constants"
|
import * as dbConstants from "./db/constants"
|
||||||
|
import logging from "./logging"
|
||||||
|
|
||||||
// mimic the outer package exports
|
// mimic the outer package exports
|
||||||
import * as db from "./pkg/db"
|
import * as db from "./pkg/db"
|
||||||
|
@ -49,6 +50,7 @@ const core = {
|
||||||
deprovisioning,
|
deprovisioning,
|
||||||
installation,
|
installation,
|
||||||
errors,
|
errors,
|
||||||
|
logging,
|
||||||
...errorClasses,
|
...errorClasses,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
export let size = "M"
|
export let size = "M"
|
||||||
export let active = false
|
export let active = false
|
||||||
export let fullWidth = false
|
export let fullWidth = false
|
||||||
|
export let noPadding = false
|
||||||
|
|
||||||
function longPress(element) {
|
function longPress(element) {
|
||||||
if (!longPressable) return
|
if (!longPressable) return
|
||||||
|
@ -41,6 +42,7 @@
|
||||||
class:spectrum-ActionButton--quiet={quiet}
|
class:spectrum-ActionButton--quiet={quiet}
|
||||||
class:spectrum-ActionButton--emphasized={emphasized}
|
class:spectrum-ActionButton--emphasized={emphasized}
|
||||||
class:is-selected={selected}
|
class:is-selected={selected}
|
||||||
|
class:noPadding
|
||||||
class:fullWidth
|
class:fullWidth
|
||||||
class="spectrum-ActionButton spectrum-ActionButton--size{size}"
|
class="spectrum-ActionButton spectrum-ActionButton--size{size}"
|
||||||
class:active
|
class:active
|
||||||
|
@ -80,4 +82,8 @@
|
||||||
.active svg {
|
.active svg {
|
||||||
color: var(--spectrum-global-color-blue-600);
|
color: var(--spectrum-global-color-blue-600);
|
||||||
}
|
}
|
||||||
|
.noPadding {
|
||||||
|
padding: 0;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,15 +1,20 @@
|
||||||
<script>
|
<script>
|
||||||
|
import { ActionButton } from "../"
|
||||||
|
|
||||||
import { createEventDispatcher } from "svelte"
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
export let type = "info"
|
export let type = "info"
|
||||||
export let icon = "Info"
|
export let icon = "Info"
|
||||||
export let message = ""
|
export let message = ""
|
||||||
export let dismissable = false
|
export let dismissable = false
|
||||||
|
export let actionMessage = null
|
||||||
|
export let action = null
|
||||||
|
export let wide = false
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
const dispatch = createEventDispatcher()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="spectrum-Toast spectrum-Toast--{type}">
|
<div class="spectrum-Toast spectrum-Toast--{type}" class:wide>
|
||||||
{#if icon}
|
{#if icon}
|
||||||
<svg
|
<svg
|
||||||
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon"
|
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Toast-typeIcon"
|
||||||
|
@ -19,8 +24,13 @@
|
||||||
<use xlink:href="#spectrum-icon-18-{icon}" />
|
<use xlink:href="#spectrum-icon-18-{icon}" />
|
||||||
</svg>
|
</svg>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="spectrum-Toast-body">
|
<div class="spectrum-Toast-body" class:actionBody={!!action}>
|
||||||
<div class="spectrum-Toast-content">{message || ""}</div>
|
<div class="spectrum-Toast-content">{message || ""}</div>
|
||||||
|
{#if action}
|
||||||
|
<ActionButton quiet emphasized on:click={action}>
|
||||||
|
<div style="color: white; font-weight: 600;">{actionMessage}</div>
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if dismissable}
|
{#if dismissable}
|
||||||
<div class="spectrum-Toast-buttons">
|
<div class="spectrum-Toast-buttons">
|
||||||
|
@ -46,4 +56,15 @@
|
||||||
.spectrum-Toast {
|
.spectrum-Toast {
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.wide {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actionBody {
|
||||||
|
justify-content: space-between;
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -8,13 +8,15 @@
|
||||||
|
|
||||||
<Portal target=".modal-container">
|
<Portal target=".modal-container">
|
||||||
<div class="notifications">
|
<div class="notifications">
|
||||||
{#each $notifications as { type, icon, message, id, dismissable } (id)}
|
{#each $notifications as { type, icon, message, id, dismissable, action, wide } (id)}
|
||||||
<div transition:fly={{ y: -30 }}>
|
<div transition:fly={{ y: -30 }}>
|
||||||
<Notification
|
<Notification
|
||||||
{type}
|
{type}
|
||||||
{icon}
|
{icon}
|
||||||
{message}
|
{message}
|
||||||
{dismissable}
|
{dismissable}
|
||||||
|
{action}
|
||||||
|
{wide}
|
||||||
on:dismiss={() => notifications.dismiss(id)}
|
on:dismiss={() => notifications.dismiss(id)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,7 +20,16 @@ export const createNotificationStore = () => {
|
||||||
setTimeout(() => (block = false), timeout)
|
setTimeout(() => (block = false), timeout)
|
||||||
}
|
}
|
||||||
|
|
||||||
const send = (message, type = "default", icon = "", autoDismiss = true) => {
|
const send = (
|
||||||
|
message,
|
||||||
|
{
|
||||||
|
type = "default",
|
||||||
|
icon = "",
|
||||||
|
autoDismiss = true,
|
||||||
|
action = null,
|
||||||
|
wide = false,
|
||||||
|
}
|
||||||
|
) => {
|
||||||
if (block) {
|
if (block) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -28,7 +37,15 @@ export const createNotificationStore = () => {
|
||||||
_notifications.update(state => {
|
_notifications.update(state => {
|
||||||
return [
|
return [
|
||||||
...state,
|
...state,
|
||||||
{ id: _id, type, message, icon, dismissable: !autoDismiss },
|
{
|
||||||
|
id: _id,
|
||||||
|
type,
|
||||||
|
message,
|
||||||
|
icon,
|
||||||
|
dismissable: !autoDismiss,
|
||||||
|
action,
|
||||||
|
wide,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
if (autoDismiss) {
|
if (autoDismiss) {
|
||||||
|
@ -50,10 +67,11 @@ export const createNotificationStore = () => {
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
send,
|
send,
|
||||||
info: msg => send(msg, "info", "Info"),
|
info: msg => send(msg, { type: "info", icon: "Info" }),
|
||||||
error: msg => send(msg, "error", "Alert", false),
|
error: msg =>
|
||||||
warning: msg => send(msg, "warning", "Alert"),
|
send(msg, { type: "error", icon: "Alert", autoDismiss: false }),
|
||||||
success: msg => send(msg, "success", "CheckmarkCircle"),
|
warning: msg => send(msg, { type: "warning", icon: "Alert" }),
|
||||||
|
success: msg => send(msg, { type: "success", icon: "CheckmarkCircle" }),
|
||||||
blockNotifications,
|
blockNotifications,
|
||||||
dismiss: dismissNotification,
|
dismiss: dismissNotification,
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -77,6 +77,7 @@
|
||||||
"@spectrum-css/page": "^3.0.1",
|
"@spectrum-css/page": "^3.0.1",
|
||||||
"@spectrum-css/vars": "^3.0.1",
|
"@spectrum-css/vars": "^3.0.1",
|
||||||
"codemirror": "^5.59.0",
|
"codemirror": "^5.59.0",
|
||||||
|
"dayjs": "^1.11.2",
|
||||||
"downloadjs": "1.4.7",
|
"downloadjs": "1.4.7",
|
||||||
"lodash": "4.17.21",
|
"lodash": "4.17.21",
|
||||||
"posthog-js": "1.4.5",
|
"posthog-js": "1.4.5",
|
||||||
|
|
|
@ -5,6 +5,7 @@ import { cloneDeep } from "lodash/fp"
|
||||||
|
|
||||||
const initialAutomationState = {
|
const initialAutomationState = {
|
||||||
automations: [],
|
automations: [],
|
||||||
|
showTestPanel: false,
|
||||||
blockDefinitions: {
|
blockDefinitions: {
|
||||||
TRIGGER: [],
|
TRIGGER: [],
|
||||||
ACTION: [],
|
ACTION: [],
|
||||||
|
@ -19,6 +20,17 @@ export const getAutomationStore = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const automationActions = store => ({
|
const automationActions = store => ({
|
||||||
|
definitions: async () => {
|
||||||
|
const response = await API.getAutomationDefinitions()
|
||||||
|
store.update(state => {
|
||||||
|
state.blockDefinitions = {
|
||||||
|
TRIGGER: response.trigger,
|
||||||
|
ACTION: response.action,
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
return response
|
||||||
|
},
|
||||||
fetch: async () => {
|
fetch: async () => {
|
||||||
const responses = await Promise.all([
|
const responses = await Promise.all([
|
||||||
API.getAutomations(),
|
API.getAutomations(),
|
||||||
|
@ -109,6 +121,20 @@ const automationActions = store => ({
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
getLogs: async ({ automationId, startDate, status, page } = {}) => {
|
||||||
|
return await API.getAutomationLogs({
|
||||||
|
automationId,
|
||||||
|
startDate,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
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)
|
||||||
|
@ -117,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
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
|
@ -65,7 +65,7 @@
|
||||||
<ActionButton
|
<ActionButton
|
||||||
disabled={!$automationStore.selectedAutomation?.testResults}
|
disabled={!$automationStore.selectedAutomation?.testResults}
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$automationStore.selectedAutomation.automation.showTestPanel = true
|
$automationStore.showTestPanel = true
|
||||||
}}
|
}}
|
||||||
size="M">Test Details</ActionButton
|
size="M">Test Details</ActionButton
|
||||||
>
|
>
|
||||||
|
|
|
@ -1,6 +1,4 @@
|
||||||
<script>
|
<script>
|
||||||
import FlowItemHeader from "./FlowItemHeader.svelte"
|
|
||||||
|
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
import {
|
import {
|
||||||
Icon,
|
Icon,
|
||||||
|
@ -16,6 +14,7 @@
|
||||||
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
import AutomationBlockSetup from "../../SetupPanel/AutomationBlockSetup.svelte"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import ActionModal from "./ActionModal.svelte"
|
import ActionModal from "./ActionModal.svelte"
|
||||||
|
import FlowItemHeader from "./FlowItemHeader.svelte"
|
||||||
|
|
||||||
export let block
|
export let block
|
||||||
export let testDataModal
|
export let testDataModal
|
||||||
|
|
|
@ -7,12 +7,19 @@
|
||||||
export let blockComplete
|
export let blockComplete
|
||||||
export let showTestStatus = false
|
export let showTestStatus = false
|
||||||
export let showParameters = {}
|
export let showParameters = {}
|
||||||
|
export let testResult
|
||||||
|
export let isTrigger
|
||||||
|
|
||||||
$: testResult =
|
$: {
|
||||||
$automationStore.selectedAutomation?.testResults?.steps.filter(step =>
|
if (!testResult) {
|
||||||
block.id ? step.id === block.id : step.stepId === block.stepId
|
testResult =
|
||||||
)
|
$automationStore.selectedAutomation?.testResults?.steps.filter(step =>
|
||||||
$: isTrigger = block.type === "TRIGGER"
|
block.id ? step.id === block.id : step.stepId === block.stepId
|
||||||
|
)[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: isTrigger = isTrigger || block.type === "TRIGGER"
|
||||||
|
$: status = updateStatus(testResult, isTrigger)
|
||||||
|
|
||||||
async function onSelect(block) {
|
async function onSelect(block) {
|
||||||
await automationStore.update(state => {
|
await automationStore.update(state => {
|
||||||
|
@ -20,6 +27,19 @@
|
||||||
return state
|
return state
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateStatus(results, isTrigger) {
|
||||||
|
if (!results) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
if (results.outputs?.status?.toLowerCase() === "stopped") {
|
||||||
|
return { yellow: true, message: "Stopped" }
|
||||||
|
} else if (results.outputs?.success || isTrigger) {
|
||||||
|
return { positive: true, message: "Success" }
|
||||||
|
} else {
|
||||||
|
return { negative: true, message: "Error" }
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="blockSection">
|
<div class="blockSection">
|
||||||
|
@ -60,16 +80,13 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="blockTitle">
|
<div class="blockTitle">
|
||||||
{#if showTestStatus && testResult && testResult[0]}
|
{#if showTestStatus && testResult}
|
||||||
<div style="float: right;">
|
<div style="float: right;">
|
||||||
<StatusLight
|
<StatusLight
|
||||||
positive={isTrigger || testResult[0].outputs?.success}
|
positive={status?.positive}
|
||||||
negative={!testResult[0].outputs?.success}
|
yellow={status?.yellow}
|
||||||
><Body size="XS"
|
negative={status?.negative}
|
||||||
>{testResult[0].outputs?.success || isTrigger
|
><Body size="XS">{status?.message}</Body></StatusLight
|
||||||
? "Success"
|
|
||||||
: "Error"}</Body
|
|
||||||
></StatusLight
|
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -51,7 +51,7 @@
|
||||||
$automationStore.selectedAutomation?.automation,
|
$automationStore.selectedAutomation?.automation,
|
||||||
testData
|
testData
|
||||||
)
|
)
|
||||||
$automationStore.selectedAutomation.automation.showTestPanel = true
|
$automationStore.showTestPanel = true
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error("Error testing notification")
|
notifications.error("Error testing notification")
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -1,11 +1,11 @@
|
||||||
<script>
|
<script>
|
||||||
import { Icon, Divider, Tabs, Tab, TextArea, Label } from "@budibase/bbui"
|
import { Icon, Divider } from "@budibase/bbui"
|
||||||
import FlowItemHeader from "./FlowChart/FlowItemHeader.svelte"
|
import TestDisplay from "./TestDisplay.svelte"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore } from "builderStore"
|
||||||
|
|
||||||
export let automation
|
export let automation
|
||||||
|
export let testResults
|
||||||
|
|
||||||
let showParameters
|
|
||||||
let blocks
|
let blocks
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -17,13 +17,15 @@
|
||||||
blocks = blocks
|
blocks = blocks
|
||||||
.concat(automation.definition.steps || [])
|
.concat(automation.definition.steps || [])
|
||||||
.filter(x => x.stepId !== "LOOP")
|
.filter(x => x.stepId !== "LOOP")
|
||||||
|
} else if (testResults) {
|
||||||
|
blocks = testResults.steps || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$: {
|
||||||
|
if (!testResults) {
|
||||||
|
testResults = $automationStore.selectedAutomation?.testResults
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: testResults =
|
|
||||||
$automationStore.selectedAutomation?.testResults?.steps.filter(
|
|
||||||
x => x.stepId !== "LOOP" || []
|
|
||||||
)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="title">
|
<div class="title">
|
||||||
|
@ -34,7 +36,7 @@
|
||||||
<div style="padding-right: var(--spacing-xl)">
|
<div style="padding-right: var(--spacing-xl)">
|
||||||
<Icon
|
<Icon
|
||||||
on:click={async () => {
|
on:click={async () => {
|
||||||
$automationStore.selectedAutomation.automation.showTestPanel = false
|
$automationStore.showTestPanel = false
|
||||||
}}
|
}}
|
||||||
hoverable
|
hoverable
|
||||||
name="Close"
|
name="Close"
|
||||||
|
@ -44,59 +46,9 @@
|
||||||
|
|
||||||
<Divider />
|
<Divider />
|
||||||
|
|
||||||
<div class="container">
|
<TestDisplay {automation} {testResults} />
|
||||||
{#each blocks as block, idx}
|
|
||||||
<div class="block">
|
|
||||||
{#if block.stepId !== "LOOP"}
|
|
||||||
<FlowItemHeader showTestStatus={true} bind:showParameters {block} />
|
|
||||||
{#if showParameters && showParameters[block.id]}
|
|
||||||
<Divider noMargin />
|
|
||||||
{#if testResults?.[idx]?.outputs.iterations}
|
|
||||||
<div style="display: flex; padding: 10px 10px 0px 12px;">
|
|
||||||
<Icon name="Reuse" />
|
|
||||||
<div style="margin-left: 10px;">
|
|
||||||
<Label>
|
|
||||||
This loop ran {testResults?.[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={JSON.stringify(testResults?.[idx]?.inputs, null, 2)}
|
|
||||||
/>
|
|
||||||
</div></Tab
|
|
||||||
>
|
|
||||||
<Tab title="Output">
|
|
||||||
<div style="padding: 10px 10px 10px 10px;">
|
|
||||||
<TextArea
|
|
||||||
minHeight="100px"
|
|
||||||
disabled
|
|
||||||
value={JSON.stringify(testResults?.[idx]?.outputs, null, 2)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if blocks.length - 1 !== idx}
|
|
||||||
<div class="separator" />
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.container {
|
|
||||||
padding: 0px 30px 0px 30px;
|
|
||||||
}
|
|
||||||
.title {
|
.title {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -106,15 +58,6 @@
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tabs {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: stretch;
|
|
||||||
position: relative;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-text {
|
.title-text {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
@ -124,23 +67,4 @@
|
||||||
.title :global(h1) {
|
.title :global(h1) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.block {
|
|
||||||
display: inline-block;
|
|
||||||
width: 400px;
|
|
||||||
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>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
<script>
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{new dayjs(value).format("MMM D, YYYY HH:mm")}
|
|
@ -52,7 +52,7 @@
|
||||||
reviewPendingDeployments(deployments, newDeployments)
|
reviewPendingDeployments(deployments, newDeployments)
|
||||||
return newDeployments
|
return newDeployments
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error fetching deployment history")
|
notifications.error("Error fetching deployment overview")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
deployments = newDeployments
|
deployments = newDeployments
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
clearInterval(poll)
|
clearInterval(poll)
|
||||||
notifications.error("Error fetching deployment history")
|
notifications.error("Error fetching deployment overview")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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>
|
|
@ -5,6 +5,7 @@
|
||||||
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
||||||
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
|
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
$: automation =
|
$: automation =
|
||||||
$automationStore.selectedAutomation?.automation ||
|
$automationStore.selectedAutomation?.automation ||
|
||||||
|
@ -12,6 +13,9 @@
|
||||||
|
|
||||||
let modal
|
let modal
|
||||||
let webhookModal
|
let webhookModal
|
||||||
|
onMount(() => {
|
||||||
|
$automationStore.showTestPanel = false
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- routify:options index=3 -->
|
<!-- routify:options index=3 -->
|
||||||
|
@ -45,7 +49,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if automation?.showTestPanel}
|
{#if $automationStore.showTestPanel}
|
||||||
<div class="setup">
|
<div class="setup">
|
||||||
<TestPanel {automation} />
|
<TestPanel {automation} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
Modal,
|
Modal,
|
||||||
Page,
|
Page,
|
||||||
notifications,
|
notifications,
|
||||||
|
Notification,
|
||||||
Body,
|
Body,
|
||||||
Search,
|
Search,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
|
@ -37,6 +38,7 @@
|
||||||
let searchTerm = ""
|
let searchTerm = ""
|
||||||
let cloud = $admin.cloud
|
let cloud = $admin.cloud
|
||||||
let creatingFromTemplate = false
|
let creatingFromTemplate = false
|
||||||
|
let automationErrors
|
||||||
|
|
||||||
const resolveWelcomeMessage = (auth, apps) => {
|
const resolveWelcomeMessage = (auth, apps) => {
|
||||||
const userWelcome = auth?.user?.firstName
|
const userWelcome = auth?.user?.firstName
|
||||||
|
@ -59,7 +61,8 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
$: lockedApps = filteredApps.filter(app => app?.lockedYou || app?.lockedOther)
|
$: lockedApps = filteredApps.filter(app => app?.lockedYou || app?.lockedOther)
|
||||||
$: unlocked = lockedApps?.length == 0
|
$: unlocked = lockedApps?.length === 0
|
||||||
|
$: automationErrors = getAutomationErrors(enrichedApps)
|
||||||
|
|
||||||
const enrichApps = (apps, user, sortBy) => {
|
const enrichApps = (apps, user, sortBy) => {
|
||||||
const enrichedApps = apps.map(app => ({
|
const enrichedApps = apps.map(app => ({
|
||||||
|
@ -89,6 +92,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getAutomationErrors = apps => {
|
||||||
|
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",
|
||||||
|
open: "error",
|
||||||
|
})
|
||||||
|
$goto(`../overview/${appId}?${params.toString()}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
const errors = automationErrors[appId]
|
||||||
|
return `${app.name} - Automation error (${errorCount(errors)})`
|
||||||
|
}
|
||||||
|
|
||||||
const initiateAppCreation = () => {
|
const initiateAppCreation = () => {
|
||||||
if ($apps?.length) {
|
if ($apps?.length) {
|
||||||
$goto("/builder/portal/apps/create")
|
$goto("/builder/portal/apps/create")
|
||||||
|
@ -208,6 +241,23 @@
|
||||||
<Page wide>
|
<Page wide>
|
||||||
<Layout noPadding gap="M">
|
<Layout noPadding gap="M">
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
|
{#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="title">
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<Layout noPadding gap="XS">
|
<Layout noPadding gap="XS">
|
||||||
|
|
|
@ -27,13 +27,23 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
let pageInfo = createPaginationStore()
|
let pageInfo = createPaginationStore()
|
||||||
let search = undefined
|
let prevSearch = undefined,
|
||||||
|
search = undefined
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchUsers(page, search)
|
$: fetchUsers(page, search)
|
||||||
|
|
||||||
let createUserModal
|
let createUserModal
|
||||||
|
|
||||||
async function fetchUsers(page, search) {
|
async function fetchUsers(page, search) {
|
||||||
|
if ($pageInfo.loading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// need to remove the page if they've started searching
|
||||||
|
if (search && !prevSearch) {
|
||||||
|
pageInfo.reset()
|
||||||
|
page = undefined
|
||||||
|
}
|
||||||
|
prevSearch = search
|
||||||
try {
|
try {
|
||||||
pageInfo.loading()
|
pageInfo.loading()
|
||||||
await users.search({ page, search })
|
await users.search({ page, search })
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
import AppLockModal from "components/common/AppLockModal.svelte"
|
import AppLockModal from "components/common/AppLockModal.svelte"
|
||||||
import EditableIcon from "components/common/EditableIcon.svelte"
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import HistoryTab from "components/portal/overview/automation/HistoryTab.svelte"
|
||||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
import { onDestroy, onMount } from "svelte"
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
|
||||||
|
@ -187,6 +188,10 @@
|
||||||
})
|
})
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
if (params.get("tab")) {
|
||||||
|
selectedTab = params.get("tab")
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
if (!apps.length) {
|
if (!apps.length) {
|
||||||
await apps.load()
|
await apps.load()
|
||||||
|
@ -211,7 +216,7 @@
|
||||||
<ProgressCircle size="XL" />
|
<ProgressCircle size="XL" />
|
||||||
</div>
|
</div>
|
||||||
{:then _}
|
{:then _}
|
||||||
<Layout paddingX="XXL" paddingY="XXL" gap="XL">
|
<Layout paddingX="XXL" paddingY="XL" gap="L">
|
||||||
<span class="page-header" class:loaded>
|
<span class="page-header" class:loaded>
|
||||||
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
||||||
Back
|
Back
|
||||||
|
@ -299,10 +304,10 @@
|
||||||
on:unpublish={e => unpublishApp(e.detail)}
|
on:unpublish={e => unpublishApp(e.detail)}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
<Tab title="Automation History">
|
||||||
|
<HistoryTab app={selectedApp} />
|
||||||
|
</Tab>
|
||||||
{#if false}
|
{#if false}
|
||||||
<Tab title="Automation History">
|
|
||||||
<div class="container">Automation History contents</div>
|
|
||||||
</Tab>
|
|
||||||
<Tab title="Backups">
|
<Tab title="Backups">
|
||||||
<div class="container">Backups contents</div>
|
<div class="container">Backups contents</div>
|
||||||
</Tab>
|
</Tab>
|
||||||
|
|
|
@ -197,6 +197,7 @@
|
||||||
.overview-tab .top {
|
.overview-tab .top {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overview-tab .bottom {
|
.overview-tab .bottom {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
@ -214,29 +215,35 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-text,
|
.status-text,
|
||||||
.last-edit-text {
|
.last-edit-text {
|
||||||
color: var(--spectrum-global-color-gray-600);
|
color: var(--spectrum-global-color-gray-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.updated-by {
|
.updated-by {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.succeeded :global(.icon) {
|
.succeeded :global(.icon) {
|
||||||
color: var(--spectrum-global-color-green-600);
|
color: var(--spectrum-global-color-green-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
.failed :global(.icon) {
|
.failed :global(.icon) {
|
||||||
color: var(
|
color: var(
|
||||||
--spectrum-semantic-negative-color-default,
|
--spectrum-semantic-negative-color-default,
|
||||||
var(--spectrum-global-color-red-500)
|
var(--spectrum-global-color-red-500)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
.metric-info {
|
.metric-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: var(--spacing-l);
|
gap: var(--spacing-l);
|
||||||
margin-top: var(--spacing-s);
|
margin-top: var(--spacing-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
.version-status,
|
.version-status,
|
||||||
.last-edit-text,
|
.last-edit-text,
|
||||||
.status-text {
|
.status-text {
|
||||||
|
|
|
@ -2467,6 +2467,11 @@ dayjs@^1.10.4:
|
||||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.10.7.tgz#2cf5f91add28116748440866a0a1d26f3a6ce468"
|
||||||
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
|
integrity sha512-P6twpd70BcPK34K26uJ1KT3wlhpuOAPoMwJzpsIWUxHZ7wpmbdZL/hQqBDfz7hGurYSa5PhzdhDHtt319hL3ig==
|
||||||
|
|
||||||
|
dayjs@^1.11.2:
|
||||||
|
version "1.11.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5"
|
||||||
|
integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==
|
||||||
|
|
||||||
debug@4, debug@4.3.2, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
|
debug@4, debug@4.3.2, debug@^4.1.0, debug@^4.1.1, debug@^4.3.2:
|
||||||
version "4.3.2"
|
version "4.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.2.tgz#f0a49c18ac8779e31d4a0c6029dfb76873c7428b"
|
||||||
|
@ -2568,9 +2573,9 @@ diff@^4.0.1:
|
||||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||||
|
|
||||||
diff@^5.0.0:
|
diff@^5.0.0:
|
||||||
version "5.0.0"
|
version "5.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
|
resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40"
|
||||||
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
|
integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==
|
||||||
|
|
||||||
dir-glob@^3.0.1:
|
dir-glob@^3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
|
@ -3246,9 +3251,9 @@ glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4:
|
||||||
path-is-absolute "^1.0.0"
|
path-is-absolute "^1.0.0"
|
||||||
|
|
||||||
glob@^7.1.6:
|
glob@^7.1.6:
|
||||||
version "7.2.2"
|
version "7.2.3"
|
||||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.2.tgz#29deb38e1ef90f132d5958abe9c3ee8e87f3c318"
|
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||||
integrity sha512-NzDgHDiJwKYByLrL5lONmQFpK/2G78SMMfo+E9CuGlX4IkvfKDsiQSNPwAYxEy+e6p7ZQ3uslSLlwlJcqezBmQ==
|
integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==
|
||||||
dependencies:
|
dependencies:
|
||||||
fs.realpath "^1.0.0"
|
fs.realpath "^1.0.0"
|
||||||
inflight "^1.0.4"
|
inflight "^1.0.4"
|
||||||
|
@ -6282,9 +6287,9 @@ yargs@^15.3.1, yargs@^15.4.1:
|
||||||
yargs-parser "^18.1.2"
|
yargs-parser "^18.1.2"
|
||||||
|
|
||||||
yargs@^17.2.1:
|
yargs@^17.2.1:
|
||||||
version "17.5.0"
|
version "17.5.1"
|
||||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.0.tgz#2706c5431f8c119002a2b106fc9f58b9bb9097a3"
|
resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.5.1.tgz#e109900cab6fcb7fd44b1d8249166feb0b36e58e"
|
||||||
integrity sha512-3sLxVhbAB5OC8qvVRebCLWuouhwh/rswsiDYx3WGxajUk/l4G20SKfrKKFeNIHboUFt2JFgv2yfn+5cgOr/t5A==
|
integrity sha512-t6YAJcxDkNX7NFYiVtKvWUz8l+PaKTLiL63mJYWR2GnHq2gjEWISzsLp9wg3aY36dY1j+gfIEL3pIF+XlJJfbA==
|
||||||
dependencies:
|
dependencies:
|
||||||
cliui "^7.0.2"
|
cliui "^7.0.2"
|
||||||
escalade "^3.1.1"
|
escalade "^3.1.1"
|
||||||
|
|
|
@ -73,4 +73,39 @@ export const buildAutomationEndpoints = API => ({
|
||||||
url: `/api/automations/${automationId}/${automationRev}`,
|
url: `/api/automations/${automationId}/${automationRev}`,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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({
|
||||||
|
url: "/api/automations/logs/search",
|
||||||
|
body: {
|
||||||
|
automationId,
|
||||||
|
startDate,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -45,6 +45,7 @@ const { getTenantId, isMultiTenant } = require("@budibase/backend-core/tenancy")
|
||||||
import { syncGlobalUsers } from "./user"
|
import { syncGlobalUsers } from "./user"
|
||||||
const { app: appCache } = require("@budibase/backend-core/cache")
|
const { app: appCache } = require("@budibase/backend-core/cache")
|
||||||
import { cleanupAutomations } from "../../automations/utils"
|
import { cleanupAutomations } from "../../automations/utils"
|
||||||
|
import { checkAppMetadata } from "../../automations/logging"
|
||||||
const {
|
const {
|
||||||
getAppDB,
|
getAppDB,
|
||||||
getProdAppDB,
|
getProdAppDB,
|
||||||
|
@ -193,7 +194,7 @@ export const fetch = async (ctx: any) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.body = apps
|
ctx.body = await checkAppMetadata(apps)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const fetchAppDefinition = async (ctx: any) => {
|
export const fetchAppDefinition = async (ctx: any) => {
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
const actions = require("../../automations/actions")
|
const actions = require("../../automations/actions")
|
||||||
const triggers = require("../../automations/triggers")
|
const triggers = require("../../automations/triggers")
|
||||||
const { getAutomationParams, generateAutomationID } = require("../../db/utils")
|
const {
|
||||||
|
getAutomationParams,
|
||||||
|
generateAutomationID,
|
||||||
|
DocumentTypes,
|
||||||
|
} = require("../../db/utils")
|
||||||
const {
|
const {
|
||||||
checkForWebhooks,
|
checkForWebhooks,
|
||||||
updateTestHistory,
|
updateTestHistory,
|
||||||
|
@ -9,8 +13,14 @@ 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 { automations } = require("@budibase/pro")
|
||||||
|
|
||||||
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)
|
||||||
|
@ -183,6 +193,29 @@ exports.destroy = async function (ctx) {
|
||||||
await events.automation.deleted(oldAutomation)
|
await events.automation.deleted(oldAutomation)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.logSearch = async function (ctx) {
|
||||||
|
ctx.body = await automations.logs.logSearch(ctx.request.body)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,10 @@ router.use(async (ctx, next) => {
|
||||||
error,
|
error,
|
||||||
}
|
}
|
||||||
ctx.log.error(err)
|
ctx.log.error(err)
|
||||||
console.trace(err)
|
// unauthorised errors don't provide a useful trace
|
||||||
|
if (!env.isTest()) {
|
||||||
|
console.trace(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,16 @@ 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"),
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -50,43 +50,51 @@ exports.definition = {
|
||||||
outputs: {
|
outputs: {
|
||||||
properties: {
|
properties: {
|
||||||
success: {
|
success: {
|
||||||
|
type: "boolean",
|
||||||
|
description: "Whether the action was successful",
|
||||||
|
},
|
||||||
|
result: {
|
||||||
type: "boolean",
|
type: "boolean",
|
||||||
description: "Whether the logic block passed",
|
description: "Whether the logic block passed",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
required: ["success"],
|
required: ["success", "result"],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.run = async function filter({ inputs }) {
|
exports.run = async function filter({ inputs }) {
|
||||||
let { field, condition, value } = inputs
|
try {
|
||||||
// coerce types so that we can use them
|
let { field, condition, value } = inputs
|
||||||
if (!isNaN(value) && !isNaN(field)) {
|
// coerce types so that we can use them
|
||||||
value = parseFloat(value)
|
if (!isNaN(value) && !isNaN(field)) {
|
||||||
field = parseFloat(field)
|
value = parseFloat(value)
|
||||||
} else if (!isNaN(Date.parse(value)) && !isNaN(Date.parse(field))) {
|
field = parseFloat(field)
|
||||||
value = Date.parse(value)
|
} else if (!isNaN(Date.parse(value)) && !isNaN(Date.parse(field))) {
|
||||||
field = Date.parse(field)
|
value = Date.parse(value)
|
||||||
}
|
field = Date.parse(field)
|
||||||
let success = false
|
|
||||||
if (typeof field !== "object" && typeof value !== "object") {
|
|
||||||
switch (condition) {
|
|
||||||
case FilterConditions.EQUAL:
|
|
||||||
success = field === value
|
|
||||||
break
|
|
||||||
case FilterConditions.NOT_EQUAL:
|
|
||||||
success = field !== value
|
|
||||||
break
|
|
||||||
case FilterConditions.GREATER_THAN:
|
|
||||||
success = field > value
|
|
||||||
break
|
|
||||||
case FilterConditions.LESS_THAN:
|
|
||||||
success = field < value
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
} else {
|
let result = false
|
||||||
success = false
|
if (typeof field !== "object" && typeof value !== "object") {
|
||||||
|
switch (condition) {
|
||||||
|
case FilterConditions.EQUAL:
|
||||||
|
result = field === value
|
||||||
|
break
|
||||||
|
case FilterConditions.NOT_EQUAL:
|
||||||
|
result = field !== value
|
||||||
|
break
|
||||||
|
case FilterConditions.GREATER_THAN:
|
||||||
|
result = field > value
|
||||||
|
break
|
||||||
|
case FilterConditions.LESS_THAN:
|
||||||
|
result = field < value
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = false
|
||||||
|
}
|
||||||
|
return { success: true, result }
|
||||||
|
} catch (err) {
|
||||||
|
return { success: false, result: false }
|
||||||
}
|
}
|
||||||
return { success }
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,8 @@ describe("test the filter logic", () => {
|
||||||
let res = await setup.runStep(setup.actions.FILTER.stepId,
|
let res = await setup.runStep(setup.actions.FILTER.stepId,
|
||||||
{ field, condition, value }
|
{ field, condition, value }
|
||||||
)
|
)
|
||||||
expect(res.success).toEqual(pass)
|
expect(res.result).toEqual(pass)
|
||||||
|
expect(res.success).toEqual(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
it("should be able test equality", async () => {
|
it("should be able test equality", async () => {
|
||||||
|
|
|
@ -65,6 +65,7 @@ async function getLinksForRows(rows) {
|
||||||
// return duplicates, could be querying for both tables in a relation
|
// return duplicates, could be querying for both tables in a relation
|
||||||
return getUniqueByProp(
|
return getUniqueByProp(
|
||||||
responses
|
responses
|
||||||
|
.filter(el => el != null)
|
||||||
// create a unique ID which we can use for getting only unique ones
|
// create a unique ID which we can use for getting only unique ones
|
||||||
.map(el => ({ ...el, unique: el.id + el.thisId + el.fieldName })),
|
.map(el => ({ ...el, unique: el.id + el.thisId + el.fieldName })),
|
||||||
"unique"
|
"unique"
|
||||||
|
|
|
@ -11,6 +11,8 @@ const {
|
||||||
isProdAppID,
|
isProdAppID,
|
||||||
getDevelopmentAppID,
|
getDevelopmentAppID,
|
||||||
generateAppID,
|
generateAppID,
|
||||||
|
getQueryIndex,
|
||||||
|
ViewNames,
|
||||||
} = require("@budibase/backend-core/db")
|
} = require("@budibase/backend-core/db")
|
||||||
|
|
||||||
const UNICODE_MAX = "\ufff0"
|
const UNICODE_MAX = "\ufff0"
|
||||||
|
@ -22,11 +24,7 @@ const AppStatus = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const DocumentTypes = {
|
const DocumentTypes = {
|
||||||
APP: CoreDocTypes.APP,
|
...CoreDocTypes,
|
||||||
DEV: CoreDocTypes.DEV,
|
|
||||||
APP_DEV: CoreDocTypes.APP_DEV,
|
|
||||||
APP_METADATA: CoreDocTypes.APP_METADATA,
|
|
||||||
ROLE: CoreDocTypes.ROLE,
|
|
||||||
TABLE: "ta",
|
TABLE: "ta",
|
||||||
ROW: "ro",
|
ROW: "ro",
|
||||||
USER: "us",
|
USER: "us",
|
||||||
|
@ -45,11 +43,6 @@ const DocumentTypes = {
|
||||||
USER_FLAG: "flag",
|
USER_FLAG: "flag",
|
||||||
}
|
}
|
||||||
|
|
||||||
const ViewNames = {
|
|
||||||
LINK: "by_link",
|
|
||||||
ROUTING: "screen_routes",
|
|
||||||
}
|
|
||||||
|
|
||||||
const InternalTables = {
|
const InternalTables = {
|
||||||
USER_METADATA: "ta_users",
|
USER_METADATA: "ta_users",
|
||||||
}
|
}
|
||||||
|
@ -89,9 +82,7 @@ exports.generateDevAppID = getDevelopmentAppID
|
||||||
exports.generateRoleID = generateRoleID
|
exports.generateRoleID = generateRoleID
|
||||||
exports.getRoleParams = getRoleParams
|
exports.getRoleParams = getRoleParams
|
||||||
|
|
||||||
exports.getQueryIndex = viewName => {
|
exports.getQueryIndex = getQueryIndex
|
||||||
return `database/${viewName}`
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
* If creating DB allDocs/query params with only a single top level ID this can be used, this
|
||||||
|
|
|
@ -78,6 +78,7 @@ module.exports = {
|
||||||
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
ALLOW_DEV_AUTOMATIONS: process.env.ALLOW_DEV_AUTOMATIONS,
|
||||||
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
DISABLE_THREADING: process.env.DISABLE_THREADING,
|
||||||
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
|
DISABLE_DEVELOPER_LICENSE: process.env.DISABLE_DEVELOPER_LICENSE,
|
||||||
|
DISABLE_AUTOMATION_LOGS: process.env.DISABLE_AUTOMATION_LOGS,
|
||||||
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
MULTI_TENANCY: process.env.MULTI_TENANCY,
|
||||||
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
ENABLE_ANALYTICS: process.env.ENABLE_ANALYTICS,
|
||||||
SELF_HOSTED: process.env.SELF_HOSTED,
|
SELF_HOSTED: process.env.SELF_HOSTED,
|
||||||
|
|
|
@ -8,3 +8,4 @@ declare module "@budibase/backend-core/constants"
|
||||||
declare module "@budibase/backend-core/auth"
|
declare module "@budibase/backend-core/auth"
|
||||||
declare module "@budibase/backend-core/sessions"
|
declare module "@budibase/backend-core/sessions"
|
||||||
declare module "@budibase/backend-core/encryption"
|
declare module "@budibase/backend-core/encryption"
|
||||||
|
declare module "@budibase/backend-core/redis"
|
||||||
|
|
|
@ -9,11 +9,12 @@ const { doInTenant } = require("@budibase/backend-core/tenancy")
|
||||||
const { definitions: triggerDefs } = require("../automations/triggerInfo")
|
const { definitions: triggerDefs } = require("../automations/triggerInfo")
|
||||||
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
const { doInAppContext, getAppDB } = require("@budibase/backend-core/context")
|
||||||
const { AutomationErrors, LoopStepTypes } = require("../constants")
|
const { AutomationErrors, LoopStepTypes } = require("../constants")
|
||||||
|
const { storeLog } = require("../automations/logging")
|
||||||
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
|
const FILTER_STEP_ID = actions.ACTION_DEFINITIONS.FILTER.stepId
|
||||||
const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
|
const LOOP_STEP_ID = actions.ACTION_DEFINITIONS.LOOP.stepId
|
||||||
|
|
||||||
const CRON_STEP_ID = triggerDefs.CRON.stepId
|
const CRON_STEP_ID = triggerDefs.CRON.stepId
|
||||||
const STOPPED_STATUS = { success: false, status: "STOPPED" }
|
const STOPPED_STATUS = { success: true, status: "STOPPED" }
|
||||||
const { cloneDeep } = require("lodash/fp")
|
const { cloneDeep } = require("lodash/fp")
|
||||||
const env = require("../environment")
|
const env = require("../environment")
|
||||||
|
|
||||||
|
@ -275,7 +276,7 @@ class Orchestrator {
|
||||||
this._context.steps[stepCount] = outputs
|
this._context.steps[stepCount] = outputs
|
||||||
// if filter causes us to stop execution don't break the loop, set a var
|
// if filter causes us to stop execution don't break the loop, set a var
|
||||||
// so that we can finish iterating through the steps and record that it stopped
|
// so that we can finish iterating through the steps and record that it stopped
|
||||||
if (step.stepId === FILTER_STEP_ID && !outputs.success) {
|
if (step.stepId === FILTER_STEP_ID && !outputs.result) {
|
||||||
stopped = true
|
stopped = true
|
||||||
this.updateExecutionOutput(step.id, step.stepId, step.inputs, {
|
this.updateExecutionOutput(step.id, step.stepId, step.inputs, {
|
||||||
...outputs,
|
...outputs,
|
||||||
|
@ -325,6 +326,8 @@ class Orchestrator {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// store the logs for the automation run
|
||||||
|
await storeLog(this._automation, this.executionOutput)
|
||||||
return this.executionOutput
|
return this.executionOutput
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -77,7 +77,7 @@ export class Thread {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
static shutdown() {
|
static stopThreads() {
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
if (Thread.workerRefs.length === 0) {
|
if (Thread.workerRefs.length === 0) {
|
||||||
resolve()
|
resolve()
|
||||||
|
@ -95,4 +95,8 @@ export class Thread {
|
||||||
Thread.workerRefs = []
|
Thread.workerRefs = []
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async shutdown() {
|
||||||
|
await Thread.stopThreads()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,5 +1,7 @@
|
||||||
import { Document } from "../document"
|
import { Document } from "../document"
|
||||||
|
|
||||||
|
export type AppMetadataErrors = { [key: string]: string[] }
|
||||||
|
|
||||||
export interface App extends Document {
|
export interface App extends Document {
|
||||||
appId: string
|
appId: string
|
||||||
type: string
|
type: string
|
||||||
|
@ -12,6 +14,7 @@ export interface App extends Document {
|
||||||
tenantId: string
|
tenantId: string
|
||||||
status: string
|
status: string
|
||||||
revertableVersion?: string
|
revertableVersion?: string
|
||||||
|
automationErrors?: AppMetadataErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppInstance {
|
export interface AppInstance {
|
||||||
|
|
|
@ -6,6 +6,7 @@ export interface Automation extends Document {
|
||||||
trigger: AutomationTrigger
|
trigger: AutomationTrigger
|
||||||
}
|
}
|
||||||
appId: string
|
appId: string
|
||||||
|
name: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutomationStep {
|
export interface AutomationStep {
|
||||||
|
@ -17,3 +18,35 @@ export interface AutomationTrigger {
|
||||||
id: string
|
id: string
|
||||||
stepId: string
|
stepId: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum AutomationStatus {
|
||||||
|
SUCCESS = "success",
|
||||||
|
ERROR = "error",
|
||||||
|
STOPPED = "stopped",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationResults {
|
||||||
|
automationId: string
|
||||||
|
status: string
|
||||||
|
trigger?: any
|
||||||
|
steps: {
|
||||||
|
stepId: string
|
||||||
|
inputs: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
outputs: {
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationLog extends AutomationResults, Document {
|
||||||
|
automationName: string
|
||||||
|
_rev?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationLogPage {
|
||||||
|
data: AutomationLog[]
|
||||||
|
hasNextPage: boolean
|
||||||
|
nextPage?: string
|
||||||
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -29,6 +29,9 @@ if [ -d "../budibase-pro" ]; then
|
||||||
echo "Linking backend-core to pro"
|
echo "Linking backend-core to pro"
|
||||||
yarn link '@budibase/backend-core'
|
yarn link '@budibase/backend-core'
|
||||||
|
|
||||||
|
echo "Linking types to pro"
|
||||||
|
yarn link '@budibase/types'
|
||||||
|
|
||||||
cd ../../../budibase
|
cd ../../../budibase
|
||||||
|
|
||||||
echo "Linking pro to worker"
|
echo "Linking pro to worker"
|
||||||
|
|
Loading…
Reference in New Issue