Initial Commit for app overview
This commit is contained in:
parent
66d4aca062
commit
1c5990b9d7
|
@ -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>
|
||||
import {
|
||||
Heading,
|
||||
Button,
|
||||
Icon,
|
||||
ActionMenu,
|
||||
MenuItem,
|
||||
StatusLight,
|
||||
} from "@budibase/bbui"
|
||||
import { Heading, Button, Icon, ActionMenu, MenuItem } from "@budibase/bbui"
|
||||
import AppLockModal from "../common/AppLockModal.svelte"
|
||||
import { processStringSync } from "@budibase/string-templates"
|
||||
|
||||
export let app
|
||||
export let exportApp
|
||||
export let viewApp
|
||||
export let editApp
|
||||
export let updateApp
|
||||
export let deleteApp
|
||||
export let previewApp
|
||||
export let unpublishApp
|
||||
export let appOverview
|
||||
export let releaseLock
|
||||
export let editIcon
|
||||
</script>
|
||||
|
@ -26,7 +19,7 @@
|
|||
<div class="app-icon" style="color: {app.icon?.color || ''}">
|
||||
<Icon size="XL" name={app.icon?.name || "Apps"} />
|
||||
</div>
|
||||
<div class="name" on:click={() => editApp(app)}>
|
||||
<div class="name" on:click={() => appOverview(app)}>
|
||||
<Heading size="XS">
|
||||
{app.name}
|
||||
</Heading>
|
||||
|
@ -43,19 +36,7 @@
|
|||
{/if}
|
||||
</div>
|
||||
<div class="desktop">
|
||||
<StatusLight
|
||||
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>
|
||||
<AppLockModal {app} />
|
||||
</div>
|
||||
<div class="desktop">
|
||||
<div class="app-status">
|
||||
|
@ -70,23 +51,15 @@
|
|||
</div>
|
||||
<div data-cy={`row_actions_${app.appId}`}>
|
||||
<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
|
||||
size="S"
|
||||
cta
|
||||
secondary
|
||||
quiet
|
||||
disabled={app.lockedOther}
|
||||
on:click={() => editApp(app)}
|
||||
>
|
||||
Edit
|
||||
>Edit
|
||||
</Button>
|
||||
<Button size="S" cta on:click={() => appOverview(app)}>View</Button>
|
||||
</div>
|
||||
<ActionMenu align="right" dataCy="app-row-actions-menu-popover">
|
||||
<span slot="control" class="app-row-actions-icon">
|
||||
|
@ -119,6 +92,7 @@
|
|||
}
|
||||
.app-status {
|
||||
display: grid;
|
||||
grid-gap: var(--spacing-s);
|
||||
grid-template-columns: 24px 100px;
|
||||
}
|
||||
.app-status span.disabled {
|
||||
|
|
|
@ -182,6 +182,10 @@
|
|||
window.open(`/${app.devId}`)
|
||||
}
|
||||
|
||||
const appOverview = app => {
|
||||
$goto(`../overview/${app.devId}`)
|
||||
}
|
||||
|
||||
const editApp = app => {
|
||||
if (app.lockedOther) {
|
||||
notifications.error(
|
||||
|
@ -404,6 +408,7 @@
|
|||
{deleteApp}
|
||||
{updateApp}
|
||||
{previewApp}
|
||||
{appOverview}
|
||||
/>
|
||||
{/each}
|
||||
</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,
|
||||
} = require("../utilities/redis")
|
||||
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 { app: appCache } = require("@budibase/backend-core/cache")
|
||||
|
||||
|
@ -51,6 +51,21 @@ async function updateAppUpdatedAt(ctx) {
|
|||
await doWithDB(appId, async db => {
|
||||
const metadata = await db.get(DocumentTypes.APP_METADATA)
|
||||
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)
|
||||
metadata._rev = response.rev
|
||||
await appCache.invalidateAppMetadata(appId, metadata)
|
||||
|
@ -67,7 +82,15 @@ module.exports = async (ctx, permType) => {
|
|||
}
|
||||
const isBuilderApi = permType === PermissionTypes.BUILDER
|
||||
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
|
||||
if (!isBuilderApi || !editingApp) {
|
||||
return
|
||||
|
|
|
@ -48,7 +48,9 @@ exports.updateLock = async (devAppId, user) => {
|
|||
...user,
|
||||
userId: globalId,
|
||||
_id: globalId,
|
||||
lockedAt: new Date().getTime(),
|
||||
}
|
||||
|
||||
await devAppClient.store(devAppId, inputUser, APP_DEV_LOCK_SECONDS)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue