1
0
Fork 0
mirror of synced 2024-05-17 10:53:15 +12:00
budibase/packages/builder/src/pages/builder/portal/apps/index.svelte
2023-07-05 18:00:50 +01:00

393 lines
11 KiB
Svelte

<script>
import {
Heading,
Layout,
Button,
Select,
Modal,
Page,
notifications,
Notification,
Body,
Search,
} from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import AppLimitModal from "components/portal/licensing/AppLimitModal.svelte"
import AccountLockedModal from "components/portal/licensing/AccountLockedModal.svelte"
import { store, automationStore } from "builderStore"
import { API } from "api"
import { onMount } from "svelte"
import { apps, auth, admin, licensing, environment } from "stores/portal"
import { goto } from "@roxi/routify"
import AppRow from "components/start/AppRow.svelte"
import { AppStatus } from "constants"
import Logo from "assets/bb-space-man.svg"
let sortBy = "name"
let template
let creationModal
let appLimitModal
let accountLockedModal
let searchTerm = ""
let creatingFromTemplate = false
let automationErrors
let accessFilterList = null
$: welcomeHeader = `Welcome ${$auth?.user?.firstName || "back"}`
$: enrichedApps = enrichApps($apps, $auth.user, sortBy)
$: filteredApps = enrichedApps.filter(
app =>
(searchTerm
? app?.name?.toLowerCase().includes(searchTerm.toLowerCase())
: true) &&
(accessFilterList !== null
? accessFilterList?.includes(
`${app?.type}_${app?.tenantId}_${app?.appId}`
)
: true)
)
$: automationErrors = getAutomationErrors(enrichedApps)
$: isOwner = $auth.accountPortalAccess && $admin.cloud
const usersLimitLockAction = $licensing?.errUserLimit
? () => accountLockedModal.show()
: null
const enrichApps = (apps, user, sortBy) => {
const enrichedApps = apps.map(app => ({
...app,
deployed: app.status === AppStatus.DEPLOYED,
lockedYou: app.lockedBy && app.lockedBy.email === user?.email,
lockedOther: app.lockedBy && app.lockedBy.email !== user?.email,
}))
if (sortBy === "status") {
return enrichedApps.sort((a, b) => {
if (a.status === b.status) {
return a.name?.toLowerCase() < b.name?.toLowerCase() ? -1 : 1
}
return a.status === AppStatus.DEPLOYED ? -1 : 1
})
} else if (sortBy === "updated") {
return enrichedApps.sort((a, b) => {
const aUpdated = a.updatedAt || "9999"
const bUpdated = b.updatedAt || "9999"
return aUpdated < bUpdated ? 1 : -1
})
} else {
return enrichedApps.sort((a, b) => {
return a.name?.toLowerCase() < b.name?.toLowerCase() ? -1 : 1
})
}
}
const getAutomationErrors = apps => {
const automationErrors = {}
for (let app of apps) {
if (app.automationErrors) {
if (errorCount(app.automationErrors) > 0) {
automationErrors[app.devId] = app.automationErrors
}
}
}
return automationErrors
}
const goToAutomationError = appId => {
const params = new URLSearchParams({
open: "error",
})
$goto(
`/builder/app/${appId}/settings/automation-history?${params.toString()}`
)
}
const errorCount = errors => {
return Object.values(errors).reduce((acc, next) => acc + next.length, 0)
}
const automationErrorMessage = appId => {
const app = enrichedApps.find(app => app.devId === appId)
const errors = automationErrors[appId]
return `${app.name} - Automation error (${errorCount(errors)})`
}
const initiateAppCreation = async () => {
if ($licensing?.usageMetrics?.apps >= 100) {
appLimitModal.show()
} else if ($apps?.length) {
$goto("/builder/portal/apps/create")
} else {
template = null
creationModal.show()
}
}
const initiateAppImport = () => {
template = { fromFile: true }
creationModal.show()
}
const autoCreateApp = async () => {
try {
// Auto name app if has same name
const templateKey = template.key.split("/")[1]
let appName = templateKey.replace(/-/g, " ")
const appsWithSameName = $apps.filter(app =>
app.name?.startsWith(appName)
)
appName = `${appName} ${appsWithSameName.length + 1}`
// Create form data to create app
let data = new FormData()
data.append("name", appName)
data.append("useTemplate", true)
data.append("templateKey", template.key)
// Create App
const createdApp = await API.createApp(data)
// Select Correct Application/DB in prep for creating user
const pkg = await API.fetchAppPackage(createdApp.instance._id)
await store.actions.initialise(pkg)
await automationStore.actions.fetch()
// Update checklist - in case first app
await admin.init()
// Create user
await API.updateOwnMetadata({
roleId: "BASIC",
})
await auth.setInitInfo({})
$goto(`/builder/app/${createdApp.instance._id}`)
} catch (error) {
notifications.error("Error creating app")
}
}
const stopAppCreation = () => {
template = null
}
function createAppFromTemplateUrl(templateKey) {
// validate the template key just to make sure
const templateParts = templateKey.split("/")
if (templateParts.length === 2 && templateParts[0] === "app") {
template = {
key: templateKey,
}
autoCreateApp()
} else {
notifications.error("Your Template URL is invalid. Please try another.")
}
}
onMount(async () => {
try {
await environment.loadVariables()
// If the portal is loaded from an external URL with a template param
const initInfo = await auth.getInitInfo()
if (initInfo?.init_template) {
creatingFromTemplate = true
createAppFromTemplateUrl(initInfo.init_template)
}
if (usersLimitLockAction) {
usersLimitLockAction()
}
} catch (error) {
notifications.error("Error getting init info")
}
})
</script>
{#if $apps.length}
<Page>
<Layout noPadding gap="L">
{#each Object.keys(automationErrors || {}) as appId}
<Notification
wide
dismissable
action={() => goToAutomationError(appId)}
type="error"
icon="Alert"
actionMessage={errorCount(automationErrors[appId]) > 1
? "View errors"
: "View error"}
on:dismiss={async () => {
await automationStore.actions.clearLogErrors({ appId })
await apps.load()
}}
message={automationErrorMessage(appId)}
/>
{/each}
<div class="title">
<div class="welcome">
<Layout noPadding gap="XS">
<Heading size="L">{welcomeHeader}</Heading>
<Body size="M">
Manage your apps and get a head start with templates
</Body>
</Layout>
</div>
</div>
{#if enrichedApps.length}
<Layout noPadding gap="L">
<div class="title">
<div class="buttons">
<Button
size="M"
cta
on:click={usersLimitLockAction || initiateAppCreation}
>
Create new app
</Button>
{#if $apps?.length > 0 && !$admin.offlineMode}
<Button
size="M"
secondary
on:click={usersLimitLockAction ||
$goto("/builder/portal/apps/templates")}
>
View templates
</Button>
{/if}
{#if !$apps?.length}
<Button
size="L"
quiet
secondary
on:click={usersLimitLockAction || initiateAppImport}
>
Import app
</Button>
{/if}
</div>
{#if enrichedApps.length > 1}
<div class="app-actions">
<Select
autoWidth
bind:value={sortBy}
placeholder={null}
options={[
{ label: "Sort by name", value: "name" },
{ label: "Sort by recently updated", value: "updated" },
{ label: "Sort by status", value: "status" },
]}
/>
<Search placeholder="Search" bind:value={searchTerm} />
</div>
{/if}
</div>
<div class="app-table">
{#each filteredApps as app (app.appId)}
<AppRow {app} lockedAction={usersLimitLockAction} />
{/each}
</div>
</Layout>
{/if}
{#if creatingFromTemplate}
<div class="empty-wrapper">
<img class="img-logo img-size" alt="logo" src={Logo} />
<p>Creating your Budibase app from your selected template...</p>
<Spinner size="10" />
</div>
{/if}
</Layout>
</Page>
{/if}
<Modal
bind:this={creationModal}
padding={false}
width="600px"
on:hide={stopAppCreation}
>
<CreateAppModal {template} />
</Modal>
<AppLimitModal bind:this={appLimitModal} />
<AccountLockedModal
bind:this={accountLockedModal}
onConfirm={() =>
isOwner ? $licensing.goToUpgradePage() : $licensing.goToPricingPage()}
/>
<style>
.title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
.buttons {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
.app-actions {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: var(--spacing-xl);
flex-wrap: wrap;
}
.app-actions :global(.spectrum-Textfield) {
max-width: 180px;
}
.app-table {
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
gap: var(--spacing-xl);
}
.empty-wrapper {
flex: 1 1 auto;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
.img-size {
width: 160px;
height: 160px;
}
@media (max-width: 1000px) {
.img-logo {
display: none;
}
}
@media (max-width: 640px) {
.app-actions {
margin-top: var(--spacing-xl);
margin-bottom: calc(-1 * var(--spacing-m));
}
.app-actions :global(.spectrum-Textfield) {
max-width: none;
}
/* Hide download apps icon */
.app-actions :global(> .spectrum-Icon) {
display: none;
}
.app-actions > :global(*) {
flex: 1 1 auto;
}
}
</style>