Adding in relocated screen components
This commit is contained in:
parent
f37c49e8af
commit
e1b63842e5
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import { automationStore } from "builderStore"
|
||||||
|
import { params } from "@roxi/routify"
|
||||||
|
|
||||||
|
if ($params.automation) {
|
||||||
|
const automation = $automationStore.automations.find(
|
||||||
|
m => m._id === $params.automation
|
||||||
|
)
|
||||||
|
if (automation) {
|
||||||
|
automationStore.actions.select(automation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<slot />
|
|
@ -0,0 +1,5 @@
|
||||||
|
<script>
|
||||||
|
import AutomationBuilder from "components/automation/AutomationBuilder/AutomationBuilder.svelte"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<AutomationBuilder />
|
|
@ -0,0 +1,131 @@
|
||||||
|
<script>
|
||||||
|
import { Heading, Body, Layout, Button, Modal } from "@budibase/bbui"
|
||||||
|
import { automationStore, selectedAutomation } from "builderStore"
|
||||||
|
import AutomationPanel from "components/automation/AutomationPanel/AutomationPanel.svelte"
|
||||||
|
import CreateAutomationModal from "components/automation/AutomationPanel/CreateAutomationModal.svelte"
|
||||||
|
import CreateWebhookModal from "components/automation/Shared/CreateWebhookModal.svelte"
|
||||||
|
import TestPanel from "components/automation/AutomationBuilder/TestPanel.svelte"
|
||||||
|
import { onDestroy, onMount } from "svelte"
|
||||||
|
import { syncURLToState } from "helpers/urlStateSync"
|
||||||
|
import * as routify from "@roxi/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
|
||||||
|
// Prevent access for other users than the lock holder
|
||||||
|
$: {
|
||||||
|
if (!$store.hasLock) {
|
||||||
|
$redirect("../data")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep URL and state in sync for selected screen ID
|
||||||
|
const stopSyncing = syncURLToState({
|
||||||
|
urlParam: "automationId",
|
||||||
|
stateKey: "selectedAutomationId",
|
||||||
|
validate: id => $automationStore.automations.some(x => x._id === id),
|
||||||
|
fallbackUrl: "./index",
|
||||||
|
store: automationStore,
|
||||||
|
up: automationStore.actions.select,
|
||||||
|
routify,
|
||||||
|
})
|
||||||
|
|
||||||
|
let modal
|
||||||
|
let webhookModal
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
$automationStore.showTestPanel = false
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(stopSyncing)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- routify:options index=3 -->
|
||||||
|
<div class="root">
|
||||||
|
<AutomationPanel {modal} {webhookModal} />
|
||||||
|
<div class="content">
|
||||||
|
{#if $automationStore.automations?.length}
|
||||||
|
<slot />
|
||||||
|
{:else}
|
||||||
|
<div class="centered">
|
||||||
|
<div class="main">
|
||||||
|
<Layout gap="S" justifyItems="center">
|
||||||
|
<svg
|
||||||
|
width="60px"
|
||||||
|
height="60px"
|
||||||
|
class="spectrum-Icon"
|
||||||
|
focusable="false"
|
||||||
|
>
|
||||||
|
<use xlink:href="#spectrum-icon-18-WorkflowAdd" />
|
||||||
|
</svg>
|
||||||
|
<Heading size="M">You have no automations</Heading>
|
||||||
|
<Body size="M">Let's fix that. Call the bots!</Body>
|
||||||
|
<Button on:click={() => modal.show()} size="M" cta>
|
||||||
|
Create automation
|
||||||
|
</Button>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if $automationStore.showTestPanel}
|
||||||
|
<div class="setup">
|
||||||
|
<TestPanel automation={$selectedAutomation} />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
<Modal bind:this={modal}>
|
||||||
|
<CreateAutomationModal {webhookModal} />
|
||||||
|
</Modal>
|
||||||
|
<Modal bind:this={webhookModal} width="30%">
|
||||||
|
<CreateWebhookModal />
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.root {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-auto-flow: column dense;
|
||||||
|
grid-template-columns: 260px minmax(510px, 1fr) fit-content(500px);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.centered {
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setup {
|
||||||
|
padding-top: var(--spectrum-global-dimension-size-200);
|
||||||
|
border-left: var(--border-light);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
background-color: var(--background);
|
||||||
|
grid-column: 3;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script>
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
import { automationStore } from "builderStore"
|
||||||
|
|
||||||
|
$: {
|
||||||
|
if ($automationStore.automations?.length) {
|
||||||
|
$redirect(`./${$automationStore.automations[0]._id}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -0,0 +1,74 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Content,
|
||||||
|
SideNav,
|
||||||
|
SideNavItem,
|
||||||
|
Breadcrumbs,
|
||||||
|
Breadcrumb,
|
||||||
|
Header,
|
||||||
|
} from "components/portal/page"
|
||||||
|
import {
|
||||||
|
Page,
|
||||||
|
Layout,
|
||||||
|
Button,
|
||||||
|
Icon,
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Helpers,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||||
|
import { url, isActive, goto } from "@roxi/routify"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- routify:options index=4 -->
|
||||||
|
<div class="settings">
|
||||||
|
<Page>
|
||||||
|
<Layout noPadding gap="L">
|
||||||
|
<Content showMobileNav>
|
||||||
|
<SideNav slot="side-nav">
|
||||||
|
<SideNavItem
|
||||||
|
text="Automation History"
|
||||||
|
url={$url("./automation-history")}
|
||||||
|
active={$isActive("./automation-history")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Backups"
|
||||||
|
url={$url("./backups")}
|
||||||
|
active={$isActive("./backups")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Embed"
|
||||||
|
url={$url("./embed")}
|
||||||
|
active={$isActive("./embed")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Name and URL"
|
||||||
|
url={$url("./name-and-url")}
|
||||||
|
active={$isActive("./name-and-url")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Version"
|
||||||
|
url={$url("./version")}
|
||||||
|
active={$isActive("./version")}
|
||||||
|
/>
|
||||||
|
</SideNav>
|
||||||
|
<slot />
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.settings {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,82 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
InlineAlert,
|
||||||
|
Heading,
|
||||||
|
Icon,
|
||||||
|
} 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
|
||||||
|
const STOPPED_ERROR = "stopped_error"
|
||||||
|
|
||||||
|
$: exists = $automationStore.automations?.find(
|
||||||
|
auto => auto._id === history?.automationId
|
||||||
|
)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if history}
|
||||||
|
<Layout noPadding>
|
||||||
|
<div class="controls">
|
||||||
|
<StatusRenderer value={history.status} />
|
||||||
|
<Icon hoverable name="Close" on:click={close} />
|
||||||
|
</div>
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Heading>{history.automationName}</Heading>
|
||||||
|
<DateTimeRenderer value={history.createdAt} />
|
||||||
|
</Layout>
|
||||||
|
{#if history.status === STOPPED_ERROR}
|
||||||
|
<div class="cron-error">
|
||||||
|
<InlineAlert
|
||||||
|
type="error"
|
||||||
|
header="CRON automation disabled"
|
||||||
|
message="Fix the error and re-publish your app to re-activate."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#if exists}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
$goto(`/builder/app/${appId}/automation/${history.automationId}`)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Edit automation
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{#key history}
|
||||||
|
<div class="history">
|
||||||
|
<TestDisplay testResults={history} width="100%" />
|
||||||
|
</div>
|
||||||
|
{/key}
|
||||||
|
</Layout>
|
||||||
|
{:else}
|
||||||
|
<Body>No details found</Body>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.cron-error {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
.history {
|
||||||
|
margin: 0 -30px;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import { Badge } from "@budibase/bbui"
|
||||||
|
export let value
|
||||||
|
|
||||||
|
$: isError = !value || value.toLowerCase() === "error"
|
||||||
|
$: isStoppedError = value?.toLowerCase() === "stopped_error"
|
||||||
|
$: isStopped = value?.toLowerCase() === "stopped" || isStoppedError
|
||||||
|
$: info = getInfo(isError, isStopped)
|
||||||
|
|
||||||
|
const getInfo = (error, stopped) => {
|
||||||
|
if (error) {
|
||||||
|
return { color: "red", message: "Error" }
|
||||||
|
} else if (stopped) {
|
||||||
|
return { color: "yellow", message: "Stopped" }
|
||||||
|
} else {
|
||||||
|
return { color: "green", message: "Success" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
green={info.color === "green"}
|
||||||
|
red={info.color === "red"}
|
||||||
|
yellow={info.color === "yellow"}
|
||||||
|
>
|
||||||
|
{info.message}
|
||||||
|
</Badge>
|
|
@ -0,0 +1,262 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Table,
|
||||||
|
Select,
|
||||||
|
Pagination,
|
||||||
|
Button,
|
||||||
|
Body,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
||||||
|
import StatusRenderer from "./_components/StatusRenderer.svelte"
|
||||||
|
import HistoryDetailsPanel from "./_components/HistoryDetailsPanel.svelte"
|
||||||
|
import { automationStore, store } from "builderStore"
|
||||||
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
|
import { getContext, onDestroy, onMount } from "svelte"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import { auth, licensing, admin } from "stores/portal"
|
||||||
|
import { Constants } from "@budibase/frontend-core"
|
||||||
|
import Portal from "svelte-portal"
|
||||||
|
|
||||||
|
const ERROR = "error",
|
||||||
|
SUCCESS = "success",
|
||||||
|
STOPPED = "stopped"
|
||||||
|
const sidePanel = getContext("side-panel")
|
||||||
|
|
||||||
|
let pageInfo = createPaginationStore()
|
||||||
|
let runHistory = null
|
||||||
|
let selectedHistory = null
|
||||||
|
let automationOptions = []
|
||||||
|
let automationId = null
|
||||||
|
let status = null
|
||||||
|
let timeRange = null
|
||||||
|
let loaded = false
|
||||||
|
|
||||||
|
$: licensePlan = $auth.user?.license?.plan
|
||||||
|
$: page = $pageInfo.page
|
||||||
|
$: fetchLogs(automationId, status, page, timeRange)
|
||||||
|
|
||||||
|
const timeOptions = [
|
||||||
|
{ value: "90-d", label: "Past 90 days" },
|
||||||
|
{ value: "30-d", label: "Past 30 days" },
|
||||||
|
{ 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" },
|
||||||
|
automationName: { displayName: "Automation" },
|
||||||
|
createdAt: { displayName: "Time" },
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRenderers = [
|
||||||
|
{ column: "createdAt", component: DateTimeRenderer },
|
||||||
|
{ column: "status", component: StatusRenderer },
|
||||||
|
]
|
||||||
|
|
||||||
|
async function fetchLogs(
|
||||||
|
automationId,
|
||||||
|
status,
|
||||||
|
page,
|
||||||
|
timeRange,
|
||||||
|
force = false
|
||||||
|
) {
|
||||||
|
if (!force && !loaded) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
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
|
||||||
|
sidePanel.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await automationStore.actions.fetch()
|
||||||
|
const params = new URLSearchParams(window.location.search)
|
||||||
|
const shouldOpen = params.get("open") === ERROR
|
||||||
|
if (shouldOpen) {
|
||||||
|
status = ERROR
|
||||||
|
}
|
||||||
|
automationOptions = []
|
||||||
|
for (let automation of $automationStore.automations) {
|
||||||
|
automationOptions.push({ value: automation._id, label: automation.name })
|
||||||
|
}
|
||||||
|
await fetchLogs(automationId, status, 0, timeRange, true)
|
||||||
|
// Open the first automation info if one exists
|
||||||
|
if (shouldOpen && runHistory?.[0]) {
|
||||||
|
viewDetails({ detail: runHistory[0] })
|
||||||
|
}
|
||||||
|
loaded = true
|
||||||
|
})
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
sidePanel.close()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading>Automation History</Heading>
|
||||||
|
<Body>View the automations your app has executed</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="search">
|
||||||
|
<div class="select">
|
||||||
|
<Select
|
||||||
|
placeholder="All"
|
||||||
|
label="Status"
|
||||||
|
bind:value={status}
|
||||||
|
options={statusOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="select">
|
||||||
|
<Select
|
||||||
|
placeholder="All"
|
||||||
|
label="Automation"
|
||||||
|
bind:value={automationId}
|
||||||
|
options={automationOptions}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="select">
|
||||||
|
<Select
|
||||||
|
placeholder="All"
|
||||||
|
label="Date range"
|
||||||
|
bind:value={timeRange}
|
||||||
|
options={timeOptions}
|
||||||
|
isOptionEnabled={x => {
|
||||||
|
if (licensePlan?.type === Constants.PlanType.FREE) {
|
||||||
|
return ["1-w", "30-d", "90-d"].indexOf(x.value) < 0
|
||||||
|
} else if (licensePlan?.type === Constants.PlanType.TEAM) {
|
||||||
|
return ["90-d"].indexOf(x.value) < 0
|
||||||
|
} else if (licensePlan?.type === Constants.PlanType.PRO) {
|
||||||
|
return ["30-d", "90-d"].indexOf(x.value) < 0
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if (licensePlan?.type !== Constants.PlanType.ENTERPRISE && $auth.user.accountPortalAccess) || !$admin.cloud}
|
||||||
|
<Button secondary on:click={$licensing.goToUpgradePage()}>
|
||||||
|
Get more history
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if runHistory}
|
||||||
|
<div>
|
||||||
|
<Table
|
||||||
|
on:click={viewDetails}
|
||||||
|
schema={runHistorySchema}
|
||||||
|
allowSelectRows={false}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
data={runHistory}
|
||||||
|
{customRenderers}
|
||||||
|
placeholderText="No history found"
|
||||||
|
border={false}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
{#if selectedHistory}
|
||||||
|
<Portal target="#side-panel">
|
||||||
|
<HistoryDetailsPanel
|
||||||
|
appId={$store.appId}
|
||||||
|
bind:history={selectedHistory}
|
||||||
|
close={sidePanel.close}
|
||||||
|
/>
|
||||||
|
</Portal>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
align-items: flex-end;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
align-items: flex-start;
|
||||||
|
flex: 1 0 auto;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.select {
|
||||||
|
flex: 1 1 0;
|
||||||
|
max-width: 150px;
|
||||||
|
min-width: 80px;
|
||||||
|
}
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,87 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
ActionMenu,
|
||||||
|
MenuItem,
|
||||||
|
Icon,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import CreateRestoreModal from "./CreateRestoreModal.svelte"
|
||||||
|
import { createEventDispatcher } from "svelte"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
let deleteDialog
|
||||||
|
let restoreDialog
|
||||||
|
let restoreBackupModal
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher()
|
||||||
|
|
||||||
|
const onClickRestore = () => {
|
||||||
|
dispatch("buttonclick", {
|
||||||
|
type: "backupRestore",
|
||||||
|
backupId: row._id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onClickDelete = () => {
|
||||||
|
dispatch("buttonclick", {
|
||||||
|
type: "backupDelete",
|
||||||
|
backupId: row._id,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function downloadExport() {
|
||||||
|
window.open(`/api/apps/${row.appId}/backups/${row._id}/file`, "_blank")
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
<ActionMenu align="right">
|
||||||
|
<div slot="control">
|
||||||
|
<Icon size="M" hoverable name="MoreSmallList" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if row.type !== "restore"}
|
||||||
|
<MenuItem on:click={restoreDialog.show} icon="Revert">Restore</MenuItem>
|
||||||
|
<MenuItem on:click={deleteDialog.show} icon="Delete">Delete</MenuItem>
|
||||||
|
<MenuItem on:click={downloadExport} icon="Download">Download</MenuItem>
|
||||||
|
{/if}
|
||||||
|
</ActionMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Modal bind:this={restoreBackupModal}>
|
||||||
|
<CreateRestoreModal confirm={onClickRestore} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deleteDialog}
|
||||||
|
okText="Delete Backup"
|
||||||
|
onOk={onClickDelete}
|
||||||
|
title="Confirm Deletion"
|
||||||
|
>
|
||||||
|
Are you sure you wish to delete this backup? This action cannot be undone.
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={restoreDialog}
|
||||||
|
okText="Continue"
|
||||||
|
onOk={restoreBackupModal?.show}
|
||||||
|
title="Confirm restore"
|
||||||
|
warning={false}
|
||||||
|
>
|
||||||
|
<Heading size="S">Backup</Heading>
|
||||||
|
<Body size="S">{new Date(row.timestamp).toLocaleString()}</Body>
|
||||||
|
</ConfirmDialog>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,41 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let row
|
||||||
|
|
||||||
|
$: automations = row?.automations
|
||||||
|
$: datasources = row?.datasources
|
||||||
|
$: screens = row?.screens
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
{#if automations != null && screens != null && datasources != null}
|
||||||
|
<div class="item">
|
||||||
|
<Icon name="Data" />
|
||||||
|
<div>{datasources || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<Icon name="WebPage" />
|
||||||
|
<div>{screens || 0}</div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<Icon name="JourneyVoyager" />
|
||||||
|
<div>{automations || 0}</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: calc(var(--spacing-xl) * 2);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,27 @@
|
||||||
|
<script>
|
||||||
|
import { ModalContent, Input, Body } from "@budibase/bbui"
|
||||||
|
import { auth } from "stores/portal"
|
||||||
|
|
||||||
|
export let confirm
|
||||||
|
|
||||||
|
let templateName = $auth.user.firstName
|
||||||
|
? `${$auth.user.firstName}'s Backup`
|
||||||
|
: "Restore Backup"
|
||||||
|
let name = templateName
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ModalContent
|
||||||
|
onConfirm={() => confirm(name)}
|
||||||
|
title="Back up your current version"
|
||||||
|
confirmText="Confirm Restore"
|
||||||
|
disabled={!name}
|
||||||
|
>
|
||||||
|
<Body size="S"
|
||||||
|
>Create a backup of your current app to allow you to roll back after
|
||||||
|
restoring this backup</Body
|
||||||
|
>
|
||||||
|
<Input label="Backup name" bind:value={name} />
|
||||||
|
</ModalContent>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -0,0 +1,21 @@
|
||||||
|
<script>
|
||||||
|
import { Icon } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
{#if value != null}
|
||||||
|
<Icon name="Data" />
|
||||||
|
<div>{value || 0}</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,15 @@
|
||||||
|
<script>
|
||||||
|
import { Badge } from "@budibase/bbui"
|
||||||
|
|
||||||
|
export let value = "started"
|
||||||
|
$: status = value[0].toUpperCase() + value?.slice(1)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Badge
|
||||||
|
grey={value === "started" || value === "pending"}
|
||||||
|
green={value === "complete"}
|
||||||
|
red={value === "failed"}
|
||||||
|
size="S"
|
||||||
|
>
|
||||||
|
{status}
|
||||||
|
</Badge>
|
|
@ -0,0 +1,10 @@
|
||||||
|
<script>
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
import relativeTime from "dayjs/plugin/relativeTime"
|
||||||
|
|
||||||
|
dayjs.extend(relativeTime)
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<span title={value}>{dayjs(value).fromNow()}</span>
|
|
@ -0,0 +1,36 @@
|
||||||
|
<script>
|
||||||
|
import { BackupTrigger } from "constants/backend/backups"
|
||||||
|
export let row
|
||||||
|
|
||||||
|
$: trigger = row?.trigger || "manual"
|
||||||
|
$: type = row?.type || "backup"
|
||||||
|
|
||||||
|
function printTrigger(trig) {
|
||||||
|
let final = "undefined"
|
||||||
|
switch (trig) {
|
||||||
|
case BackupTrigger.PUBLISH:
|
||||||
|
final = "published"
|
||||||
|
break
|
||||||
|
case BackupTrigger.RESTORING:
|
||||||
|
final = "pre-restore"
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
final = trig
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return final.charAt(0).toUpperCase() + final.slice(1)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
{printTrigger(trigger)}
|
||||||
|
{type}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,18 @@
|
||||||
|
<script>
|
||||||
|
import { UserAvatar } from "@budibase/frontend-core"
|
||||||
|
|
||||||
|
export let value
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="cell">
|
||||||
|
<UserAvatar user={value} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,340 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
DatePicker,
|
||||||
|
Divider,
|
||||||
|
Layout,
|
||||||
|
notifications,
|
||||||
|
Pagination,
|
||||||
|
Select,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Tags,
|
||||||
|
Tag,
|
||||||
|
Table,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { backups, licensing, auth, admin } from "stores/portal"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
|
import TimeAgoRenderer from "./_components/TimeAgoRenderer.svelte"
|
||||||
|
import AppSizeRenderer from "./_components/AppSizeRenderer.svelte"
|
||||||
|
import ActionsRenderer from "./_components/ActionsRenderer.svelte"
|
||||||
|
import UserRenderer from "./_components/UserRenderer.svelte"
|
||||||
|
import StatusRenderer from "./_components/StatusRenderer.svelte"
|
||||||
|
import TypeRenderer from "./_components/TypeRenderer.svelte"
|
||||||
|
import BackupsDefault from "assets/backups-default.png"
|
||||||
|
import { BackupTrigger, BackupType } from "constants/backend/backups"
|
||||||
|
import { onMount } from "svelte"
|
||||||
|
|
||||||
|
let loading = true
|
||||||
|
let backupData = null
|
||||||
|
let pageInfo = createPaginationStore()
|
||||||
|
let filterOpt = null
|
||||||
|
let startDate = null
|
||||||
|
let endDate = null
|
||||||
|
let filters = [
|
||||||
|
{
|
||||||
|
label: "Manual backup",
|
||||||
|
value: { type: BackupType.BACKUP, trigger: BackupTrigger.MANUAL },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Published backup",
|
||||||
|
value: { type: BackupType.BACKUP, trigger: BackupTrigger.PUBLISH },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Pre-restore backup",
|
||||||
|
value: { type: BackupType.BACKUP, trigger: BackupTrigger.RESTORING },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Manual restore",
|
||||||
|
value: { type: BackupType.RESTORE, trigger: BackupTrigger.MANUAL },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
$: page = $pageInfo.page
|
||||||
|
$: fetchBackups(filterOpt, page, startDate, endDate)
|
||||||
|
|
||||||
|
let schema = {
|
||||||
|
type: {
|
||||||
|
displayName: "Type",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
createdAt: {
|
||||||
|
displayName: "Date",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
appSize: {
|
||||||
|
displayName: "App size",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
createdBy: {
|
||||||
|
displayName: "User",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
displayName: "Status",
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
actions: {
|
||||||
|
displayName: null,
|
||||||
|
width: "auto",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const customRenderers = [
|
||||||
|
{ column: "appSize", component: AppSizeRenderer },
|
||||||
|
{ column: "actions", component: ActionsRenderer },
|
||||||
|
{ column: "createdAt", component: TimeAgoRenderer },
|
||||||
|
{ column: "createdBy", component: UserRenderer },
|
||||||
|
{ column: "status", component: StatusRenderer },
|
||||||
|
{ column: "type", component: TypeRenderer },
|
||||||
|
]
|
||||||
|
|
||||||
|
function flattenBackups(backups) {
|
||||||
|
return backups.map(backup => {
|
||||||
|
return {
|
||||||
|
...backup,
|
||||||
|
...backup?.contents,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchBackups(filters, page, startDate, endDate) {
|
||||||
|
const response = await backups.searchBackups({
|
||||||
|
appId: $store.appId,
|
||||||
|
...filters,
|
||||||
|
page,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
})
|
||||||
|
pageInfo.fetched(response.hasNextPage, response.nextPage)
|
||||||
|
|
||||||
|
// flatten so we have an easier structure to use for the table schema
|
||||||
|
backupData = flattenBackups(response.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createManualBackup() {
|
||||||
|
try {
|
||||||
|
loading = true
|
||||||
|
let response = await backups.createManualBackup({
|
||||||
|
appId: $store.appId,
|
||||||
|
})
|
||||||
|
await fetchBackups(filterOpt, page)
|
||||||
|
notifications.success(response.message)
|
||||||
|
} catch {
|
||||||
|
notifications.error("Unable to create backup")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const poll = backupData => {
|
||||||
|
if (backupData === null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupData.some(datum => datum.status === "started")) {
|
||||||
|
setTimeout(() => fetchBackups(filterOpt, page), 2000)
|
||||||
|
} else {
|
||||||
|
loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$: poll(backupData)
|
||||||
|
|
||||||
|
async function handleButtonClick({ detail }) {
|
||||||
|
if (detail.type === "backupDelete") {
|
||||||
|
await backups.deleteBackup({
|
||||||
|
appId: $store.appId,
|
||||||
|
backupId: detail.backupId,
|
||||||
|
})
|
||||||
|
await fetchBackups(filterOpt, page)
|
||||||
|
} else if (detail.type === "backupRestore") {
|
||||||
|
await backups.restoreBackup({
|
||||||
|
appId: $store.appId,
|
||||||
|
backupId: detail.backupId,
|
||||||
|
name: detail.restoreBackupName,
|
||||||
|
})
|
||||||
|
await fetchBackups(filterOpt, page)
|
||||||
|
} else if (detail.type === "backupUpdate") {
|
||||||
|
await backups.updateBackup({
|
||||||
|
appId: $store.appId,
|
||||||
|
backupId: detail.backupId,
|
||||||
|
name: detail.name,
|
||||||
|
})
|
||||||
|
await fetchBackups(filterOpt, page)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
await fetchBackups(filterOpt, page, startDate, endDate)
|
||||||
|
loading = false
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<div class="title">
|
||||||
|
<Heading>Backups</Heading>
|
||||||
|
{#if !$licensing.backupsEnabled}
|
||||||
|
<Tags>
|
||||||
|
<Tag icon="LockClosed">Premium</Tag>
|
||||||
|
</Tags>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<Body>Back up your apps and restore them to their previous state</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{#if !$licensing.backupsEnabled}
|
||||||
|
{#if !$auth.accountPortalAccess && $admin.cloud}
|
||||||
|
<Body>Contact your account holder to upgrade your plan.</Body>
|
||||||
|
{/if}
|
||||||
|
<div class="pro-buttons">
|
||||||
|
{#if $auth.accountPortalAccess}
|
||||||
|
<Button
|
||||||
|
primary
|
||||||
|
disabled={!$auth.accountPortalAccess && $admin.cloud}
|
||||||
|
on:click={$licensing.goToUpgradePage()}
|
||||||
|
>
|
||||||
|
Upgrade
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
on:click={() => {
|
||||||
|
window.open("https://budibase.com/pricing/", "_blank")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
View plans
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else if !backupData?.length && !loading && !filterOpt && !startDate}
|
||||||
|
<div class="center">
|
||||||
|
<Layout noPadding gap="S" justifyItems="center">
|
||||||
|
<img height="130px" src={BackupsDefault} alt="BackupsDefault" />
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Heading>You have no backups yet</Heading>
|
||||||
|
<Body>You can manually back up your app any time</Body>
|
||||||
|
</Layout>
|
||||||
|
<div>
|
||||||
|
<Button cta disabled={loading} on:click={createManualBackup}>
|
||||||
|
Create backup
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Layout noPadding gap="M" alignContent="start">
|
||||||
|
<div class="controls">
|
||||||
|
<div class="search">
|
||||||
|
<div class="select">
|
||||||
|
<Select
|
||||||
|
placeholder="All"
|
||||||
|
label="Type"
|
||||||
|
options={filters}
|
||||||
|
getOptionValue={filter => filter.value}
|
||||||
|
getOptionLabel={filter => filter.label}
|
||||||
|
bind:value={filterOpt}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<DatePicker
|
||||||
|
range={true}
|
||||||
|
label="Date Range"
|
||||||
|
on:change={e => {
|
||||||
|
if (e.detail[0].length > 1) {
|
||||||
|
startDate = e.detail[0][0].toISOString()
|
||||||
|
endDate = e.detail[0][1].toISOString()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button cta disabled={loading} on:click={createManualBackup}
|
||||||
|
>Create new backup</Button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="table">
|
||||||
|
<Table
|
||||||
|
{schema}
|
||||||
|
disableSorting
|
||||||
|
allowSelectRows={false}
|
||||||
|
allowEditColumns={false}
|
||||||
|
allowEditRows={false}
|
||||||
|
data={backupData}
|
||||||
|
{customRenderers}
|
||||||
|
placeholderText="No backups found"
|
||||||
|
border={false}
|
||||||
|
on:buttonclick={handleButtonClick}
|
||||||
|
/>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
.search :global(.spectrum-InputGroup) {
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
flex-basis: 160px;
|
||||||
|
width: 0;
|
||||||
|
min-width: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pagination {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-end;
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pro-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,23 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Table,
|
||||||
|
Select,
|
||||||
|
Pagination,
|
||||||
|
Button,
|
||||||
|
Body,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading>Embed</Heading>
|
||||||
|
<Body>View the automations your app has executed</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
</style>
|
|
@ -0,0 +1,4 @@
|
||||||
|
<script>
|
||||||
|
import { redirect } from "@roxi/routify"
|
||||||
|
$redirect("../settings/automation-history")
|
||||||
|
</script>
|
|
@ -0,0 +1,79 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Divider,
|
||||||
|
Heading,
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
Modal,
|
||||||
|
Icon,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
|
||||||
|
let updatingModal
|
||||||
|
|
||||||
|
$: filteredApps = $apps.filter(app => app.devId == $store.appId)
|
||||||
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
|
$: appUrl = `${window.origin}/app${app?.url}`
|
||||||
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading>Name and URL</Heading>
|
||||||
|
<Body>Edit your app's name and URL</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<Layout noPadding gap="XXS">
|
||||||
|
<Label size="L">Name</Label>
|
||||||
|
<Body>{app?.name}</Body>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Layout noPadding gap="XS">
|
||||||
|
<Label size="L">Icon</Label>
|
||||||
|
<div class="icon">
|
||||||
|
<Icon
|
||||||
|
size="L"
|
||||||
|
name={app?.icon?.name || "Apps"}
|
||||||
|
color={app?.icon?.color}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Layout noPadding gap="XXS">
|
||||||
|
<Label size="L">URL</Label>
|
||||||
|
<Body>{appUrl}</Body>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={() => {
|
||||||
|
updatingModal.show()
|
||||||
|
}}
|
||||||
|
disabled={appDeployed}
|
||||||
|
tooltip={appDeployed
|
||||||
|
? "You must unpublish your app to make changes to these settings"
|
||||||
|
: null}
|
||||||
|
icon={appDeployed ? "HelpOutline" : null}
|
||||||
|
>
|
||||||
|
Edit
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<Modal bind:this={updatingModal} padding={false} width="600px">
|
||||||
|
<UpdateAppModal {app} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.icon {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,39 @@
|
||||||
|
<script>
|
||||||
|
import { Layout, Heading, Body, Divider, Button } from "@budibase/bbui"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
|
||||||
|
let versionModal
|
||||||
|
|
||||||
|
$: updateAvailable = $store.upgradableVersion !== $store.version
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading>Version</Heading>
|
||||||
|
<Body>See the current version of your app and check for updates</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
{#if updateAvailable}
|
||||||
|
<Body>
|
||||||
|
The app is currently using version <strong>{$store.version}</strong>
|
||||||
|
but version <strong>{$store.upgradableVersion}</strong> is available.
|
||||||
|
<br />
|
||||||
|
Updates can contain new features, performance improvements and bug fixes.
|
||||||
|
</Body>
|
||||||
|
<div>
|
||||||
|
<Button cta on:click={versionModal.show}>Update app</Button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<Body>
|
||||||
|
The app is currently using version <strong>{$store.version}</strong>.
|
||||||
|
<br />
|
||||||
|
You're running the latest!
|
||||||
|
</Body>
|
||||||
|
<div>
|
||||||
|
<Button secondary on:click={versionModal.show}>Revert app</Button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<VersionModal bind:this={versionModal} hideIcon={true} />
|
Loading…
Reference in New Issue