|
|
|
@ -4,12 +4,18 @@ |
|
|
|
Layout, |
|
|
|
Page, |
|
|
|
Button, |
|
|
|
ActionButton, |
|
|
|
ButtonGroup, |
|
|
|
Heading, |
|
|
|
Tab, |
|
|
|
Tabs, |
|
|
|
notifications, |
|
|
|
ProgressCircle, |
|
|
|
Input, |
|
|
|
ActionMenu, |
|
|
|
MenuItem, |
|
|
|
Icon, |
|
|
|
Helpers, |
|
|
|
} from "@budibase/bbui" |
|
|
|
import OverviewTab from "../_components/OverviewTab.svelte" |
|
|
|
import SettingsTab from "../_components/SettingsTab.svelte" |
|
|
|
@ -20,12 +26,17 @@ |
|
|
|
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 { checkIncomingDeploymentStatus } from "components/deploy/utils" |
|
|
|
import { onDestroy, onMount } from "svelte" |
|
|
|
|
|
|
|
export let application |
|
|
|
|
|
|
|
let promise = getPackage() |
|
|
|
let loaded = false |
|
|
|
let deletionModal |
|
|
|
let unpublishModal |
|
|
|
let appName = "" |
|
|
|
|
|
|
|
// App |
|
|
|
$: filteredApps = $apps.filter(app => app.devId === application) |
|
|
|
@ -74,6 +85,7 @@ |
|
|
|
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}`) |
|
|
|
@ -121,6 +133,58 @@ |
|
|
|
$goto(`../../../app/${app.devId}`) |
|
|
|
} |
|
|
|
|
|
|
|
const copyAppId = async app => { |
|
|
|
await Helpers.copyToClipboard(app.prodId) |
|
|
|
notifications.success("App ID copied to clipboard.") |
|
|
|
} |
|
|
|
|
|
|
|
const exportApp = app => { |
|
|
|
const id = isPublished ? app.prodId : app.devId |
|
|
|
const appName = encodeURIComponent(app.name) |
|
|
|
window.location = `/api/backups/export?appId=${id}&appname=${appName}` |
|
|
|
} |
|
|
|
|
|
|
|
const unpublishApp = app => { |
|
|
|
selectedApp = app |
|
|
|
unpublishModal.show() |
|
|
|
} |
|
|
|
|
|
|
|
const confirmUnpublishApp = async () => { |
|
|
|
if (!selectedApp) { |
|
|
|
return |
|
|
|
} |
|
|
|
try { |
|
|
|
analytics.captureEvent(Events.APP.UNPUBLISHED, { |
|
|
|
appId: selectedApp.appId, |
|
|
|
}) |
|
|
|
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() |
|
|
|
}) |
|
|
|
@ -130,6 +194,7 @@ |
|
|
|
if (!apps.length) { |
|
|
|
await apps.load() |
|
|
|
} |
|
|
|
await API.syncApp(application) |
|
|
|
deployments = await fetchDeployments() |
|
|
|
} catch (error) { |
|
|
|
notifications.error("Error initialising app overview") |
|
|
|
@ -137,95 +202,155 @@ |
|
|
|
}) |
|
|
|
</script> |
|
|
|
|
|
|
|
<Page wide> |
|
|
|
<Layout noPadding gap="XL"> |
|
|
|
<span> |
|
|
|
<Button quiet secondary icon={"ChevronLeft"} on:click={backToAppList}> |
|
|
|
Back |
|
|
|
</Button> |
|
|
|
</span> |
|
|
|
<span class="overview-wrap"> |
|
|
|
<Page wide noPadding> |
|
|
|
{#await promise} |
|
|
|
<span class="page-header"> |
|
|
|
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}> |
|
|
|
Back |
|
|
|
</ActionButton> |
|
|
|
</span> |
|
|
|
<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 || ''}" |
|
|
|
> |
|
|
|
<EditableIcon |
|
|
|
app={selectedApp} |
|
|
|
size="XL" |
|
|
|
name={selectedApp?.icon?.name || "Apps"} |
|
|
|
/> |
|
|
|
<Layout paddingX="XXL" paddingY="XXL" gap="XL"> |
|
|
|
<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="app-details"> |
|
|
|
<Heading size="M">{selectedApp?.name}</Heading> |
|
|
|
<div class="app-url">{appUrl}</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(selectedApp)} icon="Download"> |
|
|
|
Export |
|
|
|
</MenuItem> |
|
|
|
{#if isPublished} |
|
|
|
<MenuItem |
|
|
|
on:click={() => unpublishApp(selectedApp)} |
|
|
|
icon="GlobeRemove" |
|
|
|
> |
|
|
|
Unpublish |
|
|
|
</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> |
|
|
|
<div class="header-right"> |
|
|
|
<AppLockModal app={selectedApp} /> |
|
|
|
<ButtonGroup gap="XS"> |
|
|
|
<Button |
|
|
|
size="M" |
|
|
|
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> |
|
|
|
</div> |
|
|
|
</div> |
|
|
|
<Tabs |
|
|
|
selected={selectedTab} |
|
|
|
noPadding |
|
|
|
on:select={e => { |
|
|
|
selectedTab = e.detail |
|
|
|
}} |
|
|
|
> |
|
|
|
<Tab title="Overview"> |
|
|
|
<OverviewTab |
|
|
|
app={selectedApp} |
|
|
|
deployments={latestDeployments} |
|
|
|
navigateTab={handleTabChange} |
|
|
|
/> |
|
|
|
</Tab> |
|
|
|
{#if false} |
|
|
|
<Tab title="Automation History"> |
|
|
|
<div class="container">Automation History contents</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} |
|
|
|
/> |
|
|
|
</Tab> |
|
|
|
<Tab title="Backups"> |
|
|
|
<div class="container">Backups contents</div> |
|
|
|
{#if false} |
|
|
|
<Tab title="Automation History"> |
|
|
|
<div class="container">Automation History contents</div> |
|
|
|
</Tab> |
|
|
|
<Tab title="Backups"> |
|
|
|
<div class="container">Backups contents</div> |
|
|
|
</Tab> |
|
|
|
{/if} |
|
|
|
<Tab title="Settings"> |
|
|
|
<SettingsTab app={selectedApp} /> |
|
|
|
</Tab> |
|
|
|
{/if} |
|
|
|
<Tab title="Settings"> |
|
|
|
<SettingsTab app={selectedApp} /> |
|
|
|
</Tab> |
|
|
|
</Tabs> |
|
|
|
</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} |
|
|
|
</Layout> |
|
|
|
</Page> |
|
|
|
</Page> |
|
|
|
</span> |
|
|
|
|
|
|
|
<style> |
|
|
|
.app-url { |
|
|
|
@ -234,11 +359,35 @@ |
|
|
|
.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); |
|
|
|
@ -252,4 +401,13 @@ |
|
|
|
line-height: 1em; |
|
|
|
margin-bottom: var(--spacing-s); |
|
|
|
} |
|
|
|
.tab-wrap :global(.spectrum-Tabs) { |
|
|
|
padding-left: calc(var(--spacing-xl) * 2); |
|
|
|
padding-right: calc(var(--spacing-xl) * 2); |
|
|
|
} |
|
|
|
.page-header { |
|
|
|
padding-left: calc(var(--spacing-xl) * 2); |
|
|
|
padding-right: calc(var(--spacing-xl) * 2); |
|
|
|
padding-top: calc(var(--spacing-xl) * 2); |
|
|
|
} |
|
|
|
</style> |
|
|
|
|