428 lines
12 KiB
Svelte
428 lines
12 KiB
Svelte
<script>
|
|
import { goto } from "@roxi/routify"
|
|
import {
|
|
Layout,
|
|
Page,
|
|
Button,
|
|
ActionButton,
|
|
ButtonGroup,
|
|
Heading,
|
|
Tab,
|
|
Tabs,
|
|
notifications,
|
|
ProgressCircle,
|
|
Input,
|
|
ActionMenu,
|
|
MenuItem,
|
|
Icon,
|
|
Helpers,
|
|
Modal,
|
|
} from "@budibase/bbui"
|
|
import OverviewTab from "../_components/OverviewTab.svelte"
|
|
import SettingsTab from "../_components/SettingsTab.svelte"
|
|
import AccessTab from "../_components/AccessTab.svelte"
|
|
import { API } from "api"
|
|
import { store } from "builderStore"
|
|
import { apps, auth } from "stores/portal"
|
|
import analytics, { Events, EventSource } from "analytics"
|
|
import { AppStatus } from "constants"
|
|
import AppLockModal from "components/common/AppLockModal.svelte"
|
|
import EditableIcon from "components/common/EditableIcon.svelte"
|
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
import HistoryTab from "components/portal/overview/automation/HistoryTab.svelte"
|
|
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
|
import { onDestroy, onMount } from "svelte"
|
|
|
|
export let application
|
|
|
|
let promise = getPackage()
|
|
let loaded = false
|
|
let deletionModal
|
|
let unpublishModal
|
|
let exportModal
|
|
let appName = ""
|
|
let published
|
|
|
|
// 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 = `${
|
|
lockedBy && 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", "Settings", "Access"]
|
|
$: selectedTab = "Overview"
|
|
|
|
const backToAppList = () => {
|
|
$goto(`../../../portal/`)
|
|
}
|
|
|
|
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)
|
|
loaded = true
|
|
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")
|
|
}
|
|
}
|
|
|
|
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.warning(
|
|
`App locked by ${lockIdentifer}. Please allow lock to expire or have them unlock this app.`
|
|
)
|
|
return
|
|
}
|
|
$goto(`../../../app/${app.devId}`)
|
|
}
|
|
|
|
const copyAppId = async app => {
|
|
await Helpers.copyToClipboard(app.prodId)
|
|
notifications.success("App ID copied to clipboard.")
|
|
}
|
|
|
|
const exportApp = opts => {
|
|
published = opts.published
|
|
exportModal.show()
|
|
}
|
|
|
|
const unpublishApp = app => {
|
|
selectedApp = app
|
|
unpublishModal.show()
|
|
}
|
|
|
|
const confirmUnpublishApp = async () => {
|
|
if (!selectedApp) {
|
|
return
|
|
}
|
|
try {
|
|
await API.unpublishApp(selectedApp.prodId)
|
|
await apps.load()
|
|
notifications.success("App unpublished successfully")
|
|
} catch (err) {
|
|
notifications.error("Error unpublishing app")
|
|
}
|
|
}
|
|
|
|
const deleteApp = app => {
|
|
selectedApp = app
|
|
deletionModal.show()
|
|
}
|
|
|
|
const confirmDeleteApp = async () => {
|
|
if (!selectedApp) {
|
|
return
|
|
}
|
|
try {
|
|
await API.deleteApp(selectedApp?.devId)
|
|
backToAppList()
|
|
notifications.success("App deleted successfully")
|
|
} catch (err) {
|
|
notifications.error("Error deleting app")
|
|
}
|
|
selectedApp = null
|
|
appName = null
|
|
}
|
|
|
|
onDestroy(() => {
|
|
store.actions.reset()
|
|
})
|
|
|
|
onMount(async () => {
|
|
const params = new URLSearchParams(window.location.search)
|
|
if (params.get("tab")) {
|
|
selectedTab = params.get("tab")
|
|
}
|
|
try {
|
|
if (!apps.length) {
|
|
await apps.load()
|
|
}
|
|
await API.syncApp(application)
|
|
deployments = await fetchDeployments()
|
|
} catch (error) {
|
|
notifications.error("Error initialising app overview")
|
|
}
|
|
})
|
|
</script>
|
|
|
|
<Modal bind:this={exportModal} padding={false} width="600px">
|
|
<ExportAppModal app={selectedApp} {published} />
|
|
</Modal>
|
|
|
|
<span class="overview-wrap">
|
|
<Page wide noPadding>
|
|
{#await promise}
|
|
<div class="loading">
|
|
<ProgressCircle size="XL" />
|
|
</div>
|
|
{:then _}
|
|
<Layout paddingX="XXL" paddingY="XL" gap="L">
|
|
<span class="page-header" class:loaded>
|
|
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
|
|
Back
|
|
</ActionButton>
|
|
</span>
|
|
<div class="overview-header">
|
|
<div class="app-title">
|
|
<div class="app-logo">
|
|
<div
|
|
class="app-icon"
|
|
style="color: {selectedApp?.icon?.color || ''}"
|
|
>
|
|
<EditableIcon
|
|
app={selectedApp}
|
|
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"
|
|
quiet
|
|
secondary
|
|
icon="Globe"
|
|
disabled={!isPublished}
|
|
on:click={viewApp}
|
|
dataCy="view-app"
|
|
>
|
|
View app
|
|
</Button>
|
|
<Button
|
|
size="M"
|
|
cta
|
|
icon="Edit"
|
|
disabled={lockedBy && !lockedByYou}
|
|
on:click={() => {
|
|
editApp(selectedApp)
|
|
}}
|
|
>
|
|
<span>Edit</span>
|
|
</Button>
|
|
</ButtonGroup>
|
|
<ActionMenu align="right" dataCy="app-overview-menu-popover">
|
|
<span slot="control" class="app-overview-actions-icon">
|
|
<Icon hoverable name="More" />
|
|
</span>
|
|
<MenuItem
|
|
on:click={() => exportApp({ published: false })}
|
|
icon="DownloadFromCloud"
|
|
>
|
|
Export latest
|
|
</MenuItem>
|
|
{#if isPublished}
|
|
<MenuItem
|
|
on:click={() => exportApp({ published: true })}
|
|
icon="DownloadFromCloudOutline"
|
|
>
|
|
Export published
|
|
</MenuItem>
|
|
<MenuItem on:click={() => copyAppId(selectedApp)} icon="Copy">
|
|
Copy app ID
|
|
</MenuItem>
|
|
{/if}
|
|
{#if !isPublished}
|
|
<MenuItem on:click={() => deleteApp(selectedApp)} icon="Delete">
|
|
Delete
|
|
</MenuItem>
|
|
{/if}
|
|
</ActionMenu>
|
|
</div>
|
|
</div>
|
|
</Layout>
|
|
<div class="tab-wrap">
|
|
<Tabs
|
|
selected={selectedTab}
|
|
noPadding
|
|
on:select={e => {
|
|
selectedTab = e.detail
|
|
}}
|
|
>
|
|
<Tab title="Overview">
|
|
<OverviewTab
|
|
app={selectedApp}
|
|
deployments={latestDeployments}
|
|
navigateTab={handleTabChange}
|
|
on:unpublish={e => unpublishApp(e.detail)}
|
|
/>
|
|
</Tab>
|
|
<Tab title="Access">
|
|
<AccessTab app={selectedApp} />
|
|
</Tab>
|
|
{#if isPublished}
|
|
<Tab title="Automation History">
|
|
<HistoryTab app={selectedApp} />
|
|
</Tab>
|
|
{/if}
|
|
{#if false}
|
|
<Tab title="Backups">
|
|
<div class="container">Backups contents</div>
|
|
</Tab>
|
|
{/if}
|
|
<Tab title="Settings">
|
|
<SettingsTab app={selectedApp} />
|
|
</Tab>
|
|
</Tabs>
|
|
</div>
|
|
<ConfirmDialog
|
|
bind:this={deletionModal}
|
|
title="Confirm deletion"
|
|
okText="Delete app"
|
|
onOk={confirmDeleteApp}
|
|
onCancel={() => (appName = null)}
|
|
disabled={appName !== selectedApp?.name}
|
|
>
|
|
Are you sure you want to delete the app <b>{selectedApp?.name}</b>?
|
|
|
|
<p>Please enter the app name below to confirm.</p>
|
|
<Input
|
|
bind:value={appName}
|
|
data-cy="delete-app-confirmation"
|
|
placeholder={selectedApp?.name}
|
|
/>
|
|
</ConfirmDialog>
|
|
<ConfirmDialog
|
|
bind:this={unpublishModal}
|
|
title="Confirm unpublish"
|
|
okText="Unpublish app"
|
|
onOk={confirmUnpublishApp}
|
|
dataCy={"unpublish-modal"}
|
|
>
|
|
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
|
|
</ConfirmDialog>
|
|
{:catch error}
|
|
<p>Something went wrong: {error.message}</p>
|
|
{/await}
|
|
</Page>
|
|
</span>
|
|
|
|
<style>
|
|
.app-url {
|
|
color: var(--spectrum-global-color-gray-600);
|
|
}
|
|
.loading {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex: 1;
|
|
}
|
|
.overview-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
}
|
|
|
|
.page-header.loaded {
|
|
padding: 0px;
|
|
}
|
|
|
|
.overview-wrap :global(> div > .container),
|
|
.tab-wrap :global(.spectrum-Tabs) {
|
|
background-color: var(--background);
|
|
background-clip: padding-box;
|
|
}
|
|
|
|
@media (max-width: 1000px) {
|
|
.overview-header {
|
|
flex-direction: column;
|
|
gap: var(--spacing-l);
|
|
}
|
|
}
|
|
@media (max-width: 640px) {
|
|
.overview-wrap :global(.content > *) {
|
|
padding: calc(var(--spacing-xl) * 1.5) !important;
|
|
}
|
|
}
|
|
.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);
|
|
}
|
|
.tab-wrap :global(> .spectrum-Tabs) {
|
|
padding-left: var(--spectrum-alias-grid-gutter-large);
|
|
padding-right: var(--spectrum-alias-grid-gutter-large);
|
|
}
|
|
.page-header {
|
|
padding-left: var(--spectrum-alias-grid-gutter-large);
|
|
padding-right: var(--spectrum-alias-grid-gutter-large);
|
|
padding-top: var(--spectrum-alias-grid-gutter-large);
|
|
}
|
|
</style>
|