Initial Commit for app overview
This commit is contained in:
parent
eeda40f829
commit
5104631d20
|
@ -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>
|
|
@ -1,22 +1,15 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||||
Heading,
|
import AppLockModal from "../common/AppLockModal.svelte"
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
StatusLight,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
export let exportApp
|
export let exportApp
|
||||||
export let viewApp
|
|
||||||
export let editApp
|
export let editApp
|
||||||
export let updateApp
|
export let updateApp
|
||||||
export let deleteApp
|
export let deleteApp
|
||||||
export let previewApp
|
|
||||||
export let unpublishApp
|
export let unpublishApp
|
||||||
|
export let appOverview
|
||||||
export let releaseLock
|
export let releaseLock
|
||||||
export let editIcon
|
export let editIcon
|
||||||
</script>
|
</script>
|
||||||
|
@ -26,7 +19,7 @@
|
||||||
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
||||||
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
||||||
</div>
|
</div>
|
||||||
<div class="name" on:click={() => editApp(app)}>
|
<div class="name" on:click={() => appOverview(app)}>
|
||||||
<Heading size="XS">
|
<Heading size="XS">
|
||||||
{app.name}
|
{app.name}
|
||||||
</Heading>
|
</Heading>
|
||||||
|
@ -43,19 +36,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<StatusLight
|
<AppLockModal {app} />
|
||||||
positive={!app.lockedYou && !app.lockedOther}
|
|
||||||
notice={app.lockedYou}
|
|
||||||
negative={app.lockedOther}
|
|
||||||
>
|
|
||||||
{#if app.lockedYou}
|
|
||||||
Locked by you
|
|
||||||
{:else if app.lockedOther}
|
|
||||||
Locked by {app.lockedBy.email}
|
|
||||||
{:else}
|
|
||||||
Open
|
|
||||||
{/if}
|
|
||||||
</StatusLight>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="desktop">
|
<div class="desktop">
|
||||||
<div class="app-status">
|
<div class="app-status">
|
||||||
|
@ -70,23 +51,15 @@
|
||||||
</div>
|
</div>
|
||||||
<div data-cy={`row_actions_${app.appId}`}>
|
<div data-cy={`row_actions_${app.appId}`}>
|
||||||
<div class="app-row-actions">
|
<div class="app-row-actions">
|
||||||
{#if app.deployed}
|
|
||||||
<Button size="S" secondary quiet on:click={() => viewApp(app)}
|
|
||||||
>View app
|
|
||||||
</Button>
|
|
||||||
{:else}
|
|
||||||
<Button size="S" secondary quiet on:click={() => previewApp(app)}
|
|
||||||
>Preview
|
|
||||||
</Button>
|
|
||||||
{/if}
|
|
||||||
<Button
|
<Button
|
||||||
size="S"
|
size="S"
|
||||||
cta
|
secondary
|
||||||
|
quiet
|
||||||
disabled={app.lockedOther}
|
disabled={app.lockedOther}
|
||||||
on:click={() => editApp(app)}
|
on:click={() => editApp(app)}
|
||||||
>
|
>Edit
|
||||||
Edit
|
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="S" cta on:click={() => appOverview(app)}>View</Button>
|
||||||
</div>
|
</div>
|
||||||
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
||||||
<span slot="control" class="app-row-actions-icon">
|
<span slot="control" class="app-row-actions-icon">
|
||||||
|
@ -119,6 +92,7 @@
|
||||||
}
|
}
|
||||||
.app-status {
|
.app-status {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
grid-gap: var(--spacing-s);
|
||||||
grid-template-columns: 24px 100px;
|
grid-template-columns: 24px 100px;
|
||||||
}
|
}
|
||||||
.app-status span.disabled {
|
.app-status span.disabled {
|
||||||
|
|
|
@ -182,6 +182,10 @@
|
||||||
window.open(`/${app.devId}`)
|
window.open(`/${app.devId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const appOverview = app => {
|
||||||
|
$goto(`../overview/${app.devId}`)
|
||||||
|
}
|
||||||
|
|
||||||
const editApp = app => {
|
const editApp = app => {
|
||||||
if (app.lockedOther) {
|
if (app.lockedOther) {
|
||||||
notifications.error(
|
notifications.error(
|
||||||
|
@ -404,6 +408,7 @@
|
||||||
{deleteApp}
|
{deleteApp}
|
||||||
{updateApp}
|
{updateApp}
|
||||||
{previewApp}
|
{previewApp}
|
||||||
|
{appOverview}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -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>
|
|
@ -6,7 +6,7 @@ const {
|
||||||
setDebounce,
|
setDebounce,
|
||||||
} = require("../utilities/redis")
|
} = require("../utilities/redis")
|
||||||
const { doWithDB } = require("@budibase/backend-core/db")
|
const { doWithDB } = require("@budibase/backend-core/db")
|
||||||
const { DocumentTypes } = require("../db/utils")
|
const { DocumentTypes, getGlobalIDFromUserMetadataID } = require("../db/utils")
|
||||||
const { PermissionTypes } = require("@budibase/backend-core/permissions")
|
const { PermissionTypes } = require("@budibase/backend-core/permissions")
|
||||||
const { app: appCache } = require("@budibase/backend-core/cache")
|
const { app: appCache } = require("@budibase/backend-core/cache")
|
||||||
|
|
||||||
|
@ -51,6 +51,21 @@ async function updateAppUpdatedAt(ctx) {
|
||||||
await doWithDB(appId, async db => {
|
await doWithDB(appId, async db => {
|
||||||
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
||||||
metadata.updatedAt = new Date().toISOString()
|
metadata.updatedAt = new Date().toISOString()
|
||||||
|
|
||||||
|
const getInitials = user => {
|
||||||
|
let initials = ""
|
||||||
|
initials += user.firstName ? user.firstName[0] : ""
|
||||||
|
initials += user.lastName ? user.lastName[0] : ""
|
||||||
|
return initials == "" ? undefined : initials
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata.updatedBy = {
|
||||||
|
email: ctx.user.email,
|
||||||
|
firstName: ctx.user.firstName,
|
||||||
|
lastName: ctx.user.lastName,
|
||||||
|
initials: getInitials(ctx.user),
|
||||||
|
_id: getGlobalIDFromUserMetadataID(ctx.user.userId),
|
||||||
|
}
|
||||||
const response = await db.put(metadata)
|
const response = await db.put(metadata)
|
||||||
metadata._rev = response.rev
|
metadata._rev = response.rev
|
||||||
await appCache.invalidateAppMetadata(appId, metadata)
|
await appCache.invalidateAppMetadata(appId, metadata)
|
||||||
|
@ -67,7 +82,15 @@ module.exports = async (ctx, permType) => {
|
||||||
}
|
}
|
||||||
const isBuilderApi = permType === PermissionTypes.BUILDER
|
const isBuilderApi = permType === PermissionTypes.BUILDER
|
||||||
const referer = ctx.headers["referer"]
|
const referer = ctx.headers["referer"]
|
||||||
const editingApp = referer ? referer.includes(appId) : false
|
|
||||||
|
const overviewPath = "/builder/portal/overview/"
|
||||||
|
const overviewContext = !referer ? false : referer.includes(overviewPath)
|
||||||
|
if (overviewContext) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAppId = !referer ? false : referer.includes(appId)
|
||||||
|
const editingApp = referer ? hasAppId : false
|
||||||
// check this is a builder call and editing
|
// check this is a builder call and editing
|
||||||
if (!isBuilderApi || !editingApp) {
|
if (!isBuilderApi || !editingApp) {
|
||||||
return
|
return
|
||||||
|
|
|
@ -48,7 +48,9 @@ exports.updateLock = async (devAppId, user) => {
|
||||||
...user,
|
...user,
|
||||||
userId: globalId,
|
userId: globalId,
|
||||||
_id: globalId,
|
_id: globalId,
|
||||||
|
lockedAt: new Date().getTime(),
|
||||||
}
|
}
|
||||||
|
|
||||||
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS)
|
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue