forked from tsai/budibase
12 changed files with 763 additions and 38 deletions
@ -0,0 +1,135 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Button, |
||||
|
ButtonGroup, |
||||
|
ModalContent, |
||||
|
Modal, |
||||
|
notifications, |
||||
|
} from "@budibase/bbui" |
||||
|
import { auth, apps } from "stores/portal" |
||||
|
import { processStringSync } from "@budibase/string-templates" |
||||
|
import { API } from "api" |
||||
|
|
||||
|
export let app |
||||
|
|
||||
|
let APP_DEV_LOCK_SECONDS = 600 |
||||
|
let appLockModal |
||||
|
|
||||
|
$: lockedBy = app?.lockedBy |
||||
|
$: lockedByYou = $auth.user.email === lockedBy?.email |
||||
|
$: lockIdentifer = `${ |
||||
|
Object.prototype.hasOwnProperty.call(lockedBy, "firstName") |
||||
|
? lockedBy?.firstName |
||||
|
: lockedBy?.email |
||||
|
}` |
||||
|
$: lockedByHeading = |
||||
|
lockedBy && lockedByYou ? "Locked by you" : `Locked by ${lockIdentifer}` |
||||
|
|
||||
|
$: lockExpiry = getExpiryDuration(app) |
||||
|
|
||||
|
const getExpiryDuration = app => { |
||||
|
if (!app.lockedBy) { |
||||
|
return -1 |
||||
|
} |
||||
|
let expiry = |
||||
|
new Date(app.lockedBy.lockedAt).getTime() + APP_DEV_LOCK_SECONDS * 1000 |
||||
|
return expiry - new Date().getTime() |
||||
|
} |
||||
|
|
||||
|
const releaseLock = async () => { |
||||
|
if (app) { |
||||
|
try { |
||||
|
await API.releaseAppLock(app.devId) |
||||
|
await apps.load() |
||||
|
notifications.success("Lock released successfully") |
||||
|
} catch (err) { |
||||
|
notifications.error("Error releasing lock") |
||||
|
} |
||||
|
} else { |
||||
|
notifications.error("No application is selected") |
||||
|
} |
||||
|
} |
||||
|
</script> |
||||
|
|
||||
|
<div class="lock-status"> |
||||
|
{#if lockedBy} |
||||
|
<Button |
||||
|
quiet |
||||
|
secondary |
||||
|
icon="LockClosed" |
||||
|
on:click={() => { |
||||
|
appLockModal.show() |
||||
|
}} |
||||
|
> |
||||
|
<span class="lock-status-text"> |
||||
|
{lockedByHeading} |
||||
|
</span> |
||||
|
</Button> |
||||
|
{/if} |
||||
|
</div> |
||||
|
|
||||
|
<Modal bind:this={appLockModal}> |
||||
|
<ModalContent |
||||
|
title={lockedByHeading} |
||||
|
dataCy={"app-lock-modal"} |
||||
|
showConfirmButton={false} |
||||
|
showCancelButton={false} |
||||
|
> |
||||
|
<p> |
||||
|
Apps are locked to prevent work from being lost from overlapping changes |
||||
|
between your team. |
||||
|
</p> |
||||
|
|
||||
|
{#if lockedByYou && lockExpiry > 0} |
||||
|
{processStringSync( |
||||
|
"This lock will expire in {{ duration time 'millisecond' }} from now", |
||||
|
{ |
||||
|
time: lockExpiry, |
||||
|
} |
||||
|
)} |
||||
|
{/if} |
||||
|
<div class="lock-modal-actions"> |
||||
|
<ButtonGroup> |
||||
|
<Button |
||||
|
secondary |
||||
|
quiet={lockedBy && lockedByYou} |
||||
|
on:click={() => { |
||||
|
appLockModal.hide() |
||||
|
}} |
||||
|
> |
||||
|
<span class="cancel" |
||||
|
>{lockedBy && !lockedByYou ? "Done" : "Cancel"}</span |
||||
|
> |
||||
|
</Button> |
||||
|
{#if lockedByYou && lockExpiry > 0} |
||||
|
<Button |
||||
|
secondary |
||||
|
on:click={() => { |
||||
|
releaseLock() |
||||
|
appLockModal.hide() |
||||
|
}} |
||||
|
> |
||||
|
<span class="unlock">Release Lock</span> |
||||
|
</Button> |
||||
|
{/if} |
||||
|
</ButtonGroup> |
||||
|
</div> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
|
||||
|
<style> |
||||
|
.lock-modal-actions { |
||||
|
display: flex; |
||||
|
justify-content: flex-end; |
||||
|
margin-top: var(--spacing-l); |
||||
|
gap: var(--spacing-xl); |
||||
|
} |
||||
|
.lock-status { |
||||
|
display: flex; |
||||
|
gap: var(--spacing-s); |
||||
|
} |
||||
|
/* .lock-status :global(.spectrum-Button-label) { |
||||
|
font-weight: 200; |
||||
|
font-family: var(--font-sans); |
||||
|
} */ |
||||
|
</style> |
||||
@ -0,0 +1,62 @@ |
|||||
|
<script> |
||||
|
import { Icon, Detail } from "@budibase/bbui" |
||||
|
|
||||
|
export let title = "" |
||||
|
export let actionIcon |
||||
|
export let action |
||||
|
|
||||
|
$: actionDefined = typeof action === "function" |
||||
|
</script> |
||||
|
|
||||
|
<div class="dash-card"> |
||||
|
<div |
||||
|
class={actionDefined ? "dash-card-header active" : "dash-card-header"} |
||||
|
on:click={() => { |
||||
|
if (actionDefined) { |
||||
|
action() |
||||
|
} |
||||
|
}} |
||||
|
> |
||||
|
<span class="dash-card-title"> |
||||
|
<Detail size="M">{title}</Detail> |
||||
|
</span> |
||||
|
<span class="dash-card-action"> |
||||
|
{#if actionDefined} |
||||
|
<Icon name={actionIcon ? actionIcon : "ChevronRight"} /> |
||||
|
{/if} |
||||
|
</span> |
||||
|
</div> |
||||
|
<div class="dash-card-body"> |
||||
|
<slot /> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.dash-card { |
||||
|
background: var(--spectrum-alias-background-color-primary); |
||||
|
border-radius: var(--border-radius-s); |
||||
|
border: 1px solid var(--spectrum-global-color-gray-300); |
||||
|
overflow: hidden; |
||||
|
min-height: 150px; |
||||
|
} |
||||
|
.dash-card-header { |
||||
|
padding: var(--spacing-l); |
||||
|
border-bottom: 1px solid var(--spectrum-global-color-gray-300); |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
.dash-card-body { |
||||
|
padding: var(--spacing-l); |
||||
|
} |
||||
|
.dash-card-title :global(.spectrum-Detail) { |
||||
|
color: var( |
||||
|
--spectrum-sidenav-heading-text-color, |
||||
|
var(--spectrum-global-color-gray-700) |
||||
|
); |
||||
|
display: inline-block; |
||||
|
} |
||||
|
.dash-card-header.active:hover { |
||||
|
background-color: var(--spectrum-global-color-gray-200); |
||||
|
cursor: pointer; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,254 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@roxi/routify" |
||||
|
import { |
||||
|
Layout, |
||||
|
Page, |
||||
|
Button, |
||||
|
ButtonGroup, |
||||
|
Heading, |
||||
|
Tab, |
||||
|
Tabs, |
||||
|
notifications, |
||||
|
Icon, |
||||
|
ProgressCircle, |
||||
|
} from "@budibase/bbui" |
||||
|
import OverviewTab from "../_components/OverviewTab.svelte" |
||||
|
import VersionTab from "../_components/VersionTab.svelte" |
||||
|
import SelfHostTab from "../_components/SelfHostTab.svelte" |
||||
|
import { API } from "api" |
||||
|
import { onMount } from "svelte" |
||||
|
import { store } from "builderStore" |
||||
|
import { roles, flags } from "stores/backend" |
||||
|
import { apps, auth } from "stores/portal" |
||||
|
import analytics, { Events, EventSource } from "analytics" |
||||
|
import { AppStatus } from "constants" |
||||
|
import AppLockModal from "../../../../../components/common/AppLockModal.svelte" |
||||
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils" |
||||
|
|
||||
|
export let application |
||||
|
|
||||
|
let promise = getPackage() |
||||
|
|
||||
|
// App |
||||
|
$: filteredApps = $apps.filter(app => app.devId === application) |
||||
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null |
||||
|
|
||||
|
// Locking |
||||
|
$: lockedBy = selectedApp?.lockedBy |
||||
|
$: lockedByYou = $auth.user.email === lockedBy?.email |
||||
|
$: lockIdentifer = `${ |
||||
|
Object.prototype.hasOwnProperty.call(lockedBy, "firstName") |
||||
|
? lockedBy?.firstName |
||||
|
: lockedBy?.email |
||||
|
}` |
||||
|
|
||||
|
// App deployments |
||||
|
$: deployments = [] |
||||
|
$: latestDeployments = deployments |
||||
|
.filter( |
||||
|
deployment => |
||||
|
deployment.status === "SUCCESS" && application === deployment.appId |
||||
|
) |
||||
|
.sort((a, b) => a.updatedAt > b.updatedAt) |
||||
|
|
||||
|
$: isPublished = |
||||
|
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0 |
||||
|
|
||||
|
$: appUrl = `${window.origin}/app${selectedApp?.url}` |
||||
|
$: tabs = [ |
||||
|
"Overview", |
||||
|
"Automation History", |
||||
|
"Backups", |
||||
|
"App Version", |
||||
|
"Self-host", |
||||
|
] |
||||
|
$: selectedTab = "Overview" |
||||
|
|
||||
|
const handleTabChange = tabKey => { |
||||
|
if (tabKey === selectedTab) { |
||||
|
return |
||||
|
} else if (tabKey && tabs.indexOf(tabKey) > -1) { |
||||
|
selectedTab = tabKey |
||||
|
} else { |
||||
|
notifications.error("Invalid tab key") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function getPackage() { |
||||
|
try { |
||||
|
const pkg = await API.fetchAppPackage(application) |
||||
|
await store.actions.initialise(pkg) |
||||
|
// await automationStore.actions.fetch() |
||||
|
await roles.fetch() |
||||
|
await flags.fetch() |
||||
|
return pkg |
||||
|
} catch (error) { |
||||
|
notifications.error(`Error initialising app: ${error?.message}`) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
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) { |
||||
|
notifications.error("Error fetching deployment history") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//Show prod: published, appUrl |
||||
|
//Behaviour incorrect. It should be enabled if at least 1 live deployment is available |
||||
|
const viewApp = () => { |
||||
|
if (isPublished) { |
||||
|
analytics.captureEvent(Events.APP.VIEW_PUBLISHED, { |
||||
|
appId: $store.appId, |
||||
|
eventSource: EventSource.PORTAL, |
||||
|
}) |
||||
|
window.open(appUrl, "_blank") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const editApp = app => { |
||||
|
if (lockedBy && !lockedByYou) { |
||||
|
notifications.warn( |
||||
|
`App locked by ${lockIdentifer}. Please allow lock to expire or have them unlock this app.` |
||||
|
) |
||||
|
return |
||||
|
} |
||||
|
$goto(`../../../app/${app.devId}`) |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
// if (!hasSynced && application) { |
||||
|
// try { |
||||
|
// await API.syncApp(application) |
||||
|
// } catch (error) { |
||||
|
// notifications.error("Failed to sync with production database") |
||||
|
// } |
||||
|
// hasSynced = true |
||||
|
// } |
||||
|
deployments = await fetchDeployments() |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<Page wide> |
||||
|
<Layout noPadding gap="XL"> |
||||
|
<span> |
||||
|
<Button |
||||
|
quiet |
||||
|
secondary |
||||
|
icon={"ChevronLeft"} |
||||
|
on:click={() => { |
||||
|
$goto("../../") |
||||
|
}} |
||||
|
> |
||||
|
Back |
||||
|
</Button> |
||||
|
</span> |
||||
|
{#await promise} |
||||
|
<div class="loading"> |
||||
|
<ProgressCircle size="XL" /> |
||||
|
</div> |
||||
|
{:then _} |
||||
|
<div class="overview-header"> |
||||
|
<div class="app-title"> |
||||
|
<div class="app-logo"> |
||||
|
<div |
||||
|
class="app-icon" |
||||
|
style="color: {selectedApp?.icon?.color || ''}" |
||||
|
> |
||||
|
<Icon size="XL" name={selectedApp?.icon?.name || "Apps"} /> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="app-details"> |
||||
|
<Heading size="M">{selectedApp?.name}</Heading> |
||||
|
<div class="app-url">{appUrl}</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="header-right"> |
||||
|
<AppLockModal app={selectedApp} /> |
||||
|
<ButtonGroup gap="XS"> |
||||
|
<Button |
||||
|
size="M" |
||||
|
secondary |
||||
|
icon="Globe" |
||||
|
disabled={!isPublished} |
||||
|
on:click={viewApp}>View app</Button |
||||
|
> |
||||
|
<Button size="M" cta icon="Edit" on:click={editApp(selectedApp)}> |
||||
|
<span>Edit</span> |
||||
|
</Button> |
||||
|
</ButtonGroup> |
||||
|
</div> |
||||
|
</div> |
||||
|
<Tabs |
||||
|
selected={selectedTab} |
||||
|
noPadding |
||||
|
on:select={e => { |
||||
|
selectedTab = e.detail |
||||
|
}} |
||||
|
> |
||||
|
<Tab title="Overview"> |
||||
|
<OverviewTab |
||||
|
app={selectedApp} |
||||
|
deployments={latestDeployments} |
||||
|
navigateTab={handleTabChange} |
||||
|
/> |
||||
|
</Tab> |
||||
|
<Tab title="Automation History"> |
||||
|
<div class="container">Automation History contents</div> |
||||
|
</Tab> |
||||
|
<Tab title="Backups"> |
||||
|
<div class="container">Backups contents</div> |
||||
|
</Tab> |
||||
|
<Tab title="App Version"> |
||||
|
<VersionTab app={selectedApp} /> |
||||
|
</Tab> |
||||
|
<Tab title="Self-host"> |
||||
|
<SelfHostTab /> |
||||
|
</Tab> |
||||
|
</Tabs> |
||||
|
{:catch error} |
||||
|
<p>Something went wrong: {error.message}</p> |
||||
|
{/await} |
||||
|
</Layout> |
||||
|
</Page> |
||||
|
|
||||
|
<style> |
||||
|
.app-url { |
||||
|
color: var(--spectrum-global-color-gray-600); |
||||
|
} |
||||
|
.loading { |
||||
|
display: flex; |
||||
|
justify-content: center; |
||||
|
} |
||||
|
.overview-header { |
||||
|
display: flex; |
||||
|
justify-content: space-between; |
||||
|
} |
||||
|
.app-title { |
||||
|
display: flex; |
||||
|
gap: var(--spacing-m); |
||||
|
} |
||||
|
.header-right { |
||||
|
display: flex; |
||||
|
align-items: center; |
||||
|
gap: var(--spacing-xl); |
||||
|
} |
||||
|
.app-details :global(.spectrum-Heading) { |
||||
|
line-height: 1em; |
||||
|
margin-bottom: var(--spacing-s); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,11 @@ |
|||||
|
<script> |
||||
|
//export let app |
||||
|
</script> |
||||
|
|
||||
|
<div class="automation-tab" /> |
||||
|
|
||||
|
<style> |
||||
|
.automation-tab { |
||||
|
color: pink; |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,174 @@ |
|||||
|
<script> |
||||
|
import DashCard from "../../../../../components/common/DashCard.svelte" |
||||
|
import { AppStatus } from "constants" |
||||
|
import { Icon, Heading, Link, Avatar } from "@budibase/bbui" |
||||
|
import { store } from "builderStore" |
||||
|
import clientPackage from "@budibase/client/package.json" |
||||
|
import { processStringSync } from "@budibase/string-templates" |
||||
|
|
||||
|
export let app |
||||
|
export let deployments |
||||
|
export let navigateTab |
||||
|
|
||||
|
$: updateAvailable = clientPackage.version !== $store.version |
||||
|
$: isPublished = app.status === AppStatus.DEPLOYED |
||||
|
</script> |
||||
|
|
||||
|
<div class="overview-tab"> |
||||
|
<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> |
||||
|
|
||||
|
<p class="status-text"> |
||||
|
{#if deployments?.length} |
||||
|
{processStringSync( |
||||
|
"Last published {{ duration time 'millisecond' }} ago", |
||||
|
{ |
||||
|
time: |
||||
|
new Date().getTime() - |
||||
|
new Date(deployments[0].updatedAt).getTime(), |
||||
|
} |
||||
|
)} |
||||
|
{/if} |
||||
|
{#if !deployments?.length} |
||||
|
- |
||||
|
{/if} |
||||
|
</p> |
||||
|
</div> |
||||
|
</DashCard> |
||||
|
<DashCard title={"Last Edited"}> |
||||
|
{app.updatedAt} |
||||
|
<div class="last-edited-content"> |
||||
|
<!-- Where is this information sourced? auditLog > Placeholder, metadata --> |
||||
|
<div class="updated-by"> |
||||
|
<!-- Add a link to the user? new window? --> |
||||
|
<Avatar size="M" initials={app.updatedBy.initials} /> |
||||
|
<div>{app.updatedBy.firstName}</div> |
||||
|
</div> |
||||
|
<p class="last-edit-text"> |
||||
|
{#if app} |
||||
|
{processStringSync( |
||||
|
"Last edited {{ duration time 'millisecond' }} ago", |
||||
|
{ |
||||
|
time: new Date().getTime() - new Date(app?.updatedAt).getTime(), |
||||
|
} |
||||
|
)} |
||||
|
{/if} |
||||
|
</p> |
||||
|
</div> |
||||
|
</DashCard> |
||||
|
<DashCard |
||||
|
title={"App Version"} |
||||
|
showIcon={true} |
||||
|
action={() => { |
||||
|
navigateTab("App Version") |
||||
|
}} |
||||
|
> |
||||
|
<div class="version-content"> |
||||
|
<Heading size="XS">{app?.version}</Heading> |
||||
|
{#if updateAvailable} |
||||
|
<p> |
||||
|
New version <strong>{clientPackage.version}</strong> is available - |
||||
|
<Link |
||||
|
on:click={() => { |
||||
|
if (typeof navigateTab === "function") { |
||||
|
navigateTab("App Version") |
||||
|
} |
||||
|
}} |
||||
|
> |
||||
|
Update |
||||
|
</Link> |
||||
|
</p> |
||||
|
{:else} |
||||
|
<p>You're running the latest!</p> |
||||
|
{/if} |
||||
|
</div> |
||||
|
</DashCard> |
||||
|
</div> |
||||
|
<div class="bottom"> |
||||
|
<DashCard |
||||
|
title={"Automation History"} |
||||
|
action={() => { |
||||
|
navigateTab("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"}> |
||||
|
<div class="backups-content">test</div> |
||||
|
</DashCard> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
/* Add in size checks */ |
||||
|
.overview-tab { |
||||
|
display: grid; |
||||
|
grid-gap: var(--spacing-xl); |
||||
|
} |
||||
|
.overview-tab .top { |
||||
|
display: grid; |
||||
|
grid-gap: var(--spacing-xl); |
||||
|
grid-template-columns: repeat(auto-fill, minmax(30%, 1fr)); |
||||
|
} |
||||
|
.overview-tab .bottom, |
||||
|
.automation-metrics { |
||||
|
display: grid; |
||||
|
grid-gap: var(--spacing-xl); |
||||
|
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); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,37 @@ |
|||||
|
<script> |
||||
|
import { Layout, Divider, Heading, Body, Page, Button } from "@budibase/bbui" |
||||
|
|
||||
|
const selfHostPath = |
||||
|
"https://docs.budibase.com/docs/hosting-methods#self-host-budibase" |
||||
|
</script> |
||||
|
|
||||
|
<div class="self-host-tab"> |
||||
|
<Page wide={false}> |
||||
|
<Layout noPadding> |
||||
|
<Layout gap="XS" noPadding> |
||||
|
<Heading>Self-host Budibase</Heading> |
||||
|
<Divider /> |
||||
|
<Body> |
||||
|
<p> |
||||
|
Self-host Budibase for free to get unlimited apps and more - and it |
||||
|
only takes a few minutes! |
||||
|
</p> |
||||
|
<div class="page-action"> |
||||
|
<Button |
||||
|
cta |
||||
|
on:click={() => { |
||||
|
window.open(selfHostPath, "_blank") |
||||
|
}}>Self-host Budibase</Button |
||||
|
> |
||||
|
</div> |
||||
|
</Body> |
||||
|
</Layout> |
||||
|
</Layout> |
||||
|
</Page> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.page-action { |
||||
|
padding-top: var(--spacing-xl); |
||||
|
} |
||||
|
</style> |
||||
@ -0,0 +1,48 @@ |
|||||
|
<script> |
||||
|
import { Layout, Divider, Heading, Body, Page, Button } from "@budibase/bbui" |
||||
|
import { store } from "builderStore" |
||||
|
import clientPackage from "@budibase/client/package.json" |
||||
|
|
||||
|
export let app |
||||
|
//Review locking behaviour. |
||||
|
//The app just needs to be unlocked/lockedByYou to proceed? |
||||
|
|
||||
|
$: updateAvailable = clientPackage.version !== $store.version |
||||
|
</script> |
||||
|
|
||||
|
<div class="version-tab"> |
||||
|
<Page wide={false}> |
||||
|
<Layout noPadding> |
||||
|
<Layout gap="XS" noPadding> |
||||
|
<Heading>App Version</Heading> |
||||
|
<Divider /> |
||||
|
<Body> |
||||
|
{#if updateAvailable} |
||||
|
<p> |
||||
|
The app is currently using version <strong>{app?.version}</strong> |
||||
|
but version <strong>{clientPackage.version}</strong> is available. |
||||
|
</p> |
||||
|
{:else} |
||||
|
<p> |
||||
|
The app is currently using version <strong>{app?.version}</strong |
||||
|
>. You're running the latest! |
||||
|
</p> |
||||
|
{/if} |
||||
|
<p> |
||||
|
Updates can contain new features, performance improvements and bug |
||||
|
fixes. |
||||
|
</p> |
||||
|
<div class="page-action"> |
||||
|
<Button cta on:click={() => {}}>Update App</Button> |
||||
|
</div> |
||||
|
</Body> |
||||
|
</Layout> |
||||
|
</Layout> |
||||
|
</Page> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.page-action { |
||||
|
padding-top: var(--spacing-xl); |
||||
|
} |
||||
|
</style> |
||||
Loading…
Reference in new issue