mirror of https://github.com/Budibase/budibase.git
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