Fix loading states and issues with overview tab

This commit is contained in:
Andrew Kingston 2022-08-08 12:08:37 +01:00
parent 3d42c9cfeb
commit 50c04c801f
2 changed files with 225 additions and 225 deletions

View File

@ -34,15 +34,19 @@
export let application export let application
let promise = getPackage()
let loaded = false let loaded = false
let deletionModal let deletionModal
let unpublishModal let unpublishModal
let appName = "" let appName = ""
let deployments = []
// App // App
$: filteredApps = $apps.filter(app => app.devId === application) $: filteredApps = $apps.filter(app => app.devId === application)
$: selectedApp = filteredApps?.length ? filteredApps[0] : null $: selectedApp = filteredApps?.length ? filteredApps[0] : null
$: loaded && !selectedApp && backToAppList()
$: isPublished =
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
$: appUrl = `${window.origin}/app${selectedApp?.url}`
// Locking // Locking
$: lockedBy = selectedApp?.lockedBy $: lockedBy = selectedApp?.lockedBy
@ -54,18 +58,11 @@
}` }`
// App deployments // App deployments
$: deployments = []
$: latestDeployments = deployments $: latestDeployments = deployments
.filter( .filter(x => x.status === "SUCCESS" && application === x.appId)
deployment =>
deployment.status === "SUCCESS" && application === deployment.appId
)
.sort((a, b) => a.updatedAt > b.updatedAt) .sort((a, b) => a.updatedAt > b.updatedAt)
$: isPublished = // Tabs
selectedApp?.status === AppStatus.DEPLOYED && latestDeployments?.length > 0
$: appUrl = `${window.origin}/app${selectedApp?.url}`
$: tabs = ["Overview", "Automation History", "Backups", "Settings", "Access"] $: tabs = ["Overview", "Automation History", "Backups", "Settings", "Access"]
$: selectedTab = "Overview" $: selectedTab = "Overview"
@ -83,17 +80,6 @@
} }
} }
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) => { const reviewPendingDeployments = (deployments, newDeployments) => {
if (deployments.length > 0) { if (deployments.length > 0) {
const pending = checkIncomingDeploymentStatus(deployments, newDeployments) const pending = checkIncomingDeploymentStatus(deployments, newDeployments)
@ -185,185 +171,190 @@
appName = null appName = null
} }
onDestroy(() => {
store.actions.reset()
})
onMount(async () => { onMount(async () => {
const params = new URLSearchParams(window.location.search) const params = new URLSearchParams(window.location.search)
if (params.get("tab")) { if (params.get("tab")) {
selectedTab = params.get("tab") selectedTab = params.get("tab")
} }
// Check app exists
try { try {
const pkg = await API.fetchAppPackage(application)
await store.actions.initialise(pkg)
} catch (error) {
// Swallow
backToAppList()
}
// Initialise application
try {
await API.syncApp(application)
deployments = await fetchDeployments()
if (!apps.length) { if (!apps.length) {
await apps.load() await apps.load()
} }
await API.syncApp(application)
deployments = await fetchDeployments()
} catch (error) { } catch (error) {
notifications.error("Error initialising app overview") notifications.error("Error initialising app overview")
} }
loaded = true
})
onDestroy(() => {
store.actions.reset()
}) })
</script> </script>
{#if selectedApp} <span class="overview-wrap">
<span class="overview-wrap"> <Page wide noPadding>
<Page wide noPadding> {#if !loaded || !selectedApp}
{#await promise} <div class="loading">
<div class="loading"> <ProgressCircle size="XL" />
<ProgressCircle size="XL" /> </div>
</div> {:else}
{:then _} <Layout paddingX="XXL" paddingY="XL" gap="L">
<Layout paddingX="XXL" paddingY="XL" gap="L"> <span class="page-header" class:loaded>
<span class="page-header" class:loaded> <ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}>
<ActionButton secondary icon={"ArrowLeft"} on:click={backToAppList}> Back
Back </ActionButton>
</ActionButton> </span>
</span> <div class="overview-header">
<div class="overview-header"> <div class="app-title">
<div class="app-title"> <div class="app-logo">
<div class="app-logo"> <div
<div class="app-icon"
class="app-icon" style="color: {selectedApp?.icon?.color || ''}"
style="color: {selectedApp?.icon?.color || ''}" >
> <EditableIcon
<EditableIcon app={selectedApp}
app={selectedApp} size="XL"
size="XL" name={selectedApp?.icon?.name || "Apps"}
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> </div>
<div class="header-right"> <div class="app-details">
<AppLockModal app={selectedApp} /> <Heading size="M">{selectedApp?.name}</Heading>
<ButtonGroup gap="XS"> <div class="app-url">{appUrl}</div>
<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, { published: false })}
icon="DownloadFromCloud"
>
Export latest
</MenuItem>
{#if isPublished}
<MenuItem
on:click={() => exportApp(selectedApp, { 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>
</div> </div>
</Layout> <div class="header-right">
<div class="tab-wrap"> <AppLockModal app={selectedApp} />
<Tabs <ButtonGroup gap="XS">
selected={selectedTab} <Button
noPadding size="M"
on:select={e => { quiet
selectedTab = e.detail secondary
}} icon="Globe"
> disabled={!isPublished}
<Tab title="Overview"> on:click={viewApp}
<OverviewTab dataCy="view-app"
app={selectedApp} >
deployments={latestDeployments} View app
navigateTab={handleTabChange} </Button>
on:unpublish={e => unpublishApp(e.detail)} <Button
/> size="M"
</Tab> cta
<Tab title="Access"> icon="Edit"
<AccessTab app={selectedApp} /> disabled={lockedBy && !lockedByYou}
</Tab> on:click={() => {
{#if isPublished} editApp(selectedApp)
<Tab title="Automation History"> }}
<HistoryTab app={selectedApp} /> >
</Tab> <span>Edit</span>
{/if} </Button>
{#if false} </ButtonGroup>
<Tab title="Backups"> <ActionMenu align="right" dataCy="app-overview-menu-popover">
<div class="container">Backups contents</div> <span slot="control" class="app-overview-actions-icon">
</Tab> <Icon hoverable name="More" />
{/if} </span>
<Tab title="Settings"> <MenuItem
<SettingsTab app={selectedApp} /> on:click={() => exportApp(selectedApp, { published: false })}
</Tab> icon="DownloadFromCloud"
</Tabs> >
Export latest
</MenuItem>
{#if isPublished}
<MenuItem
on:click={() => exportApp(selectedApp, { 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> </div>
<ConfirmDialog </Layout>
bind:this={deletionModal} <div class="tab-wrap">
title="Confirm deletion" <Tabs
okText="Delete app" selected={selectedTab}
onOk={confirmDeleteApp} noPadding
onCancel={() => (appName = null)} on:select={e => {
disabled={appName !== selectedApp?.name} selectedTab = e.detail
}}
> >
Are you sure you want to delete the app <b>{selectedApp?.name}</b>? <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> <p>Please enter the app name below to confirm.</p>
<Input <Input
bind:value={appName} bind:value={appName}
data-cy="delete-app-confirmation" data-cy="delete-app-confirmation"
placeholder={selectedApp?.name} placeholder={selectedApp?.name}
/> />
</ConfirmDialog> </ConfirmDialog>
<ConfirmDialog <ConfirmDialog
bind:this={unpublishModal} bind:this={unpublishModal}
title="Confirm unpublish" title="Confirm unpublish"
okText="Unpublish app" okText="Unpublish app"
onOk={confirmUnpublishApp} onOk={confirmUnpublishApp}
dataCy={"unpublish-modal"} dataCy={"unpublish-modal"}
> >
Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>? Are you sure you want to unpublish the app <b>{selectedApp?.name}</b>?
</ConfirmDialog> </ConfirmDialog>
{:catch error} {/if}
<p>Something went wrong: {error.message}</p> </Page>
{/await} </span>
</Page>
</span>
{/if}
<style> <style>
.app-url { .app-url {

View File

@ -6,17 +6,26 @@
import clientPackage from "@budibase/client/package.json" import clientPackage from "@budibase/client/package.json"
import { processStringSync } from "@budibase/string-templates" import { processStringSync } from "@budibase/string-templates"
import { users, auth, apps } from "stores/portal" import { users, auth, apps } from "stores/portal"
import { createEventDispatcher, onMount } from "svelte" import { createEventDispatcher } from "svelte"
import { fetchData } from "@budibase/frontend-core"
import { API } from "api"
export let app export let app
export let deployments export let deployments
export let navigateTab export let navigateTab
let userCount
const dispatch = createEventDispatcher()
const unpublishApp = () => { const dispatch = createEventDispatcher()
dispatch("unpublish", app) const appUsersFetch = fetchData({
} API,
datasource: {
type: "user",
},
options: {
query: {
appId: apps.getProdAppID(app.devId),
},
},
})
let appEditor, appEditorPromise let appEditor, appEditorPromise
@ -25,6 +34,11 @@
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy $: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
$: appEditorText = appEditor?.firstName || appEditor?.email $: appEditorText = appEditor?.firstName || appEditor?.email
$: fetchAppEditor(appEditorId) $: fetchAppEditor(appEditorId)
$: appUsers = $appUsersFetch.rows || []
const unpublishApp = () => {
dispatch("unpublish", app)
}
async function fetchAppEditor(editorId) { async function fetchAppEditor(editorId) {
appEditorPromise = users.get(editorId) appEditorPromise = users.get(editorId)
@ -36,16 +50,8 @@
initials += user.firstName ? user.firstName[0] : "" initials += user.firstName ? user.firstName[0] : ""
initials += user.lastName ? user.lastName[0] : "" initials += user.lastName ? user.lastName[0] : ""
return initials == "" ? user.email[0] : initials return initials === "" ? user.email[0] : initials
} }
onMount(async () => {
let resp = await users.getUserCountByApp({
appId: apps.getProdAppID(app.devId),
})
userCount = resp.userCount
await users.search({ appId: apps.getProdAppID(app.devId), limit: 4 })
})
</script> </script>
<div class="overview-tab"> <div class="overview-tab">
@ -141,38 +147,41 @@
{/if} {/if}
</div> </div>
</DashCard> </DashCard>
<DashCard {#if $appUsersFetch.loaded}
title={"Access"} <DashCard
showIcon={true} title={"Access"}
action={() => { showIcon={true}
navigateTab("Access") action={() => {
}} navigateTab("Access")
dataCy={"access"} }}
> dataCy={"access"}
<div class="last-edited-content"> >
{#if $users?.data?.length} <div class="last-edited-content">
<Layout noPadding gap="S"> {#if appUsers.length}
<div class="users-tab"> <Layout noPadding gap="S">
{#each $users?.data as user} <div class="users-tab">
<Avatar size="M" initials={getInitials(user)} /> {#each appUsers.slice(0, 4) as user}
{/each} <Avatar size="M" initials={getInitials(user)} />
</div> {/each}
</div>
<div class="users-text"> <div class="users-text">
{userCount} {appUsers.length}
{userCount > 1 ? `users have` : `user has`} access to this app {appUsers.length > 1 ? `users have` : `user has`} access to this
</div> app
</Layout> </div>
{:else} </Layout>
<Layout noPadding gap="S"> {:else}
<Body>No users</Body> <Layout noPadding gap="S">
<div class="users-text"> <Body>No users</Body>
No users have been assigned to this app <div class="users-text">
</div> No users have been assigned to this app
</Layout> </div>
{/if} </Layout>
</div> {/if}
</DashCard> </div>
</DashCard>
{/if}
</div> </div>
{#if false} {#if false}
<div class="bottom"> <div class="bottom">
@ -229,7 +238,7 @@
.users-tab { .users-tab {
display: flex; display: flex;
gap: var(--spacing-m); gap: var(--spacing-s);
} }
.users-text { .users-text {