Added embed state, behaviours and event tracking to client app. UI/UX Refactoring
This commit is contained in:
parent
370d7314b0
commit
2304032423
|
@ -126,6 +126,16 @@ http {
|
|||
proxy_pass http://app-service;
|
||||
}
|
||||
|
||||
location /embed {
|
||||
rewrite /embed/(.*) /app/$1 break;
|
||||
proxy_pass http://app-service;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header x-budibase-embed "true";
|
||||
add_header x-budibase-embed "true";
|
||||
add_header Content-Security-Policy "frame-ancestors *";
|
||||
}
|
||||
|
||||
location /builder {
|
||||
proxy_read_timeout 120s;
|
||||
proxy_connect_timeout 120s;
|
||||
|
|
|
@ -92,6 +92,16 @@ http {
|
|||
proxy_pass $apps;
|
||||
}
|
||||
|
||||
location /embed {
|
||||
rewrite /embed/(.*) /app/$1 break;
|
||||
proxy_pass $apps;
|
||||
proxy_redirect off;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header x-budibase-embed "true";
|
||||
add_header x-budibase-embed "true";
|
||||
add_header Content-Security-Policy "frame-ancestors *";
|
||||
}
|
||||
|
||||
location = / {
|
||||
proxy_pass $apps;
|
||||
}
|
||||
|
|
|
@ -14,10 +14,15 @@ async function servedBuilder(timezone: string) {
|
|||
await publishEvent(Event.SERVED_BUILDER, properties)
|
||||
}
|
||||
|
||||
async function servedApp(app: App, timezone: string) {
|
||||
async function servedApp(
|
||||
app: App,
|
||||
timezone: string,
|
||||
embed: boolean | undefined
|
||||
) {
|
||||
const properties: AppServedEvent = {
|
||||
appVersion: app.version,
|
||||
timezone,
|
||||
embed,
|
||||
}
|
||||
await publishEvent(Event.SERVED_APP, properties)
|
||||
}
|
||||
|
|
|
@ -333,7 +333,7 @@
|
|||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||
>
|
||||
{/if}
|
||||
{#if value.type === "string" && value.enum && canShowField(key)}
|
||||
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||
<Select
|
||||
on:change={e => onChange(e, key)}
|
||||
value={inputData[key]}
|
||||
|
|
|
@ -59,7 +59,6 @@
|
|||
$store.upgradableVersion !== $store.version
|
||||
|
||||
const initialiseApp = async () => {
|
||||
console.log("AppActions :: Reinitialise")
|
||||
const applicationPkg = await API.fetchAppPackage($store.devId)
|
||||
await store.actions.initialise(applicationPkg)
|
||||
}
|
||||
|
@ -73,8 +72,6 @@
|
|||
: ""
|
||||
}
|
||||
|
||||
$: deploymentString = updateDeploymentString(deployments)
|
||||
|
||||
const reviewPendingDeployments = (deployments, newDeployments) => {
|
||||
if (deployments.length > 0) {
|
||||
const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
|
||||
|
@ -185,8 +182,10 @@
|
|||
{#if updateAvailable}
|
||||
<div class="app-action-button version" on:click={versionModal.show}>
|
||||
<div class="app-action">
|
||||
<ActionButton quiet>
|
||||
<StatusLight notice />
|
||||
Update
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
@ -196,10 +195,10 @@
|
|||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||
>
|
||||
<div class="app-action-button users">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="app-action"
|
||||
id="builder-app-users-button"
|
||||
<div class="app-action" id="builder-app-users-button">
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="UserGroup"
|
||||
on:click={() => {
|
||||
store.update(state => {
|
||||
state.builderSidePanel = true
|
||||
|
@ -207,30 +206,33 @@
|
|||
})
|
||||
}}
|
||||
>
|
||||
<Icon name={"UserGroup"} />
|
||||
Users
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
</TourWrap>
|
||||
|
||||
<div class="app-action-button preview">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div class="app-action" on:click={previewApp}>
|
||||
<Icon name={"PlayCircle"} />
|
||||
|
||||
<div class="app-action">
|
||||
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||
Preview
|
||||
</ActionButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="app-action-button publish app-action-popover">
|
||||
<div bind:this={appActionPopoverAnchor}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<div
|
||||
class="app-action"
|
||||
class="app-action-button publish app-action-popover"
|
||||
on:click={() => {
|
||||
if (!appActionPopoverOpen) {
|
||||
appActionPopover.show()
|
||||
} else {
|
||||
appActionPopover.hide()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div bind:this={appActionPopoverAnchor}>
|
||||
<div class="app-action">
|
||||
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
||||
<span class="publish-open" id="builder-app-publish-button">
|
||||
|
@ -248,7 +250,7 @@
|
|||
align="right"
|
||||
disabled={!isPublished}
|
||||
anchor={appActionPopoverAnchor}
|
||||
offset={10}
|
||||
offset={35}
|
||||
on:close={() => {
|
||||
appActionPopoverOpen = false
|
||||
}}
|
||||
|
@ -283,7 +285,9 @@
|
|||
<Body size="S">
|
||||
<span class="publish-popover-status">
|
||||
{#if isPublished}
|
||||
<span class="status-text">{deploymentString}</span>
|
||||
<span class="status-text">
|
||||
{updateDeploymentString(deployments)}
|
||||
</span>
|
||||
<span class="unpublish-link">
|
||||
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
||||
</span>
|
||||
|
@ -297,8 +301,9 @@
|
|||
</Body>
|
||||
<div class="action-buttons">
|
||||
{#if $store.hasLock}
|
||||
<Button
|
||||
secondary
|
||||
{#if isPublished}
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="Code"
|
||||
on:click={() => {
|
||||
$goto("./settings/embed")
|
||||
|
@ -306,7 +311,8 @@
|
|||
}}
|
||||
>
|
||||
Embed
|
||||
</Button>
|
||||
</ActionButton>
|
||||
{/if}
|
||||
<Button
|
||||
cta
|
||||
on:click={publishApp}
|
||||
|
@ -350,6 +356,7 @@
|
|||
<style>
|
||||
.app-action-popover-content {
|
||||
padding: var(--spacing-xl);
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.app-action-popover-content :global(.icon svg.spectrum-Icon) {
|
||||
|
@ -410,17 +417,22 @@
|
|||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px var(--spacing-l);
|
||||
/* border-left: var(--border-light); */
|
||||
padding-right: var(--spacing-m);
|
||||
}
|
||||
|
||||
.app-action-button:hover {
|
||||
.app-action-button.publish:hover {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.app-action-button.publish {
|
||||
border-left: var(--border-light);
|
||||
padding: 0px var(--spacing-l);
|
||||
}
|
||||
|
||||
.app-action-button.version :global(.spectrum-ActionButton-label) {
|
||||
display: flex;
|
||||
gap: var(--spectrum-actionbutton-icon-gap);
|
||||
}
|
||||
|
||||
.app-action {
|
||||
|
|
|
@ -0,0 +1,45 @@
|
|||
<script>
|
||||
import { Layout, Input, notifications } from "@budibase/bbui"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { store } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { apps } from "stores/portal"
|
||||
import { API } from "api"
|
||||
|
||||
export const show = () => {
|
||||
deletionModal.show()
|
||||
}
|
||||
|
||||
export const hide = () => {
|
||||
deletionModal.hide()
|
||||
}
|
||||
|
||||
let deletionModal
|
||||
let deletionConfirmationAppName
|
||||
|
||||
const deleteApp = async () => {
|
||||
try {
|
||||
await API.deleteApp($store.appId)
|
||||
apps.load()
|
||||
notifications.success("App deleted successfully")
|
||||
$goto("/builder")
|
||||
} catch (err) {
|
||||
notifications.error("Error deleting app")
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={deletionModal}
|
||||
title="Delete app"
|
||||
okText="Delete"
|
||||
onOk={deleteApp}
|
||||
onCancel={() => (deletionConfirmationAppName = null)}
|
||||
disabled={deletionConfirmationAppName !== $store.name}
|
||||
>
|
||||
Are you sure you want to delete <b>{$store.name}</b>?
|
||||
<br />
|
||||
Please enter the app name below to confirm.
|
||||
<br /><br />
|
||||
<Input bind:value={deletionConfirmationAppName} placeholder={$store.name} />
|
||||
</ConfirmDialog>
|
|
@ -5,6 +5,7 @@
|
|||
notifications,
|
||||
ModalContent,
|
||||
ActionButton,
|
||||
Link,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { API } from "api"
|
||||
|
@ -28,16 +29,15 @@
|
|||
notifications.error(`Error reverting changes: ${error}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ActionButton
|
||||
quiet
|
||||
icon="Revert"
|
||||
size="M"
|
||||
tooltip="Revert changes"
|
||||
on:click={revertModal.show}
|
||||
{disabled}
|
||||
/>
|
||||
export const hide = () => {
|
||||
revertModal.hide()
|
||||
}
|
||||
|
||||
export const show = () => {
|
||||
revertModal.show()
|
||||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={revertModal}>
|
||||
<ModalContent
|
||||
|
|
|
@ -70,9 +70,7 @@
|
|||
</script>
|
||||
|
||||
{#if !hideIcon && updateAvailable}
|
||||
<StatusLight hoverable on:click={updateModal.show} notice>
|
||||
Update available
|
||||
</StatusLight>
|
||||
<StatusLight hoverable on:click={updateModal.show} notice>Update</StatusLight>
|
||||
{/if}
|
||||
<Modal bind:this={updateModal}>
|
||||
<ModalContent
|
||||
|
|
|
@ -11,7 +11,7 @@ export const TOUR_STEP_KEYS = {
|
|||
BUILDER_DATA_SECTION: "builder-data-section",
|
||||
BUILDER_DESIGN_SECTION: "builder-design-section",
|
||||
BUILDER_USER_MANAGEMENT: "builder-user-management",
|
||||
BUILDER_AUTOMATE_SECTION: "builder-automate-section",
|
||||
BUILDER_AUTOMATION_SECTION: "builder-automation-section",
|
||||
FEATURE_USER_MANAGEMENT: "feature-user-management",
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ const getTours = () => {
|
|||
title: "Data",
|
||||
route: "/builder/app/:application/data",
|
||||
layout: OnboardingData,
|
||||
query: ".topcenternav .spectrum-Tabs-item#builder-data-tab",
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||
onLoad: async () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||
},
|
||||
|
@ -45,20 +45,20 @@ const getTours = () => {
|
|||
title: "Design",
|
||||
route: "/builder/app/:application/design",
|
||||
layout: OnboardingDesign,
|
||||
query: ".topcenternav .spectrum-Tabs-item#builder-design-tab",
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||
},
|
||||
align: "left",
|
||||
},
|
||||
{
|
||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION,
|
||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
||||
title: "Automations",
|
||||
route: "/builder/app/:application/automate",
|
||||
query: ".topcenternav .spectrum-Tabs-item#builder-automate-tab",
|
||||
route: "/builder/app/:application/automation",
|
||||
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
||||
onLoad: () => {
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION)
|
||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
||||
},
|
||||
align: "left",
|
||||
},
|
||||
|
|
|
@ -4,18 +4,27 @@
|
|||
export let active = false
|
||||
</script>
|
||||
|
||||
<a on:click href={url} class:active>
|
||||
{#if url}
|
||||
<a on:click href={url} class:active>
|
||||
{text || ""}
|
||||
</a>
|
||||
</a>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<span on:click class:active>
|
||||
{text || ""}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
a {
|
||||
a,
|
||||
span {
|
||||
padding: var(--spacing-s) var(--spacing-m);
|
||||
color: var(--spectrum-global-color-gray-900);
|
||||
border-radius: 4px;
|
||||
transition: background 130ms ease-out;
|
||||
}
|
||||
.active,
|
||||
span:hover,
|
||||
a:hover {
|
||||
background-color: var(--spectrum-global-color-gray-200);
|
||||
cursor: pointer;
|
||||
|
|
|
@ -171,8 +171,10 @@
|
|||
{:else}
|
||||
<div class="secondary-editor">
|
||||
<Icon name="LockClosed" />
|
||||
<div class="secondary-editor-body">
|
||||
Another user is currently editing your screens and automations
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="topcenternav">
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
<script>
|
||||
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
||||
import { Page, Layout } from "@budibase/bbui"
|
||||
import { url, isActive, layout } from "@roxi/routify"
|
||||
import { Page, Layout, Input, notifications } from "@budibase/bbui"
|
||||
import { url, isActive, layout, goto } from "@roxi/routify"
|
||||
import { capitalise } from "helpers"
|
||||
|
||||
// $: $url(), console.log("Hello ", $url())
|
||||
// $: console.log("is auto", $isActive("./automation-history"))
|
||||
// $: selected = capitalise(
|
||||
// $layout.children.find(layout => $isActive(layout.path))?.title ?? "settings"
|
||||
// )
|
||||
// $: console.log("Settings Selected ", selected)
|
||||
import { store } from "builderStore"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { apps } from "stores/portal"
|
||||
import { API } from "api"
|
||||
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||
|
||||
// if updated to Settings and automation-history is not selected, switch to ./automation-history
|
||||
let deleteModal
|
||||
</script>
|
||||
|
||||
<!-- routify:options index=4 -->
|
||||
|
@ -50,6 +49,14 @@
|
|||
url={$url("./version")}
|
||||
active={$isActive("./version")}
|
||||
/>
|
||||
<div class="delete-action">
|
||||
<SideNavItem
|
||||
text="Delete app"
|
||||
on:click={() => {
|
||||
deleteModal.show()
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SideNav>
|
||||
<slot />
|
||||
</Content>
|
||||
|
@ -57,7 +64,15 @@
|
|||
</Page>
|
||||
</div>
|
||||
|
||||
<DeleteModal bind:this={deleteModal} />
|
||||
|
||||
<style>
|
||||
.delete-action :global(span) {
|
||||
color: #ee4331;
|
||||
}
|
||||
.delete-action {
|
||||
display: contents;
|
||||
}
|
||||
.settings {
|
||||
flex: 1 1 auto;
|
||||
display: flex;
|
||||
|
|
|
@ -191,13 +191,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!--
|
||||
From the apps list header
|
||||
isOwner = $auth.accountPortalAccess && $admin.cloud
|
||||
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()
|
||||
-->
|
||||
{` acc portal access ${$auth.user.accountPortalAccess} `}
|
||||
<!-- {#if (licensePlan?.type !== Constants.PlanType.ENTERPRISE && $auth.user.accountPortalAccess) || !$admin.cloud} -->
|
||||
{#if (!$licensing.isEnterprisePlan && $auth.user.accountPortalAccess) || !$admin.cloud}
|
||||
<Button secondary on:click={$licensing.goToUpgradePage()}>
|
||||
Get more history
|
||||
|
|
|
@ -15,10 +15,10 @@
|
|||
|
||||
$: filteredApps = $apps.filter(app => app.devId == $store.appId)
|
||||
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||
$: appUrl = `${window.origin}/app${app?.url}`
|
||||
$: appUrl = `${window.origin}/embed${app?.url}`
|
||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||
|
||||
$: embed = `<iframe width="560" height="315" frameborder="0" allow="clipboard-write" src="${appUrl}" ></iframe>`
|
||||
$: embed = `<iframe width="800" height="600" frameborder="0" allow="clipboard-write" src="${appUrl}" ></iframe>`
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
|
|
|
@ -1,252 +0,0 @@
|
|||
<script>
|
||||
import { url, isActive, goto } from "@roxi/routify"
|
||||
import {
|
||||
Page,
|
||||
Layout,
|
||||
Button,
|
||||
Icon,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
Helpers,
|
||||
Input,
|
||||
Modal,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import {
|
||||
Content,
|
||||
SideNav,
|
||||
SideNavItem,
|
||||
Breadcrumbs,
|
||||
Breadcrumb,
|
||||
Header,
|
||||
} from "components/portal/page"
|
||||
import { apps, overview } from "stores/portal"
|
||||
import { AppStatus } from "constants"
|
||||
import analytics, { Events, EventSource } from "analytics"
|
||||
import { store } from "builderStore"
|
||||
import EditableIcon from "components/common/EditableIcon.svelte"
|
||||
import { API } from "api"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
||||
import { syncURLToState } from "helpers/urlStateSync"
|
||||
import * as routify from "@roxi/routify"
|
||||
import { onDestroy } from "svelte"
|
||||
|
||||
// Keep URL and state in sync for selected app ID
|
||||
const stopSyncing = syncURLToState({
|
||||
urlParam: "appId",
|
||||
stateKey: "selectedAppId",
|
||||
validate: id => $apps.some(app => app.devId === id),
|
||||
fallbackUrl: "../../",
|
||||
store: overview,
|
||||
routify,
|
||||
})
|
||||
|
||||
let exportModal
|
||||
let deletionModal
|
||||
let exportPublishedVersion = false
|
||||
let deletionConfirmationAppName
|
||||
let loaded = false
|
||||
|
||||
$: app = $overview.selectedApp
|
||||
$: appId = $overview.selectedAppId
|
||||
$: initialiseApp(appId)
|
||||
$: isPublished = app?.status === AppStatus.DEPLOYED
|
||||
|
||||
const initialiseApp = async appId => {
|
||||
loaded = false
|
||||
try {
|
||||
const pkg = await API.fetchAppPackage(appId)
|
||||
await store.actions.initialise(pkg)
|
||||
await API.syncApp(appId)
|
||||
loaded = true
|
||||
} catch (error) {
|
||||
notifications.error("Error initialising app overview")
|
||||
$goto("../../")
|
||||
}
|
||||
}
|
||||
|
||||
const viewApp = () => {
|
||||
if (isPublished) {
|
||||
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
|
||||
appId: $store.appId,
|
||||
eventSource: EventSource.PORTAL,
|
||||
})
|
||||
window.open(`/app${app?.url}`, "_blank")
|
||||
}
|
||||
}
|
||||
|
||||
const editApp = () => {
|
||||
$goto(`../../../app/${app.devId}`)
|
||||
}
|
||||
|
||||
const exportApp = opts => {
|
||||
exportPublishedVersion = !!opts?.published
|
||||
exportModal.show()
|
||||
}
|
||||
|
||||
const copyAppId = async () => {
|
||||
await Helpers.copyToClipboard(app.prodId)
|
||||
notifications.success("App ID copied to clipboard")
|
||||
}
|
||||
|
||||
const deleteApp = async () => {
|
||||
try {
|
||||
await API.deleteApp(app?.devId)
|
||||
apps.load()
|
||||
notifications.success("App deleted successfully")
|
||||
$goto("../../")
|
||||
} catch (err) {
|
||||
notifications.error("Error deleting app")
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy(() => {
|
||||
stopSyncing()
|
||||
store.actions.reset()
|
||||
})
|
||||
</script>
|
||||
|
||||
{#key appId}
|
||||
<Page>
|
||||
<Layout noPadding gap="L">
|
||||
<Breadcrumbs>
|
||||
<Breadcrumb url={$url("../")} text="Apps" />
|
||||
<Breadcrumb text={app?.name} />
|
||||
</Breadcrumbs>
|
||||
<Header title={app?.name} wrap={false}>
|
||||
<div slot="icon">
|
||||
<EditableIcon
|
||||
{app}
|
||||
autoSave
|
||||
size="XL"
|
||||
name={app?.icon?.name || "Apps"}
|
||||
color={app?.icon?.color}
|
||||
/>
|
||||
</div>
|
||||
<div slot="buttons">
|
||||
<span class="desktop">
|
||||
<Button
|
||||
size="M"
|
||||
quiet
|
||||
secondary
|
||||
disabled={!isPublished}
|
||||
on:click={viewApp}
|
||||
>
|
||||
View
|
||||
</Button>
|
||||
</span>
|
||||
<span class="desktop">
|
||||
<Button size="M" cta on:click={editApp}>Edit</Button>
|
||||
</span>
|
||||
<ActionMenu align="right">
|
||||
<span slot="control" class="app-overview-actions-icon">
|
||||
<Icon hoverable name="More" />
|
||||
</span>
|
||||
<span class="mobile">
|
||||
<MenuItem icon="Globe" disabled={!isPublished} on:click={viewApp}>
|
||||
View
|
||||
</MenuItem>
|
||||
</span>
|
||||
<span class="mobile">
|
||||
<MenuItem icon="Edit" on:click={editApp}>Edit</MenuItem>
|
||||
</span>
|
||||
<MenuItem
|
||||
on:click={() => exportApp({ published: false })}
|
||||
icon="DownloadFromCloud"
|
||||
>
|
||||
Export latest
|
||||
</MenuItem>
|
||||
{#if isPublished}
|
||||
<MenuItem
|
||||
on:click={() => exportApp({ published: true })}
|
||||
icon="DownloadFromCloudOutline"
|
||||
>
|
||||
Export published
|
||||
</MenuItem>
|
||||
<MenuItem on:click={copyAppId} icon="Copy">Copy app ID</MenuItem>
|
||||
{/if}
|
||||
{#if !isPublished}
|
||||
<MenuItem on:click={deletionModal.show} icon="Delete">
|
||||
Delete
|
||||
</MenuItem>
|
||||
{/if}
|
||||
</ActionMenu>
|
||||
</div>
|
||||
</Header>
|
||||
<Content showMobileNav>
|
||||
<SideNav slot="side-nav">
|
||||
<SideNavItem
|
||||
text="Overview"
|
||||
url={$url("./overview")}
|
||||
active={$isActive("./overview")}
|
||||
/>
|
||||
<SideNavItem
|
||||
text="Access"
|
||||
url={$url("./access")}
|
||||
active={$isActive("./access")}
|
||||
/>
|
||||
<SideNavItem
|
||||
text="Automation History"
|
||||
url={$url("./automation-history")}
|
||||
active={$isActive("./automation-history")}
|
||||
/>
|
||||
<SideNavItem
|
||||
text="Backups"
|
||||
url={$url("./backups")}
|
||||
active={$isActive("./backups")}
|
||||
/>
|
||||
<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>
|
||||
{#if loaded}
|
||||
<slot />
|
||||
{/if}
|
||||
</Content>
|
||||
</Layout>
|
||||
</Page>
|
||||
|
||||
<Modal bind:this={exportModal} padding={false}>
|
||||
<ExportAppModal {app} published={exportPublishedVersion} />
|
||||
</Modal>
|
||||
<ConfirmDialog
|
||||
bind:this={deletionModal}
|
||||
title="Delete app"
|
||||
okText="Delete"
|
||||
onOk={deleteApp}
|
||||
onCancel={() => (deletionConfirmationAppName = null)}
|
||||
disabled={deletionConfirmationAppName !== app?.name}
|
||||
>
|
||||
Are you sure you want to delete <b>{app?.name}</b>?
|
||||
<br />
|
||||
Please enter the app name below to confirm.
|
||||
<br /><br />
|
||||
<Input bind:value={deletionConfirmationAppName} placeholder={app?.name} />
|
||||
</ConfirmDialog>
|
||||
{/key}
|
||||
|
||||
<style>
|
||||
.desktop {
|
||||
display: contents;
|
||||
}
|
||||
.mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.desktop {
|
||||
display: none;
|
||||
}
|
||||
.mobile {
|
||||
display: contents;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1,211 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
ModalContent,
|
||||
PickerDropdown,
|
||||
ActionButton,
|
||||
Layout,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { roles } from "stores/backend"
|
||||
import { groups, users, licensing, apps } from "stores/portal"
|
||||
import { Constants, RoleUtils, fetchData } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import { createEventDispatcher } from "svelte"
|
||||
|
||||
export let app
|
||||
export let appUsers = []
|
||||
export let showUsers = false
|
||||
export let showGroups = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
const usersFetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "user",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
email: "",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
let search = ""
|
||||
let data = [{ id: "", role: "" }]
|
||||
|
||||
$: usersFetch.update({
|
||||
query: {
|
||||
email: search,
|
||||
},
|
||||
})
|
||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
||||
$: availableUsers = getAvailableUsers($usersFetch.rows, appUsers, data)
|
||||
$: availableGroups = getAvailableGroups($groups, app.appId, search, data)
|
||||
$: valid = data?.length && !data?.some(x => !x.id?.length || !x.role?.length)
|
||||
$: optionSections = {
|
||||
...(showGroups &&
|
||||
$licensing.groupsEnabled &&
|
||||
availableGroups.length && {
|
||||
["User groups"]: {
|
||||
data: availableGroups,
|
||||
getLabel: group => group.name,
|
||||
getValue: group => group._id,
|
||||
getIcon: group => group.icon,
|
||||
getColour: group => group.color,
|
||||
},
|
||||
}),
|
||||
...(showUsers && {
|
||||
users: {
|
||||
data: availableUsers,
|
||||
getLabel: user => user.email,
|
||||
getValue: user => user._id,
|
||||
getIcon: user => user.icon,
|
||||
getColour: user => user.color,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
const addData = async appData => {
|
||||
const gr_prefix = "gr"
|
||||
const us_prefix = "us"
|
||||
for (let data of appData) {
|
||||
// Assign group
|
||||
if (data.id.startsWith(gr_prefix)) {
|
||||
const group = $groups.find(group => {
|
||||
return group._id === data.id
|
||||
})
|
||||
if (!group) {
|
||||
continue
|
||||
}
|
||||
await groups.actions.addApp(group._id, fixedAppId, data.role)
|
||||
}
|
||||
// Assign user
|
||||
else if (data.id.startsWith(us_prefix)) {
|
||||
const user = await users.get(data.id)
|
||||
await users.save({
|
||||
...user,
|
||||
roles: {
|
||||
...user.roles,
|
||||
[fixedAppId]: data.role,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh data when completed
|
||||
await usersFetch.refresh()
|
||||
dispatch("update")
|
||||
}
|
||||
|
||||
const getAvailableUsers = (allUsers, appUsers, newUsers) => {
|
||||
return (allUsers || []).filter(user => {
|
||||
// Filter out admin users
|
||||
if (user?.admin?.global || user?.builder?.global) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out assigned users
|
||||
if (appUsers.find(x => x._id === user._id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out new users which are going to be assigned
|
||||
return !newUsers.find(x => x.id === user._id)
|
||||
})
|
||||
}
|
||||
|
||||
const getAvailableGroups = (allGroups, appId, search, newGroups) => {
|
||||
search = search?.toLowerCase()
|
||||
return (allGroups || []).filter(group => {
|
||||
// Filter out assigned groups
|
||||
const appIds = groups.actions.getGroupAppIds(group)
|
||||
if (appIds.includes(apps.getProdAppID(appId))) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Filter out new groups which are going to be assigned
|
||||
if (newGroups.find(x => x.id === group._id)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Match search string
|
||||
return !search || group.name.toLowerCase().includes(search)
|
||||
})
|
||||
}
|
||||
|
||||
function addNewInput() {
|
||||
data = [...data, { id: "", role: "" }]
|
||||
}
|
||||
|
||||
const removeItem = index => {
|
||||
data = data.filter((x, idx) => idx !== index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<ModalContent
|
||||
size="M"
|
||||
title="Assign access to your app"
|
||||
confirmText="Done"
|
||||
cancelText="Cancel"
|
||||
onConfirm={() => addData(data)}
|
||||
showCloseIcon={false}
|
||||
disabled={!valid}
|
||||
>
|
||||
{#if data.length}
|
||||
<Layout noPadding gap="XS">
|
||||
{#each data as input, index}
|
||||
<div class="item">
|
||||
<div class="picker">
|
||||
<PickerDropdown
|
||||
autocomplete
|
||||
showClearIcon={false}
|
||||
primaryOptions={optionSections}
|
||||
secondaryOptions={$roles.filter(
|
||||
x => x._id !== Constants.Roles.PUBLIC
|
||||
)}
|
||||
secondaryPlaceholder="Access"
|
||||
bind:primaryValue={input.id}
|
||||
bind:secondaryValue={input.role}
|
||||
bind:searchTerm={search}
|
||||
getPrimaryOptionLabel={group => group.name}
|
||||
getPrimaryOptionValue={group => group.name}
|
||||
getPrimaryOptionIcon={group => group.icon}
|
||||
getPrimaryOptionColour={group => group.colour}
|
||||
getSecondaryOptionLabel={role => role.name}
|
||||
getSecondaryOptionValue={role => role._id}
|
||||
getSecondaryOptionColour={role =>
|
||||
RoleUtils.getRoleColour(role._id)}
|
||||
/>
|
||||
</div>
|
||||
<div class="icon">
|
||||
<Icon
|
||||
name="Close"
|
||||
hoverable
|
||||
size="S"
|
||||
on:click={() => removeItem(index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Layout>
|
||||
{/if}
|
||||
<div>
|
||||
<ActionButton on:click={addNewInput} icon="Add">Add more</ActionButton>
|
||||
</div>
|
||||
</ModalContent>
|
||||
|
||||
<style>
|
||||
.item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.picker {
|
||||
width: calc(100% - 30px);
|
||||
}
|
||||
.icon {
|
||||
width: 20px;
|
||||
}
|
||||
</style>
|
|
@ -1,26 +0,0 @@
|
|||
<script>
|
||||
import RoleSelect from "components/common/RoleSelect.svelte"
|
||||
import { getContext } from "svelte"
|
||||
|
||||
const rolesContext = getContext("roles")
|
||||
|
||||
export let value
|
||||
export let row
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<RoleSelect
|
||||
{value}
|
||||
quiet
|
||||
allowRemove
|
||||
allowPublic={false}
|
||||
on:change={e => rolesContext.updateRole(e.detail, row._id)}
|
||||
on:remove={() => rolesContext.removeRole(row._id)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
div {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -1,271 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Layout,
|
||||
Heading,
|
||||
Body,
|
||||
Button,
|
||||
Modal,
|
||||
notifications,
|
||||
Pagination,
|
||||
Divider,
|
||||
Table,
|
||||
} from "@budibase/bbui"
|
||||
import { onMount, setContext } from "svelte"
|
||||
import { users, groups, apps, licensing, overview } from "stores/portal"
|
||||
import AssignmentModal from "./_components/AssignmentModal.svelte"
|
||||
import { roles } from "stores/backend"
|
||||
import { API } from "api"
|
||||
import { fetchData } from "@budibase/frontend-core"
|
||||
import EditableRoleRenderer from "./_components/EditableRoleRenderer.svelte"
|
||||
|
||||
const userSchema = {
|
||||
email: {
|
||||
type: "string",
|
||||
width: "1fr",
|
||||
},
|
||||
role: {
|
||||
displayName: "Access",
|
||||
width: "150px",
|
||||
borderLeft: true,
|
||||
},
|
||||
}
|
||||
const groupSchema = {
|
||||
name: {
|
||||
type: "string",
|
||||
width: "1fr",
|
||||
},
|
||||
role: {
|
||||
displayName: "Access",
|
||||
width: "150px",
|
||||
borderLeft: true,
|
||||
},
|
||||
}
|
||||
const customRenderers = [
|
||||
{
|
||||
column: "role",
|
||||
component: EditableRoleRenderer,
|
||||
},
|
||||
]
|
||||
|
||||
let assignmentModal
|
||||
let appGroups
|
||||
let appUsers
|
||||
let showAddUsers = false
|
||||
let showAddGroups = false
|
||||
|
||||
$: app = $overview.selectedApp
|
||||
$: devAppId = app.devId
|
||||
$: prodAppId = apps.getProdAppID(app.devId)
|
||||
$: usersFetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "user",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
appId: apps.getProdAppID(devAppId),
|
||||
},
|
||||
},
|
||||
})
|
||||
$: appUsers = getAppUsers($usersFetch.rows, prodAppId)
|
||||
$: appGroups = getAppGroups($groups, prodAppId)
|
||||
|
||||
const getAppUsers = (users, appId) => {
|
||||
return users.map(user => ({
|
||||
...user,
|
||||
role: user.roles[Object.keys(user.roles).find(x => x === appId)],
|
||||
}))
|
||||
}
|
||||
|
||||
const getAppGroups = (allGroups, appId) => {
|
||||
return allGroups
|
||||
.filter(group => {
|
||||
if (!group.roles) {
|
||||
return false
|
||||
}
|
||||
return groups.actions.getGroupAppIds(group).includes(appId)
|
||||
})
|
||||
.map(group => ({
|
||||
...group,
|
||||
role: group.roles[
|
||||
groups.actions.getGroupAppIds(group).find(x => x === appId)
|
||||
],
|
||||
}))
|
||||
}
|
||||
|
||||
const updateRole = async (role, id) => {
|
||||
// Check if this is a user or a group
|
||||
if ($usersFetch.rows.some(user => user._id === id)) {
|
||||
await updateUserRole(role, id)
|
||||
} else {
|
||||
await updateGroupRole(role, id)
|
||||
}
|
||||
}
|
||||
|
||||
const removeRole = async id => {
|
||||
// Check if this is a user or a group
|
||||
if ($usersFetch.rows.some(user => user._id === id)) {
|
||||
await removeUserRole(id)
|
||||
} else {
|
||||
await removeGroupRole(id)
|
||||
}
|
||||
}
|
||||
|
||||
const updateUserRole = async (role, userId) => {
|
||||
const user = $usersFetch.rows.find(user => user._id === userId)
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
user.roles[prodAppId] = role
|
||||
await users.save(user)
|
||||
await usersFetch.refresh()
|
||||
}
|
||||
|
||||
const removeUserRole = async userId => {
|
||||
const user = $usersFetch.rows.find(user => user._id === userId)
|
||||
if (!user) {
|
||||
return
|
||||
}
|
||||
const filteredRoles = { ...user.roles }
|
||||
delete filteredRoles[prodAppId]
|
||||
await users.save({
|
||||
...user,
|
||||
roles: {
|
||||
...filteredRoles,
|
||||
},
|
||||
})
|
||||
await usersFetch.refresh()
|
||||
}
|
||||
|
||||
const updateGroupRole = async (role, groupId) => {
|
||||
const group = $groups.find(group => group._id === groupId)
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
await groups.actions.addApp(group._id, prodAppId, role)
|
||||
await usersFetch.refresh()
|
||||
}
|
||||
|
||||
const removeGroupRole = async groupId => {
|
||||
const group = $groups.find(group => group._id === groupId)
|
||||
if (!group) {
|
||||
return
|
||||
}
|
||||
await groups.actions.removeApp(group._id, prodAppId)
|
||||
await usersFetch.refresh()
|
||||
}
|
||||
|
||||
const showUsersModal = () => {
|
||||
showAddUsers = true
|
||||
showAddGroups = false
|
||||
assignmentModal.show()
|
||||
}
|
||||
|
||||
const showGroupsModal = () => {
|
||||
showAddUsers = false
|
||||
showAddGroups = true
|
||||
assignmentModal.show()
|
||||
}
|
||||
|
||||
setContext("roles", {
|
||||
updateRole,
|
||||
removeRole,
|
||||
})
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
await roles.fetch()
|
||||
} catch (error) {
|
||||
notifications.error(error)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
<Layout gap="XS" noPadding>
|
||||
<Heading>Access</Heading>
|
||||
<Body>Assign users to your app and set their access</Body>
|
||||
</Layout>
|
||||
<Divider />
|
||||
<Layout noPadding gap="L">
|
||||
{#if $usersFetch.loaded}
|
||||
<Layout noPadding gap="S">
|
||||
<div class="title">
|
||||
<Heading size="S">Users</Heading>
|
||||
<Button cta on:click={showUsersModal}>Assign user</Button>
|
||||
</div>
|
||||
<Table
|
||||
customPlaceholder
|
||||
data={appUsers}
|
||||
schema={userSchema}
|
||||
allowEditRows={false}
|
||||
{customRenderers}
|
||||
>
|
||||
<div class="placeholder" slot="placeholder">
|
||||
<Heading size="S">You have no users assigned yet</Heading>
|
||||
</div>
|
||||
</Table>
|
||||
<div class="pagination">
|
||||
<Pagination
|
||||
page={$usersFetch.pageNumber + 1}
|
||||
hasPrevPage={$usersFetch.hasPrevPage}
|
||||
hasNextPage={$usersFetch.hasNextPage}
|
||||
goToPrevPage={$usersFetch.loading ? null : usersFetch.prevPage}
|
||||
goToNextPage={$usersFetch.loading ? null : usersFetch.nextPage}
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
|
||||
{#if $usersFetch.loaded && $licensing.groupsEnabled}
|
||||
<Layout noPadding gap="S">
|
||||
<div class="title">
|
||||
<Heading size="S">Groups</Heading>
|
||||
<Button cta on:click={showGroupsModal}>Assign group</Button>
|
||||
</div>
|
||||
<Table
|
||||
customPlaceholder
|
||||
data={appGroups}
|
||||
schema={groupSchema}
|
||||
allowEditRows={false}
|
||||
{customRenderers}
|
||||
>
|
||||
<div class="placeholder" slot="placeholder">
|
||||
<Heading size="S">You have no groups assigned yet</Heading>
|
||||
</div>
|
||||
</Table>
|
||||
</Layout>
|
||||
{/if}
|
||||
</Layout>
|
||||
</Layout>
|
||||
|
||||
<Modal bind:this={assignmentModal}>
|
||||
<AssignmentModal
|
||||
{app}
|
||||
{appUsers}
|
||||
on:update={usersFetch.refresh}
|
||||
showGroups={showAddGroups}
|
||||
showUsers={showAddUsers}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.title {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: flex-end;
|
||||
}
|
||||
.placeholder {
|
||||
flex: 1 1 auto;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.pagination {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-end;
|
||||
margin-top: calc(-1 * var(--spacing-s));
|
||||
}
|
||||
</style>
|
|
@ -1,82 +0,0 @@
|
|||
<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(`../../../../app/${appId}/automate/${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>
|
|
@ -1,27 +0,0 @@
|
|||
<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>
|
|
@ -1,263 +0,0 @@
|
|||
<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 } from "builderStore"
|
||||
import { createPaginationStore } from "helpers/pagination"
|
||||
import { getContext, onDestroy, onMount } from "svelte"
|
||||
import dayjs from "dayjs"
|
||||
import { auth, licensing, admin, overview } 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
|
||||
$: app = $overview.selectedApp
|
||||
$: 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={app.devId}
|
||||
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>
|
|
@ -1,87 +0,0 @@
|
|||
<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>
|
|
@ -1,41 +0,0 @@
|
|||
<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>
|
|
@ -1,27 +0,0 @@
|
|||
<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>
|
|
@ -1,21 +0,0 @@
|
|||
<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>
|
|
@ -1,15 +0,0 @@
|
|||
<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>
|
|
@ -1,10 +0,0 @@
|
|||
<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>
|
|
@ -1,36 +0,0 @@
|
|||
<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>
|
|
@ -1,18 +0,0 @@
|
|||
<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>
|
|
@ -1,344 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Button,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Layout,
|
||||
notifications,
|
||||
Pagination,
|
||||
Select,
|
||||
Heading,
|
||||
Body,
|
||||
Tags,
|
||||
Tag,
|
||||
Table,
|
||||
} from "@budibase/bbui"
|
||||
import { backups, licensing, auth, admin, overview } from "stores/portal"
|
||||
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 },
|
||||
},
|
||||
]
|
||||
|
||||
$: app = $overview.selectedApp
|
||||
$: 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: app.instance._id,
|
||||
...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: app.instance._id,
|
||||
})
|
||||
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: app.instance._id,
|
||||
backupId: detail.backupId,
|
||||
})
|
||||
await fetchBackups(filterOpt, page)
|
||||
} else if (detail.type === "backupRestore") {
|
||||
await backups.restoreBackup({
|
||||
appId: app.instance._id,
|
||||
backupId: detail.backupId,
|
||||
name: detail.restoreBackupName,
|
||||
})
|
||||
await fetchBackups(filterOpt, page)
|
||||
} else if (detail.type === "backupUpdate") {
|
||||
await backups.updateBackup({
|
||||
appId: app.instance._id,
|
||||
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);
|
||||
}
|
||||
|
||||
.table {
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.center {
|
||||
text-align: center;
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
|
@ -1,4 +0,0 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
$redirect("./overview")
|
||||
</script>
|
|
@ -1,77 +0,0 @@
|
|||
<script>
|
||||
import {
|
||||
Layout,
|
||||
Divider,
|
||||
Heading,
|
||||
Body,
|
||||
Button,
|
||||
Label,
|
||||
Modal,
|
||||
Icon,
|
||||
} from "@budibase/bbui"
|
||||
import { AppStatus } from "constants"
|
||||
import { overview } from "stores/portal"
|
||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||
|
||||
let updatingModal
|
||||
|
||||
$: app = $overview.selectedApp
|
||||
$: 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={$overview.selectedApp} />
|
||||
</Modal>
|
||||
|
||||
<style>
|
||||
.icon {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
</style>
|
|
@ -1,368 +0,0 @@
|
|||
<script>
|
||||
import { onMount } from "svelte"
|
||||
import DashCard from "components/common/DashCard.svelte"
|
||||
import { AppStatus } from "constants"
|
||||
import { goto } from "@roxi/routify"
|
||||
import {
|
||||
Icon,
|
||||
Heading,
|
||||
Link,
|
||||
Layout,
|
||||
Body,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { store } from "builderStore"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
import { users, auth, apps, groups, overview } from "stores/portal"
|
||||
import { fetchData, UserAvatar } from "@budibase/frontend-core"
|
||||
import { API } from "api"
|
||||
import GroupIcon from "../../users/groups/_components/GroupIcon.svelte"
|
||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||
|
||||
let appEditor
|
||||
let unpublishModal
|
||||
let deployments
|
||||
|
||||
$: app = $overview.selectedApp
|
||||
$: devAppId = app.devId
|
||||
$: prodAppId = apps.getProdAppID(devAppId)
|
||||
$: appUsersFetch = fetchData({
|
||||
API,
|
||||
datasource: {
|
||||
type: "user",
|
||||
},
|
||||
options: {
|
||||
query: {
|
||||
appId: apps.getProdAppID(devAppId),
|
||||
},
|
||||
},
|
||||
})
|
||||
$: updateAvailable = $store.upgradableVersion !== $store.version
|
||||
$: isPublished = app?.status === AppStatus.DEPLOYED
|
||||
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
||||
$: appEditorText = appEditor?.firstName || appEditor?.email
|
||||
$: fetchAppEditor(appEditorId)
|
||||
$: appUsers = $appUsersFetch.rows || []
|
||||
$: appGroups = $groups.filter(group => {
|
||||
if (!group.roles) {
|
||||
return false
|
||||
}
|
||||
return groups.actions.getGroupAppIds(group).includes(prodAppId)
|
||||
})
|
||||
|
||||
const updateDeploymentString = () => {
|
||||
return deployments?.length
|
||||
? processStringSync(
|
||||
"Last published {{ duration time 'millisecond' }} ago",
|
||||
{
|
||||
time:
|
||||
new Date().getTime() -
|
||||
new Date(deployments[0].updatedAt).getTime(),
|
||||
}
|
||||
)
|
||||
: ""
|
||||
}
|
||||
// App is updating in the layout asynchronously
|
||||
$: if ($store.appId?.length) {
|
||||
fetchDeployments().then(resp => {
|
||||
deployments = resp
|
||||
})
|
||||
}
|
||||
$: deploymentString = updateDeploymentString(deployments)
|
||||
|
||||
async function fetchAppEditor(editorId) {
|
||||
appEditor = await users.get(editorId)
|
||||
}
|
||||
|
||||
const confirmUnpublishApp = async () => {
|
||||
try {
|
||||
await API.unpublishApp(app.prodId)
|
||||
await apps.load()
|
||||
notifications.success("App unpublished successfully")
|
||||
} catch (err) {
|
||||
notifications.error("Error unpublishing app")
|
||||
}
|
||||
}
|
||||
|
||||
const reviewPendingDeployments = (deployments, newDeployments) => {
|
||||
if (deployments?.length > 0) {
|
||||
const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
|
||||
if (pending.length) {
|
||||
notifications.warning(
|
||||
"Deployment has been queued and will be processed shortly"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
async function fetchDeployments() {
|
||||
try {
|
||||
const newDeployments = await API.getAppDeployments()
|
||||
reviewPendingDeployments(deployments, newDeployments)
|
||||
return newDeployments
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
notifications.error("Error fetching deployment history")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
deployments = await fetchDeployments()
|
||||
})
|
||||
</script>
|
||||
|
||||
<div class="overview-tab">
|
||||
<Layout noPadding gap="XL">
|
||||
<div class="top">
|
||||
<DashCard title={"App Status"}>
|
||||
<div class="status-content">
|
||||
<div class="status-display">
|
||||
{#if isPublished}
|
||||
<Icon name="GlobeCheck" size="XL" disabled={false} />
|
||||
<span>Published</span>
|
||||
{:else}
|
||||
<Icon name="GlobeStrike" size="XL" disabled={true} />
|
||||
<span class="disabled">Unpublished</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="status-text">
|
||||
{#if isPublished}
|
||||
{deploymentString}
|
||||
- <Link on:click={unpublishModal.show}>Unpublish</Link>
|
||||
{/if}
|
||||
|
||||
{#if !deployments?.length}
|
||||
-
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</DashCard>
|
||||
{#if appEditor}
|
||||
<DashCard title={"Last Edited"}>
|
||||
<div class="last-edited-content">
|
||||
<div class="updated-by">
|
||||
{#if appEditor}
|
||||
<UserAvatar user={appEditor} showTooltip={false} />
|
||||
<div class="editor-name">
|
||||
{appEditor._id === $auth.user._id ? "You" : appEditorText}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="last-edit-text">
|
||||
{#if app}
|
||||
{processStringSync(
|
||||
"Last edited {{ duration time 'millisecond' }} ago",
|
||||
{
|
||||
time:
|
||||
new Date().getTime() - new Date(app?.updatedAt).getTime(),
|
||||
}
|
||||
)}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</DashCard>
|
||||
{/if}
|
||||
<DashCard
|
||||
title={"Version"}
|
||||
showIcon={true}
|
||||
action={() => {
|
||||
$goto("./version")
|
||||
}}
|
||||
>
|
||||
<div class="version-content">
|
||||
<Heading size="XS">{$store.version}</Heading>
|
||||
{#if updateAvailable}
|
||||
<div class="version-status">
|
||||
New version <strong>{$store.upgradableVersion}</strong> is
|
||||
available -
|
||||
<Link
|
||||
on:click={() => {
|
||||
$goto("./version")
|
||||
}}
|
||||
>
|
||||
Update
|
||||
</Link>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="version-status">You're running the latest!</div>
|
||||
{/if}
|
||||
</div>
|
||||
</DashCard>
|
||||
{#if $appUsersFetch.loaded}
|
||||
<DashCard
|
||||
title={"Access"}
|
||||
showIcon={true}
|
||||
action={() => {
|
||||
$goto("./access")
|
||||
}}
|
||||
>
|
||||
{#if appUsers.length || appGroups.length}
|
||||
<Layout noPadding gap="S">
|
||||
<div class="access-tab-content">
|
||||
{#if appUsers.length}
|
||||
<div class="users">
|
||||
<div class="list">
|
||||
{#each appUsers.slice(0, 4) as user}
|
||||
<UserAvatar {user} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="text">
|
||||
{appUsers.length}
|
||||
{appUsers.length > 1 ? "users" : "user"} assigned
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if appGroups.length}
|
||||
<div class="groups">
|
||||
<div class="list">
|
||||
{#each appGroups.slice(0, 4) as group}
|
||||
<GroupIcon {group} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="text">
|
||||
{appGroups.length} user
|
||||
{appGroups.length > 1 ? "groups" : "group"} assigned
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Layout>
|
||||
{:else}
|
||||
<Layout noPadding gap="S">
|
||||
<Body>No users</Body>
|
||||
<div class="users-text">
|
||||
No users have been assigned to this app
|
||||
</div>
|
||||
</Layout>
|
||||
{/if}
|
||||
</DashCard>
|
||||
{/if}
|
||||
</div>
|
||||
{#if false}
|
||||
<div class="bottom">
|
||||
<DashCard
|
||||
title={"Automation History"}
|
||||
action={() => {
|
||||
$goto("../automation-history")
|
||||
}}
|
||||
>
|
||||
<div class="automation-content">
|
||||
<div class="automation-metrics">
|
||||
<div class="succeeded">
|
||||
<Heading size="XL">0</Heading>
|
||||
<div class="metric-info">
|
||||
<Icon name="CheckmarkCircle" />
|
||||
Success
|
||||
</div>
|
||||
</div>
|
||||
<div class="failed">
|
||||
<Heading size="XL">0</Heading>
|
||||
<div class="metric-info">
|
||||
<Icon name="Alert" />
|
||||
Error
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashCard>
|
||||
<DashCard
|
||||
title={"Backups"}
|
||||
action={() => {
|
||||
$goto("../backups")
|
||||
}}
|
||||
>
|
||||
<div class="backups-content">test</div>
|
||||
</DashCard>
|
||||
</div>
|
||||
{/if}
|
||||
</Layout>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
bind:this={unpublishModal}
|
||||
title="Confirm unpublish"
|
||||
okText="Unpublish app"
|
||||
onOk={confirmUnpublishApp}
|
||||
>
|
||||
Are you sure you want to unpublish the app <b>{app?.name}</b>?
|
||||
</ConfirmDialog>
|
||||
|
||||
<style>
|
||||
.overview-tab {
|
||||
display: grid;
|
||||
}
|
||||
|
||||
.overview-tab .top {
|
||||
display: grid;
|
||||
grid-gap: var(--spectrum-alias-grid-gutter-medium);
|
||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||
}
|
||||
|
||||
.access-tab-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
align-items: flex-start;
|
||||
gap: var(--spacing-xl);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.access-tab-content > * {
|
||||
flex: 1 1 0;
|
||||
}
|
||||
.access-tab-content .list {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
}
|
||||
.access-tab-content .text {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
margin-top: var(--spacing-xl);
|
||||
}
|
||||
|
||||
.overview-tab .bottom,
|
||||
.automation-metrics {
|
||||
display: grid;
|
||||
grid-gap: var(--spectrum-alias-grid-gutter-large);
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.status-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.status-text,
|
||||
.last-edit-text {
|
||||
color: var(--spectrum-global-color-gray-600);
|
||||
}
|
||||
|
||||
.updated-by {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
|
||||
.succeeded :global(.icon) {
|
||||
color: var(--spectrum-global-color-green-600);
|
||||
}
|
||||
|
||||
.failed :global(.icon) {
|
||||
color: var(
|
||||
--spectrum-semantic-negative-color-default,
|
||||
var(--spectrum-global-color-red-500)
|
||||
);
|
||||
}
|
||||
|
||||
.metric-info {
|
||||
display: flex;
|
||||
gap: var(--spacing-l);
|
||||
margin-top: var(--spacing-s);
|
||||
}
|
||||
|
||||
.version-status,
|
||||
.last-edit-text,
|
||||
.status-text {
|
||||
padding-top: var(--spacing-xl);
|
||||
}
|
||||
</style>
|
|
@ -1,39 +0,0 @@
|
|||
<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} />
|
|
@ -1,15 +0,0 @@
|
|||
<script>
|
||||
import { groups } from "stores/portal"
|
||||
import { onMount } from "svelte"
|
||||
|
||||
let loaded = false
|
||||
|
||||
onMount(async () => {
|
||||
await groups.actions.init()
|
||||
loaded = true
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<slot />
|
||||
{/if}
|
|
@ -1,4 +0,0 @@
|
|||
<script>
|
||||
import { redirect } from "@roxi/routify"
|
||||
$redirect("../")
|
||||
</script>
|
|
@ -46,6 +46,7 @@
|
|||
|
||||
let dataLoaded = false
|
||||
let permissionError = false
|
||||
let embedNoScreens = false
|
||||
|
||||
// Determine if we should show devtools or not
|
||||
$: showDevTools = $devToolsEnabled && !$routeStore.queryParams?.peek
|
||||
|
@ -68,6 +69,8 @@
|
|||
// If the user is logged in but has no screens, they don't have
|
||||
// permission to use the app
|
||||
permissionError = true
|
||||
} else if ($appStore.embedded) {
|
||||
embedNoScreens = true
|
||||
} else {
|
||||
// If they have no screens and are not logged in, it probably means
|
||||
// they should log in to gain access
|
||||
|
@ -86,7 +89,10 @@
|
|||
if (get(builderStore).inBuilder) {
|
||||
builderStore.actions.notifyLoaded()
|
||||
} else {
|
||||
builderStore.actions.analyticsPing({ source: "app" })
|
||||
builderStore.actions.analyticsPing({
|
||||
source: "app",
|
||||
...{ embedded: $appStore.embedded || undefined },
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
@ -158,6 +164,15 @@
|
|||
</Body>
|
||||
</Layout>
|
||||
</div>
|
||||
{:else if embedNoScreens}
|
||||
<div class="error">
|
||||
<Layout justifyItems="center" gap="S">
|
||||
{@html ErrorSVG}
|
||||
<Heading size="L">
|
||||
Your application could not be loaded
|
||||
</Heading>
|
||||
</Layout>
|
||||
</div>
|
||||
{:else}
|
||||
<CustomThemeWrapper>
|
||||
{#key $screenStore.activeLayout._id}
|
||||
|
|
|
@ -34,6 +34,8 @@
|
|||
export let navWidth
|
||||
export let pageWidth
|
||||
|
||||
export let embedded = false
|
||||
|
||||
const NavigationClasses = {
|
||||
Top: "top",
|
||||
Left: "left",
|
||||
|
@ -186,9 +188,11 @@
|
|||
<Heading size="S">{title}</Heading>
|
||||
{/if}
|
||||
</div>
|
||||
{#if !embedded}
|
||||
<div class="portal">
|
||||
<Icon hoverable name="Apps" on:click={navigateToPortal} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<div
|
||||
class="mobile-click-handler"
|
||||
|
|
|
@ -45,6 +45,9 @@ const loadBudibase = async () => {
|
|||
// server rendered app HTML
|
||||
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
||||
|
||||
// Set the flag used to determine if the app is being loaded via an iframe
|
||||
appStore.actions.setAppEmbedded(window["##BUDIBASE_APP_EMBEDDED##"] == "true")
|
||||
|
||||
// Fetch environment info
|
||||
if (!get(environmentStore)?.loaded) {
|
||||
await environmentStore.actions.fetchEnvironment()
|
||||
|
|
|
@ -5,6 +5,7 @@ const initialState = {
|
|||
appId: null,
|
||||
isDevApp: false,
|
||||
clientLoadTime: window.INIT_TIME ? Date.now() - window.INIT_TIME : null,
|
||||
embedded: false,
|
||||
}
|
||||
|
||||
const createAppStore = () => {
|
||||
|
@ -46,9 +47,20 @@ const createAppStore = () => {
|
|||
})
|
||||
}
|
||||
|
||||
const setAppEmbedded = embeddded => {
|
||||
store.update(state => {
|
||||
if (state) {
|
||||
state.embedded = embeddded
|
||||
} else {
|
||||
state = { embeddded }
|
||||
}
|
||||
return state
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
subscribe: derivedStore.subscribe,
|
||||
actions: { setAppId, fetchAppDefinition },
|
||||
actions: { setAppId, setAppEmbedded, fetchAppDefinition },
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,9 +53,9 @@ const createBuilderStore = () => {
|
|||
notifyLoaded: () => {
|
||||
eventStore.actions.dispatchEvent("preview-loaded")
|
||||
},
|
||||
analyticsPing: async () => {
|
||||
analyticsPing: async ({ embedded }) => {
|
||||
try {
|
||||
await API.analyticsPing({ source: "app" })
|
||||
await API.analyticsPing({ source: "app", embedded })
|
||||
} catch (error) {
|
||||
// Do nothing
|
||||
}
|
||||
|
|
|
@ -33,7 +33,6 @@ const createScreenStore = () => {
|
|||
]) => {
|
||||
let activeLayout, activeScreen
|
||||
let screens
|
||||
|
||||
if ($builderStore.inBuilder) {
|
||||
// Use builder defined definitions if inside the builder preview
|
||||
activeScreen = Helpers.cloneDeep($builderStore.screen)
|
||||
|
@ -175,10 +174,10 @@ const createScreenStore = () => {
|
|||
},
|
||||
],
|
||||
...navigationSettings,
|
||||
embedded: $appStore.embedded,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return { screens, activeLayout, activeScreen }
|
||||
}
|
||||
)
|
||||
|
|
|
@ -7,11 +7,11 @@ export const buildAnalyticsEndpoints = API => ({
|
|||
url: "/api/bbtel",
|
||||
})
|
||||
},
|
||||
analyticsPing: async ({ source }) => {
|
||||
analyticsPing: async ({ source, embedded }) => {
|
||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||
return await API.post({
|
||||
url: "/api/bbtel/ping",
|
||||
body: { source, timezone },
|
||||
body: { source, timezone, embedded },
|
||||
})
|
||||
},
|
||||
})
|
||||
|
|
|
@ -12,6 +12,7 @@ export const isEnabled = async (ctx: any) => {
|
|||
|
||||
export const ping = async (ctx: any) => {
|
||||
const body = ctx.request.body as AnalyticsPingRequest
|
||||
|
||||
switch (body.source) {
|
||||
case PingSource.APP: {
|
||||
const db = context.getAppDB({ skip_setup: true })
|
||||
|
@ -21,7 +22,7 @@ export const ping = async (ctx: any) => {
|
|||
if (isDevAppID(appId)) {
|
||||
await events.serve.servedAppPreview(appInfo, body.timezone)
|
||||
} else {
|
||||
await events.serve.servedApp(appInfo, body.timezone)
|
||||
await events.serve.servedApp(appInfo, body.timezone, body.embedded)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
|
|
@ -100,6 +100,10 @@ export const deleteObjects = async function (ctx: any) {
|
|||
}
|
||||
|
||||
export const serveApp = async function (ctx: any) {
|
||||
const bbHeaderEmbed = ctx.request.get("x-budibase-embed")
|
||||
? ctx.get("x-budibase-embed").toLowerCase() == "true"
|
||||
: false
|
||||
|
||||
//Public Settings
|
||||
const { config } = await configs.getSettingsConfigDoc()
|
||||
const branding = await pro.branding.getBrandingConfig(config)
|
||||
|
@ -140,6 +144,7 @@ export const serveApp = async function (ctx: any) {
|
|||
body: html,
|
||||
style: css.code,
|
||||
appId,
|
||||
embedded: bbHeaderEmbed,
|
||||
})
|
||||
} else {
|
||||
// just return the app info for jest to assert on
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
|
@ -7,6 +6,7 @@
|
|||
|
||||
<script>
|
||||
window["##BUDIBASE_APP_ID##"] = "{{appId}}"
|
||||
window["##BUDIBASE_APP_EMBEDDED##"] = "{{embedded}}"
|
||||
</script>
|
||||
|
||||
{{{body}}}
|
||||
|
|
|
@ -6,4 +6,5 @@ export enum PingSource {
|
|||
export interface AnalyticsPingRequest {
|
||||
source: PingSource
|
||||
timezone: string
|
||||
embedded?: boolean
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ export interface BuilderServedEvent extends BaseEvent {
|
|||
export interface AppServedEvent extends BaseEvent {
|
||||
appVersion: string
|
||||
timezone: string
|
||||
embed?: boolean
|
||||
}
|
||||
|
||||
export interface AppPreviewServedEvent extends BaseEvent {
|
||||
|
|
Loading…
Reference in New Issue