Merge branch 'develop' of github.com:Budibase/budibase into cheeks-fixes
This commit is contained in:
commit
b7603f8bf1
|
@ -126,6 +126,16 @@ http {
|
||||||
proxy_pass http://app-service;
|
proxy_pass http://app-service;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /embed {
|
||||||
|
rewrite /embed/(.*) /app/$1 break;
|
||||||
|
proxy_pass http://app-service;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header x-budibase-embed "true";
|
||||||
|
add_header x-budibase-embed "true";
|
||||||
|
add_header Content-Security-Policy "frame-ancestors *";
|
||||||
|
}
|
||||||
|
|
||||||
location /builder {
|
location /builder {
|
||||||
proxy_read_timeout 120s;
|
proxy_read_timeout 120s;
|
||||||
proxy_connect_timeout 120s;
|
proxy_connect_timeout 120s;
|
||||||
|
|
|
@ -92,6 +92,16 @@ http {
|
||||||
proxy_pass $apps;
|
proxy_pass $apps;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
location /embed {
|
||||||
|
rewrite /embed/(.*) /app/$1 break;
|
||||||
|
proxy_pass $apps;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header x-budibase-embed "true";
|
||||||
|
add_header x-budibase-embed "true";
|
||||||
|
add_header Content-Security-Policy "frame-ancestors *";
|
||||||
|
}
|
||||||
|
|
||||||
location = / {
|
location = / {
|
||||||
proxy_pass $apps;
|
proxy_pass $apps;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"version": "2.7.34-alpha.4",
|
"version": "2.7.34-alpha.9",
|
||||||
"npmClient": "yarn",
|
"npmClient": "yarn",
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/*"
|
"packages/*"
|
||||||
|
|
|
@ -14,10 +14,15 @@ async function servedBuilder(timezone: string) {
|
||||||
await publishEvent(Event.SERVED_BUILDER, properties)
|
await publishEvent(Event.SERVED_BUILDER, properties)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function servedApp(app: App, timezone: string) {
|
async function servedApp(
|
||||||
|
app: App,
|
||||||
|
timezone: string,
|
||||||
|
embed?: boolean | undefined
|
||||||
|
) {
|
||||||
const properties: AppServedEvent = {
|
const properties: AppServedEvent = {
|
||||||
appVersion: app.version,
|
appVersion: app.version,
|
||||||
timezone,
|
timezone,
|
||||||
|
embed: embed === true,
|
||||||
}
|
}
|
||||||
await publishEvent(Event.SERVED_APP, properties)
|
await publishEvent(Event.SERVED_APP, properties)
|
||||||
}
|
}
|
||||||
|
|
|
@ -120,10 +120,13 @@ export const getFrontendStore = () => {
|
||||||
reset: () => {
|
reset: () => {
|
||||||
store.set({ ...INITIAL_FRONTEND_STATE })
|
store.set({ ...INITIAL_FRONTEND_STATE })
|
||||||
websocket?.disconnect()
|
websocket?.disconnect()
|
||||||
|
websocket = null
|
||||||
},
|
},
|
||||||
initialise: async pkg => {
|
initialise: async pkg => {
|
||||||
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
const { layouts, screens, application, clientLibPath, hasLock } = pkg
|
||||||
|
if (!websocket) {
|
||||||
websocket = createBuilderWebsocket(application.appId)
|
websocket = createBuilderWebsocket(application.appId)
|
||||||
|
}
|
||||||
await store.actions.components.refreshDefinitions(application.appId)
|
await store.actions.components.refreshDefinitions(application.appId)
|
||||||
|
|
||||||
// Reset store state
|
// Reset store state
|
||||||
|
|
|
@ -333,7 +333,7 @@
|
||||||
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
: null}>{value.title || (key === "row" ? "Table" : key)}</Label
|
||||||
>
|
>
|
||||||
{/if}
|
{/if}
|
||||||
{#if value.type === "string" && value.enum && canShowField(key)}
|
{#if value.type === "string" && value.enum && canShowField(key, value)}
|
||||||
<Select
|
<Select
|
||||||
on:change={e => onChange(e, key)}
|
on:change={e => onChange(e, key)}
|
||||||
value={inputData[key]}
|
value={inputData[key]}
|
||||||
|
|
|
@ -55,7 +55,7 @@
|
||||||
name: "Automations",
|
name: "Automations",
|
||||||
description: "",
|
description: "",
|
||||||
icon: "Compass",
|
icon: "Compass",
|
||||||
action: () => $goto("./automate"),
|
action: () => $goto("./automation"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: "Publish",
|
type: "Publish",
|
||||||
|
@ -127,7 +127,7 @@
|
||||||
type: "Automation",
|
type: "Automation",
|
||||||
name: automation.name,
|
name: automation.name,
|
||||||
icon: "ShareAndroid",
|
icon: "ShareAndroid",
|
||||||
action: () => $goto(`./automate/${automation._id}`),
|
action: () => $goto(`./automation/${automation._id}`),
|
||||||
})),
|
})),
|
||||||
...Constants.Themes.map(theme => ({
|
...Constants.Themes.map(theme => ({
|
||||||
type: "Change Builder Theme",
|
type: "Change Builder Theme",
|
||||||
|
|
|
@ -3,34 +3,45 @@
|
||||||
notifications,
|
notifications,
|
||||||
Popover,
|
Popover,
|
||||||
Layout,
|
Layout,
|
||||||
Heading,
|
|
||||||
Body,
|
Body,
|
||||||
Button,
|
Button,
|
||||||
ActionButton,
|
ActionButton,
|
||||||
|
Icon,
|
||||||
|
Link,
|
||||||
|
Modal,
|
||||||
|
StatusLight,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import RevertModal from "components/deploy/RevertModal.svelte"
|
import RevertModal from "components/deploy/RevertModal.svelte"
|
||||||
import VersionModal from "components/deploy/VersionModal.svelte"
|
import VersionModal from "components/deploy/VersionModal.svelte"
|
||||||
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
import { processStringSync } from "@budibase/string-templates"
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
import analytics, { Events, EventSource } from "analytics"
|
||||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
import DeployModal from "components/deploy/DeployModal.svelte"
|
|
||||||
import { apps } from "stores/portal"
|
import { apps } from "stores/portal"
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
import TourWrap from "components/portal/onboarding/TourWrap.svelte"
|
||||||
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
import { TOUR_STEP_KEYS } from "components/portal/onboarding/tours.js"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
|
||||||
export let application
|
export let application
|
||||||
|
export let loaded
|
||||||
|
|
||||||
let publishPopover
|
|
||||||
let publishPopoverAnchor
|
|
||||||
let unpublishModal
|
let unpublishModal
|
||||||
|
let updateAppModal
|
||||||
|
let revertModal
|
||||||
|
let versionModal
|
||||||
|
|
||||||
$: filteredApps = $apps.filter(
|
let appActionPopover
|
||||||
app => app.devId === application && app.status === "published"
|
let appActionPopoverOpen = false
|
||||||
)
|
let appActionPopoverAnchor
|
||||||
|
|
||||||
|
let publishing = false
|
||||||
|
|
||||||
|
$: filteredApps = $apps.filter(app => app.devId === application)
|
||||||
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
$: selectedApp = filteredApps?.length ? filteredApps[0] : null
|
||||||
|
|
||||||
$: deployments = []
|
$: deployments = []
|
||||||
|
@ -38,7 +49,29 @@
|
||||||
.filter(deployment => deployment.status === "SUCCESS")
|
.filter(deployment => deployment.status === "SUCCESS")
|
||||||
.sort((a, b) => a.updatedAt > b.updatedAt)
|
.sort((a, b) => a.updatedAt > b.updatedAt)
|
||||||
|
|
||||||
$: isPublished = selectedApp && latestDeployments?.length > 0
|
$: isPublished =
|
||||||
|
selectedApp?.status === "published" && latestDeployments?.length > 0
|
||||||
|
|
||||||
|
$: updateAvailable =
|
||||||
|
$store.upgradableVersion &&
|
||||||
|
$store.version &&
|
||||||
|
$store.upgradableVersion !== $store.version
|
||||||
|
|
||||||
|
$: canPublish = !publishing && loaded
|
||||||
|
|
||||||
|
const initialiseApp = async () => {
|
||||||
|
const applicationPkg = await API.fetchAppPackage($store.devId)
|
||||||
|
await store.actions.initialise(applicationPkg)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateDeploymentString = () => {
|
||||||
|
return deployments?.length
|
||||||
|
? processStringSync("Published {{ duration time 'millisecond' }} ago", {
|
||||||
|
time:
|
||||||
|
new Date().getTime() - new Date(deployments[0].updatedAt).getTime(),
|
||||||
|
})
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
|
||||||
const reviewPendingDeployments = (deployments, newDeployments) => {
|
const reviewPendingDeployments = (deployments, newDeployments) => {
|
||||||
if (deployments.length > 0) {
|
if (deployments.length > 0) {
|
||||||
|
@ -80,11 +113,36 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function publishApp() {
|
||||||
|
try {
|
||||||
|
publishing = true
|
||||||
|
|
||||||
|
await API.publishAppChanges($store.appId)
|
||||||
|
|
||||||
|
notifications.send("App published", {
|
||||||
|
type: "success",
|
||||||
|
icon: "GlobeCheck",
|
||||||
|
})
|
||||||
|
|
||||||
|
await completePublish()
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
analytics.captureException(error)
|
||||||
|
notifications.error("Error publishing app")
|
||||||
|
}
|
||||||
|
publishing = false
|
||||||
|
}
|
||||||
|
|
||||||
const unpublishApp = () => {
|
const unpublishApp = () => {
|
||||||
publishPopover.hide()
|
appActionPopover.hide()
|
||||||
unpublishModal.show()
|
unpublishModal.show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const revertApp = () => {
|
||||||
|
appActionPopover.hide()
|
||||||
|
revertModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
const confirmUnpublishApp = async () => {
|
const confirmUnpublishApp = async () => {
|
||||||
if (!application || !isPublished) {
|
if (!application || !isPublished) {
|
||||||
//confirm the app has loaded.
|
//confirm the app has loaded.
|
||||||
|
@ -93,7 +151,10 @@
|
||||||
try {
|
try {
|
||||||
await API.unpublishApp(selectedApp.prodId)
|
await API.unpublishApp(selectedApp.prodId)
|
||||||
await apps.load()
|
await apps.load()
|
||||||
notifications.success("App unpublished successfully")
|
notifications.send("App unpublished", {
|
||||||
|
type: "success",
|
||||||
|
icon: "GlobeStrike",
|
||||||
|
})
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error("Error unpublishing app")
|
notifications.error("Error unpublishing app")
|
||||||
}
|
}
|
||||||
|
@ -117,83 +178,29 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $store.hasLock}
|
{#if $store.hasLock}
|
||||||
<div class="action-top-nav">
|
<div class="action-top-nav" class:has-lock={$store.hasLock}>
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<div class="version">
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
<VersionModal />
|
{#if updateAvailable}
|
||||||
|
<div class="app-action-button version" on:click={versionModal.show}>
|
||||||
|
<div class="app-action">
|
||||||
|
<ActionButton quiet>
|
||||||
|
<StatusLight notice />
|
||||||
|
Update
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
<RevertModal />
|
|
||||||
|
|
||||||
{#if isPublished}
|
|
||||||
<div class="publish-popover">
|
|
||||||
<div bind:this={publishPopoverAnchor}>
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="Globe"
|
|
||||||
size="M"
|
|
||||||
tooltip="Your published app"
|
|
||||||
on:click={publishPopover.show()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Popover
|
|
||||||
bind:this={publishPopover}
|
|
||||||
align="right"
|
|
||||||
disabled={!isPublished}
|
|
||||||
anchor={publishPopoverAnchor}
|
|
||||||
offset={10}
|
|
||||||
>
|
|
||||||
<div class="popover-content">
|
|
||||||
<Layout noPadding gap="M">
|
|
||||||
<Heading size="XS">Your published app</Heading>
|
|
||||||
<Body size="S">
|
|
||||||
<span class="publish-popover-message">
|
|
||||||
{processStringSync(
|
|
||||||
"Last published {{ duration time 'millisecond' }} ago",
|
|
||||||
{
|
|
||||||
time:
|
|
||||||
new Date().getTime() -
|
|
||||||
new Date(latestDeployments[0].updatedAt).getTime(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</Body>
|
|
||||||
<div class="buttons">
|
|
||||||
<Button
|
|
||||||
warning={true}
|
|
||||||
icon="GlobeStrike"
|
|
||||||
disabled={!isPublished}
|
|
||||||
on:click={unpublishApp}
|
|
||||||
>
|
|
||||||
Unpublish
|
|
||||||
</Button>
|
|
||||||
<Button cta on:click={viewApp}>View app</Button>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
</Popover>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !isPublished}
|
|
||||||
<ActionButton
|
|
||||||
quiet
|
|
||||||
icon="GlobeStrike"
|
|
||||||
size="M"
|
|
||||||
tooltip="Your app has not been published yet"
|
|
||||||
disabled
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<TourWrap
|
<TourWrap
|
||||||
tourStepKey={$store.onboarding
|
tourStepKey={$store.onboarding
|
||||||
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
? TOUR_STEP_KEYS.BUILDER_USER_MANAGEMENT
|
||||||
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
: TOUR_STEP_KEYS.FEATURE_USER_MANAGEMENT}
|
||||||
>
|
>
|
||||||
<span id="builder-app-users-button">
|
<div class="app-action-button users">
|
||||||
|
<div class="app-action" id="builder-app-users-button">
|
||||||
<ActionButton
|
<ActionButton
|
||||||
quiet
|
quiet
|
||||||
icon="UserGroup"
|
icon="UserGroup"
|
||||||
size="M"
|
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
store.update(state => {
|
store.update(state => {
|
||||||
state.builderSidePanel = true
|
state.builderSidePanel = true
|
||||||
|
@ -203,11 +210,129 @@
|
||||||
>
|
>
|
||||||
Users
|
Users
|
||||||
</ActionButton>
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TourWrap>
|
||||||
|
|
||||||
|
<div class="app-action-button preview">
|
||||||
|
<div class="app-action">
|
||||||
|
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||||
|
Preview
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<div
|
||||||
|
class="app-action-button publish app-action-popover"
|
||||||
|
on:click={() => {
|
||||||
|
if (!appActionPopoverOpen) {
|
||||||
|
appActionPopover.show()
|
||||||
|
} else {
|
||||||
|
appActionPopover.hide()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div bind:this={appActionPopoverAnchor}>
|
||||||
|
<div class="app-action">
|
||||||
|
<Icon name={isPublished ? "GlobeCheck" : "GlobeStrike"} />
|
||||||
|
<TourWrap tourStepKey={TOUR_STEP_KEYS.BUILDER_APP_PUBLISH}>
|
||||||
|
<span class="publish-open" id="builder-app-publish-button">
|
||||||
|
Publish
|
||||||
|
<Icon
|
||||||
|
name={appActionPopoverOpen ? "ChevronUp" : "ChevronDown"}
|
||||||
|
size="M"
|
||||||
|
/>
|
||||||
</span>
|
</span>
|
||||||
</TourWrap>
|
</TourWrap>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Popover
|
||||||
|
bind:this={appActionPopover}
|
||||||
|
align="right"
|
||||||
|
disabled={!isPublished}
|
||||||
|
anchor={appActionPopoverAnchor}
|
||||||
|
offset={35}
|
||||||
|
on:close={() => {
|
||||||
|
appActionPopoverOpen = false
|
||||||
|
}}
|
||||||
|
on:open={() => {
|
||||||
|
appActionPopoverOpen = true
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div class="app-action-popover-content">
|
||||||
|
<Layout noPadding gap="M">
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<Body size="M">
|
||||||
|
<span
|
||||||
|
class="app-link"
|
||||||
|
on:click={() => {
|
||||||
|
if (isPublished) {
|
||||||
|
viewApp()
|
||||||
|
} else {
|
||||||
|
appActionPopover.hide()
|
||||||
|
updateAppModal.show()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectedApp ? `${selectedApp?.url}` : ""}
|
||||||
|
{#if isPublished}
|
||||||
|
<Icon size="S" name="LinkOut" />
|
||||||
|
{:else}
|
||||||
|
<Icon size="S" name="Edit" />
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
|
||||||
|
<Body size="S">
|
||||||
|
<span class="publish-popover-status">
|
||||||
|
{#if isPublished}
|
||||||
|
<span class="status-text">
|
||||||
|
{updateDeploymentString(deployments)}
|
||||||
|
</span>
|
||||||
|
<span class="unpublish-link">
|
||||||
|
<Link quiet on:click={unpublishApp}>Unpublish</Link>
|
||||||
|
</span>
|
||||||
|
<span class="revert-link">
|
||||||
|
<Link quiet secondary on:click={revertApp}>Revert</Link>
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span class="status-text unpublished">Not published</span>
|
||||||
|
{/if}
|
||||||
|
</span>
|
||||||
|
</Body>
|
||||||
|
<div class="action-buttons">
|
||||||
|
{#if $store.hasLock}
|
||||||
|
{#if isPublished}
|
||||||
|
<ActionButton
|
||||||
|
quiet
|
||||||
|
icon="Code"
|
||||||
|
on:click={() => {
|
||||||
|
$goto("./settings/embed")
|
||||||
|
appActionPopover.hide()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Embed
|
||||||
|
</ActionButton>
|
||||||
|
{/if}
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
on:click={publishApp}
|
||||||
|
id={"builder-app-publish-button"}
|
||||||
|
disabled={!canPublish}
|
||||||
|
>
|
||||||
|
Publish
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
<ConfirmDialog
|
<ConfirmDialog
|
||||||
bind:this={unpublishModal}
|
bind:this={unpublishModal}
|
||||||
title="Confirm unpublish"
|
title="Confirm unpublish"
|
||||||
|
@ -216,45 +341,117 @@
|
||||||
>
|
>
|
||||||
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>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="buttons">
|
<Modal bind:this={updateAppModal} padding={false} width="600px">
|
||||||
<Button on:click={previewApp} secondary>Preview</Button>
|
<UpdateAppModal
|
||||||
{#if $store.hasLock}
|
app={selectedApp}
|
||||||
<DeployModal onOk={completePublish} />
|
onUpdateComplete={async () => {
|
||||||
{/if}
|
await initialiseApp()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<RevertModal bind:this={revertModal} />
|
||||||
|
<VersionModal hideIcon bind:this={versionModal} />
|
||||||
|
{:else}
|
||||||
|
<div class="app-action-button preview-locked">
|
||||||
|
<div class="app-action">
|
||||||
|
<ActionButton quiet icon="PlayCircle" on:click={previewApp}>
|
||||||
|
Preview
|
||||||
|
</ActionButton>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
/* .banner-btn {
|
.app-action-popover-content {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-s);
|
|
||||||
} */
|
|
||||||
.popover-content {
|
|
||||||
padding: var(--spacing-xl);
|
padding: var(--spacing-xl);
|
||||||
|
width: 360px;
|
||||||
}
|
}
|
||||||
.buttons {
|
|
||||||
display: flex;
|
.app-action-popover-content :global(.icon svg.spectrum-Icon) {
|
||||||
flex-direction: row;
|
height: 0.8em;
|
||||||
justify-content: flex-end;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-l);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* gap: var(--spacing-s); */
|
height: 100%;
|
||||||
}
|
|
||||||
.version {
|
|
||||||
margin-right: var(--spacing-s);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-top-nav {
|
.action-top-nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.app-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.app-action-popover-content .status-text {
|
||||||
|
color: var(--spectrum-global-color-green-500);
|
||||||
|
border-right: 1px solid var(--spectrum-global-color-gray-500);
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.app-action-popover-content .status-text.unpublished {
|
||||||
|
color: var(--spectrum-global-color-gray-600);
|
||||||
|
border-right: 0px;
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
.app-action-popover-content .action-buttons {
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.app-action-popover-content
|
||||||
|
.publish-popover-status
|
||||||
|
.unpublish-link
|
||||||
|
:global(.spectrum-Link) {
|
||||||
|
color: var(--spectrum-global-color-red-400);
|
||||||
|
}
|
||||||
|
.publish-popover-status {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-m);
|
||||||
|
}
|
||||||
|
.app-action-popover .publish-open {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-right: var(--spacing-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button.publish:hover {
|
||||||
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button.publish {
|
||||||
|
border-left: var(--border-light);
|
||||||
|
padding: 0px var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button.version :global(.spectrum-ActionButton-label) {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spectrum-actionbutton-icon-gap);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action-button.preview-locked {
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-action {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
<script>
|
||||||
|
import { Input, notifications } from "@budibase/bbui"
|
||||||
|
import { goto } from "@roxi/routify"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
|
export const show = () => {
|
||||||
|
deletionModal.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hide = () => {
|
||||||
|
deletionModal.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
let deletionModal
|
||||||
|
let deletionConfirmationAppName
|
||||||
|
|
||||||
|
const deleteApp = async () => {
|
||||||
|
try {
|
||||||
|
await API.deleteApp($store.appId)
|
||||||
|
apps.load()
|
||||||
|
notifications.success("App deleted successfully")
|
||||||
|
$goto("/builder")
|
||||||
|
} catch (err) {
|
||||||
|
notifications.error("Error deleting app")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ConfirmDialog
|
||||||
|
bind:this={deletionModal}
|
||||||
|
title="Delete app"
|
||||||
|
okText="Delete"
|
||||||
|
onOk={deleteApp}
|
||||||
|
onCancel={() => (deletionConfirmationAppName = null)}
|
||||||
|
disabled={deletionConfirmationAppName !== $store.name}
|
||||||
|
>
|
||||||
|
Are you sure you want to delete <b>{$store.name}</b>?
|
||||||
|
<br />
|
||||||
|
Please enter the app name below to confirm.
|
||||||
|
<br /><br />
|
||||||
|
<Input bind:value={deletionConfirmationAppName} placeholder={$store.name} />
|
||||||
|
</ConfirmDialog>
|
|
@ -1,15 +1,9 @@
|
||||||
<script>
|
<script>
|
||||||
import {
|
import { Input, Modal, notifications, ModalContent } from "@budibase/bbui"
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
notifications,
|
|
||||||
ModalContent,
|
|
||||||
ActionButton,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
import { store } from "builderStore"
|
||||||
import { API } from "api"
|
import { API } from "api"
|
||||||
|
|
||||||
export let disabled = false
|
export let onComplete = () => {}
|
||||||
|
|
||||||
let revertModal
|
let revertModal
|
||||||
let appName
|
let appName
|
||||||
|
@ -24,20 +18,20 @@
|
||||||
const applicationPkg = await API.fetchAppPackage(appId)
|
const applicationPkg = await API.fetchAppPackage(appId)
|
||||||
await store.actions.initialise(applicationPkg)
|
await store.actions.initialise(applicationPkg)
|
||||||
notifications.info("Changes reverted successfully")
|
notifications.info("Changes reverted successfully")
|
||||||
|
onComplete()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifications.error(`Error reverting changes: ${error}`)
|
notifications.error(`Error reverting changes: ${error}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
|
||||||
|
|
||||||
<ActionButton
|
export const hide = () => {
|
||||||
quiet
|
revertModal.hide()
|
||||||
icon="Revert"
|
}
|
||||||
size="M"
|
|
||||||
tooltip="Revert changes"
|
export const show = () => {
|
||||||
on:click={revertModal.show}
|
revertModal.show()
|
||||||
{disabled}
|
}
|
||||||
/>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={revertModal}>
|
<Modal bind:this={revertModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
updateModal.hide()
|
updateModal.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export let onComplete = () => {}
|
||||||
export let hideIcon = false
|
export let hideIcon = false
|
||||||
|
|
||||||
let updateModal
|
let updateModal
|
||||||
|
@ -47,6 +48,7 @@
|
||||||
notifications.success(
|
notifications.success(
|
||||||
`App updated successfully to version ${$store.upgradableVersion}`
|
`App updated successfully to version ${$store.upgradableVersion}`
|
||||||
)
|
)
|
||||||
|
onComplete()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
notifications.error(`Error updating app: ${err}`)
|
notifications.error(`Error updating app: ${err}`)
|
||||||
}
|
}
|
||||||
|
@ -70,9 +72,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if !hideIcon && updateAvailable}
|
{#if !hideIcon && updateAvailable}
|
||||||
<StatusLight hoverable on:click={updateModal.show} notice>
|
<StatusLight hoverable on:click={updateModal.show} notice>Update</StatusLight>
|
||||||
Update available
|
|
||||||
</StatusLight>
|
|
||||||
{/if}
|
{/if}
|
||||||
<Modal bind:this={updateModal}>
|
<Modal bind:this={updateModal}>
|
||||||
<ModalContent
|
<ModalContent
|
||||||
|
|
|
@ -11,7 +11,7 @@ export const TOUR_STEP_KEYS = {
|
||||||
BUILDER_DATA_SECTION: "builder-data-section",
|
BUILDER_DATA_SECTION: "builder-data-section",
|
||||||
BUILDER_DESIGN_SECTION: "builder-design-section",
|
BUILDER_DESIGN_SECTION: "builder-design-section",
|
||||||
BUILDER_USER_MANAGEMENT: "builder-user-management",
|
BUILDER_USER_MANAGEMENT: "builder-user-management",
|
||||||
BUILDER_AUTOMATE_SECTION: "builder-automate-section",
|
BUILDER_AUTOMATION_SECTION: "builder-automation-section",
|
||||||
FEATURE_USER_MANAGEMENT: "feature-user-management",
|
FEATURE_USER_MANAGEMENT: "feature-user-management",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ const getTours = () => {
|
||||||
title: "Data",
|
title: "Data",
|
||||||
route: "/builder/app/:application/data",
|
route: "/builder/app/:application/data",
|
||||||
layout: OnboardingData,
|
layout: OnboardingData,
|
||||||
query: ".topcenternav .spectrum-Tabs-item#builder-data-tab",
|
query: ".topleftnav .spectrum-Tabs-item#builder-data-tab",
|
||||||
onLoad: async () => {
|
onLoad: async () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DATA_SECTION)
|
||||||
},
|
},
|
||||||
|
@ -45,20 +45,20 @@ const getTours = () => {
|
||||||
title: "Design",
|
title: "Design",
|
||||||
route: "/builder/app/:application/design",
|
route: "/builder/app/:application/design",
|
||||||
layout: OnboardingDesign,
|
layout: OnboardingDesign,
|
||||||
query: ".topcenternav .spectrum-Tabs-item#builder-design-tab",
|
query: ".topleftnav .spectrum-Tabs-item#builder-design-tab",
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_DESIGN_SECTION)
|
||||||
},
|
},
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION,
|
id: TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION,
|
||||||
title: "Automations",
|
title: "Automations",
|
||||||
route: "/builder/app/:application/automate",
|
route: "/builder/app/:application/automation",
|
||||||
query: ".topcenternav .spectrum-Tabs-item#builder-automate-tab",
|
query: ".topleftnav .spectrum-Tabs-item#builder-automation-tab",
|
||||||
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
body: "Once you have your app screens made, you can set up automations to fit in with your current workflow",
|
||||||
onLoad: () => {
|
onLoad: () => {
|
||||||
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATE_SECTION)
|
tourEvent(TOUR_STEP_KEYS.BUILDER_AUTOMATION_SECTION)
|
||||||
},
|
},
|
||||||
align: "left",
|
align: "left",
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,18 +4,27 @@
|
||||||
export let active = false
|
export let active = false
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#if url}
|
||||||
<a on:click href={url} class:active>
|
<a on:click href={url} class:active>
|
||||||
{text || ""}
|
{text || ""}
|
||||||
</a>
|
</a>
|
||||||
|
{:else}
|
||||||
|
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||||
|
<span on:click class:active>
|
||||||
|
{text || ""}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
a {
|
a,
|
||||||
|
span {
|
||||||
padding: var(--spacing-s) var(--spacing-m);
|
padding: var(--spacing-s) var(--spacing-m);
|
||||||
color: var(--spectrum-global-color-gray-900);
|
color: var(--spectrum-global-color-gray-900);
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
transition: background 130ms ease-out;
|
transition: background 130ms ease-out;
|
||||||
}
|
}
|
||||||
.active,
|
.active,
|
||||||
|
span:hover,
|
||||||
a:hover {
|
a:hover {
|
||||||
background-color: var(--spectrum-global-color-gray-200);
|
background-color: var(--spectrum-global-color-gray-200);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
const goToOverview = () => {
|
const goToOverview = () => {
|
||||||
$goto(`../overview/${app.devId}`)
|
$goto(`../../app/${app.devId}/settings`)
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
import EditableIcon from "../common/EditableIcon.svelte"
|
import EditableIcon from "../common/EditableIcon.svelte"
|
||||||
|
|
||||||
export let app
|
export let app
|
||||||
|
export let onUpdateComplete
|
||||||
|
|
||||||
const values = writable({
|
const values = writable({
|
||||||
name: app.name,
|
name: app.name,
|
||||||
|
@ -54,6 +55,9 @@
|
||||||
color: $values.iconColor,
|
color: $values.iconColor,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
if (typeof onUpdateComplete == "function") {
|
||||||
|
onUpdateComplete()
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
notifications.error("Error updating app")
|
notifications.error("Error updating app")
|
||||||
|
|
|
@ -4,8 +4,6 @@
|
||||||
import { auth } from "stores/portal"
|
import { auth } from "stores/portal"
|
||||||
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
import { TENANT_FEATURE_FLAGS, isEnabled } from "helpers/featureFlags"
|
||||||
import {
|
import {
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
Icon,
|
Icon,
|
||||||
Tabs,
|
Tabs,
|
||||||
Tab,
|
Tab,
|
||||||
|
@ -142,56 +140,17 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="root" class:blur={$store.showPreview}>
|
<div class="root" class:blur={$store.showPreview}>
|
||||||
<div class="top-nav">
|
<div class="top-nav" class:has-lock={$store.hasLock}>
|
||||||
{#if $store.initialised}
|
{#if $store.initialised}
|
||||||
<div class="topleftnav">
|
<div class="topleftnav">
|
||||||
<ActionMenu>
|
<span class="back-to-apps">
|
||||||
<div slot="control">
|
<Icon
|
||||||
<Icon size="M" hoverable name="ShowMenu" />
|
size="S"
|
||||||
</div>
|
hoverable
|
||||||
<MenuItem on:click={() => $goto("../../portal/apps")}>
|
name="BackAndroid"
|
||||||
Exit to portal
|
on:click={() => $goto("../../portal/apps")}
|
||||||
</MenuItem>
|
/>
|
||||||
<MenuItem
|
</span>
|
||||||
on:click={() => $goto(`../../portal/overview/${application}`)}
|
|
||||||
>
|
|
||||||
Overview
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
on:click={() =>
|
|
||||||
$goto(`../../portal/overview/${application}/access`)}
|
|
||||||
>
|
|
||||||
Access
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
on:click={() =>
|
|
||||||
$goto(`../../portal/overview/${application}/automation-history`)}
|
|
||||||
>
|
|
||||||
Automation history
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
on:click={() =>
|
|
||||||
$goto(`../../portal/overview/${application}/backups`)}
|
|
||||||
>
|
|
||||||
Backups
|
|
||||||
</MenuItem>
|
|
||||||
|
|
||||||
<MenuItem
|
|
||||||
on:click={() =>
|
|
||||||
$goto(`../../portal/overview/${application}/name-and-url`)}
|
|
||||||
>
|
|
||||||
Name and URL
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem
|
|
||||||
on:click={() =>
|
|
||||||
$goto(`../../portal/overview/${application}/version`)}
|
|
||||||
>
|
|
||||||
Version
|
|
||||||
</MenuItem>
|
|
||||||
</ActionMenu>
|
|
||||||
<Heading size="XS">{$store.name}</Heading>
|
|
||||||
</div>
|
|
||||||
<div class="topcenternav">
|
|
||||||
{#if $store.hasLock}
|
{#if $store.hasLock}
|
||||||
<Tabs {selected} size="M">
|
<Tabs {selected} size="M">
|
||||||
{#each $layout.children as { path, title }}
|
{#each $layout.children as { path, title }}
|
||||||
|
@ -209,13 +168,23 @@
|
||||||
{:else}
|
{:else}
|
||||||
<div class="secondary-editor">
|
<div class="secondary-editor">
|
||||||
<Icon name="LockClosed" />
|
<Icon name="LockClosed" />
|
||||||
|
<div
|
||||||
|
class="secondary-editor-body"
|
||||||
|
title="Another user is currently editing your screens and automations"
|
||||||
|
>
|
||||||
Another user is currently editing your screens and automations
|
Another user is currently editing your screens and automations
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="topcenternav">
|
||||||
|
<Heading size="XS">{$store.name}</Heading>
|
||||||
|
</div>
|
||||||
<div class="toprightnav">
|
<div class="toprightnav">
|
||||||
|
<span class:nav-lock={!$store.hasLock}>
|
||||||
<UserAvatars users={$userStore} />
|
<UserAvatars users={$userStore} />
|
||||||
<AppActions {application} />
|
</span>
|
||||||
|
<AppActions {application} {loaded} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -241,6 +210,13 @@
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.back-to-apps {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.back-to-apps :global(.icon) {
|
||||||
|
margin-left: 12px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
.loading {
|
.loading {
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
@ -272,27 +248,34 @@
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topleftnav {
|
.top-nav.has-lock {
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topcenternav {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-xl);
|
gap: var(--spacing-xl);
|
||||||
}
|
}
|
||||||
.topleftnav :global(.spectrum-Heading) {
|
|
||||||
|
.topcenternav :global(.spectrum-Heading) {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
width: 0;
|
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
padding: 0px var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topcenternav {
|
.topleftnav {
|
||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
margin-bottom: -2px;
|
margin-bottom: -2px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.topcenternav :global(.spectrum-Tabs-itemLabel) {
|
|
||||||
|
.topleftnav :global(.spectrum-Tabs-itemLabel) {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -301,7 +284,10 @@
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--spacing-l);
|
}
|
||||||
|
|
||||||
|
.toprightnav :global(.avatars) {
|
||||||
|
margin-right: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
.secondary-editor {
|
.secondary-editor {
|
||||||
|
@ -309,6 +295,16 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-left: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary-editor-body {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
min-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.body {
|
.body {
|
||||||
|
|
|
@ -0,0 +1,78 @@
|
||||||
|
<script>
|
||||||
|
import { Content, SideNav, SideNavItem } from "components/portal/page"
|
||||||
|
import { Page, Layout } from "@budibase/bbui"
|
||||||
|
import { url, isActive } from "@roxi/routify"
|
||||||
|
import DeleteModal from "components/deploy/DeleteModal.svelte"
|
||||||
|
|
||||||
|
let deleteModal
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- routify:options index=4 -->
|
||||||
|
<div class="settings">
|
||||||
|
<Page>
|
||||||
|
<Layout noPadding gap="L">
|
||||||
|
<Content showMobileNav>
|
||||||
|
<SideNav slot="side-nav">
|
||||||
|
<SideNavItem
|
||||||
|
text="Automation History"
|
||||||
|
url={$url("./automation-history")}
|
||||||
|
active={$isActive("./automation-history")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Backups"
|
||||||
|
url={$url("./backups")}
|
||||||
|
active={$isActive("./backups")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Embed"
|
||||||
|
url={$url("./embed")}
|
||||||
|
active={$isActive("./embed")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Export"
|
||||||
|
url={$url("./export")}
|
||||||
|
active={$isActive("./export")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Name and URL"
|
||||||
|
url={$url("./name-and-url")}
|
||||||
|
active={$isActive("./name-and-url")}
|
||||||
|
/>
|
||||||
|
<SideNavItem
|
||||||
|
text="Version"
|
||||||
|
url={$url("./version")}
|
||||||
|
active={$isActive("./version")}
|
||||||
|
/>
|
||||||
|
<div class="delete-action">
|
||||||
|
<SideNavItem
|
||||||
|
text="Delete app"
|
||||||
|
on:click={() => {
|
||||||
|
deleteModal.show()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SideNav>
|
||||||
|
<slot />
|
||||||
|
</Content>
|
||||||
|
</Layout>
|
||||||
|
</Page>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DeleteModal bind:this={deleteModal} />
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.delete-action :global(span) {
|
||||||
|
color: var(--spectrum-global-color-red-400);
|
||||||
|
}
|
||||||
|
.delete-action {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
.settings {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: stretch;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -47,7 +47,7 @@
|
||||||
<Button
|
<Button
|
||||||
secondary
|
secondary
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
$goto(`../../../../app/${appId}/automate/${history.automationId}`)
|
$goto(`/builder/app/${appId}/automation/${history.automationId}`)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Edit automation
|
Edit automation
|
|
@ -12,11 +12,11 @@
|
||||||
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
import DateTimeRenderer from "components/common/renderers/DateTimeRenderer.svelte"
|
||||||
import StatusRenderer from "./_components/StatusRenderer.svelte"
|
import StatusRenderer from "./_components/StatusRenderer.svelte"
|
||||||
import HistoryDetailsPanel from "./_components/HistoryDetailsPanel.svelte"
|
import HistoryDetailsPanel from "./_components/HistoryDetailsPanel.svelte"
|
||||||
import { automationStore } from "builderStore"
|
import { automationStore, store } from "builderStore"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import { getContext, onDestroy, onMount } from "svelte"
|
import { getContext, onDestroy, onMount } from "svelte"
|
||||||
import dayjs from "dayjs"
|
import dayjs from "dayjs"
|
||||||
import { auth, licensing, admin, overview } from "stores/portal"
|
import { auth, licensing, admin } from "stores/portal"
|
||||||
import { Constants } from "@budibase/frontend-core"
|
import { Constants } from "@budibase/frontend-core"
|
||||||
import Portal from "svelte-portal"
|
import Portal from "svelte-portal"
|
||||||
|
|
||||||
|
@ -35,7 +35,6 @@
|
||||||
let loaded = false
|
let loaded = false
|
||||||
|
|
||||||
$: licensePlan = $auth.user?.license?.plan
|
$: licensePlan = $auth.user?.license?.plan
|
||||||
$: app = $overview.selectedApp
|
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchLogs(automationId, status, page, timeRange)
|
$: fetchLogs(automationId, status, page, timeRange)
|
||||||
|
|
||||||
|
@ -191,7 +190,8 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#if (licensePlan?.type !== Constants.PlanType.ENTERPRISE && $auth.user.accountPortalAccess) || !$admin.cloud}
|
|
||||||
|
{#if (!$licensing.isEnterprisePlan && $auth.user.accountPortalAccess) || !$admin.cloud}
|
||||||
<Button secondary on:click={$licensing.goToUpgradePage()}>
|
<Button secondary on:click={$licensing.goToUpgradePage()}>
|
||||||
Get more history
|
Get more history
|
||||||
</Button>
|
</Button>
|
||||||
|
@ -227,7 +227,7 @@
|
||||||
{#if selectedHistory}
|
{#if selectedHistory}
|
||||||
<Portal target="#side-panel">
|
<Portal target="#side-panel">
|
||||||
<HistoryDetailsPanel
|
<HistoryDetailsPanel
|
||||||
appId={app.devId}
|
appId={$store.appId}
|
||||||
bind:history={selectedHistory}
|
bind:history={selectedHistory}
|
||||||
close={sidePanel.close}
|
close={sidePanel.close}
|
||||||
/>
|
/>
|
|
@ -13,7 +13,8 @@
|
||||||
Tag,
|
Tag,
|
||||||
Table,
|
Table,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { backups, licensing, auth, admin, overview } from "stores/portal"
|
import { backups, licensing, auth, admin } from "stores/portal"
|
||||||
|
import { store } from "builderStore"
|
||||||
import { createPaginationStore } from "helpers/pagination"
|
import { createPaginationStore } from "helpers/pagination"
|
||||||
import TimeAgoRenderer from "./_components/TimeAgoRenderer.svelte"
|
import TimeAgoRenderer from "./_components/TimeAgoRenderer.svelte"
|
||||||
import AppSizeRenderer from "./_components/AppSizeRenderer.svelte"
|
import AppSizeRenderer from "./_components/AppSizeRenderer.svelte"
|
||||||
|
@ -50,7 +51,6 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
$: app = $overview.selectedApp
|
|
||||||
$: page = $pageInfo.page
|
$: page = $pageInfo.page
|
||||||
$: fetchBackups(filterOpt, page, startDate, endDate)
|
$: fetchBackups(filterOpt, page, startDate, endDate)
|
||||||
|
|
||||||
|
@ -101,7 +101,7 @@
|
||||||
|
|
||||||
async function fetchBackups(filters, page, startDate, endDate) {
|
async function fetchBackups(filters, page, startDate, endDate) {
|
||||||
const response = await backups.searchBackups({
|
const response = await backups.searchBackups({
|
||||||
appId: app.instance._id,
|
appId: $store.appId,
|
||||||
...filters,
|
...filters,
|
||||||
page,
|
page,
|
||||||
startDate,
|
startDate,
|
||||||
|
@ -117,7 +117,7 @@
|
||||||
try {
|
try {
|
||||||
loading = true
|
loading = true
|
||||||
let response = await backups.createManualBackup({
|
let response = await backups.createManualBackup({
|
||||||
appId: app.instance._id,
|
appId: $store.appId,
|
||||||
})
|
})
|
||||||
await fetchBackups(filterOpt, page)
|
await fetchBackups(filterOpt, page)
|
||||||
notifications.success(response.message)
|
notifications.success(response.message)
|
||||||
|
@ -143,20 +143,20 @@
|
||||||
async function handleButtonClick({ detail }) {
|
async function handleButtonClick({ detail }) {
|
||||||
if (detail.type === "backupDelete") {
|
if (detail.type === "backupDelete") {
|
||||||
await backups.deleteBackup({
|
await backups.deleteBackup({
|
||||||
appId: app.instance._id,
|
appId: $store.appId,
|
||||||
backupId: detail.backupId,
|
backupId: detail.backupId,
|
||||||
})
|
})
|
||||||
await fetchBackups(filterOpt, page)
|
await fetchBackups(filterOpt, page)
|
||||||
} else if (detail.type === "backupRestore") {
|
} else if (detail.type === "backupRestore") {
|
||||||
await backups.restoreBackup({
|
await backups.restoreBackup({
|
||||||
appId: app.instance._id,
|
appId: $store.appId,
|
||||||
backupId: detail.backupId,
|
backupId: detail.backupId,
|
||||||
name: detail.restoreBackupName,
|
name: detail.restoreBackupName,
|
||||||
})
|
})
|
||||||
await fetchBackups(filterOpt, page)
|
await fetchBackups(filterOpt, page)
|
||||||
} else if (detail.type === "backupUpdate") {
|
} else if (detail.type === "backupUpdate") {
|
||||||
await backups.updateBackup({
|
await backups.updateBackup({
|
||||||
appId: app.instance._id,
|
appId: $store.appId,
|
||||||
backupId: detail.backupId,
|
backupId: detail.backupId,
|
||||||
name: detail.name,
|
name: detail.name,
|
||||||
})
|
})
|
||||||
|
@ -333,10 +333,6 @@
|
||||||
gap: var(--spacing-m);
|
gap: var(--spacing-m);
|
||||||
}
|
}
|
||||||
|
|
||||||
.table {
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
.center {
|
.center {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: contents;
|
display: contents;
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Body,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
Button,
|
||||||
|
Helpers,
|
||||||
|
Icon,
|
||||||
|
notifications,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
|
||||||
|
$: filteredApps = $apps.filter(app => app.devId == $store.appId)
|
||||||
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
|
$: appUrl = `${window.origin}/embed${app?.url}`
|
||||||
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
|
$: embed = `<iframe width="800" height="600" frameborder="0" allow="clipboard-write;camera" src="${appUrl}"></iframe>`
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading>Embed</Heading>
|
||||||
|
<Body>Embed your app into your other tools of choice</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
<div class="embed-body">
|
||||||
|
<div class="embed-code">{embed}</div>
|
||||||
|
{#if appDeployed}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
cta
|
||||||
|
disabled={!appDeployed}
|
||||||
|
on:click={async () => {
|
||||||
|
await Helpers.copyToClipboard(embed)
|
||||||
|
notifications.success("Copied")
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Code
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="embed-info">
|
||||||
|
<Icon size="S" name="Info" /> Embeds will only work with a published app
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.embed-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-s);
|
||||||
|
}
|
||||||
|
.embed-body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spectrum-alias-grid-gutter-small);
|
||||||
|
}
|
||||||
|
.embed-code {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/* justify-content: center; */
|
||||||
|
background-color: var(
|
||||||
|
--spectrum-textfield-m-background-color,
|
||||||
|
var(--spectrum-global-color-gray-50)
|
||||||
|
);
|
||||||
|
border-radius: var(--border-radius-s);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,57 @@
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
Layout,
|
||||||
|
Body,
|
||||||
|
Heading,
|
||||||
|
Divider,
|
||||||
|
ActionButton,
|
||||||
|
Modal,
|
||||||
|
} from "@budibase/bbui"
|
||||||
|
import { AppStatus } from "constants"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
|
import { store } from "builderStore"
|
||||||
|
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
||||||
|
|
||||||
|
$: filteredApps = $apps.filter(app => app.devId == $store.appId)
|
||||||
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
|
let exportModal
|
||||||
|
let exportPublishedVersion = false
|
||||||
|
|
||||||
|
const exportApp = opts => {
|
||||||
|
exportPublishedVersion = !!opts?.published
|
||||||
|
exportModal.show()
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal bind:this={exportModal} padding={false}>
|
||||||
|
<ExportAppModal {app} published={exportPublishedVersion} />
|
||||||
|
</Modal>
|
||||||
|
|
||||||
|
<Layout noPadding>
|
||||||
|
<Layout gap="XS" noPadding>
|
||||||
|
<Heading>Export your app</Heading>
|
||||||
|
<Body>Export your latest edited or published app</Body>
|
||||||
|
</Layout>
|
||||||
|
<Divider />
|
||||||
|
<div class="export-body">
|
||||||
|
<ActionButton secondary on:click={() => exportApp({ published: false })}>
|
||||||
|
Export latest edited app
|
||||||
|
</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
secondary
|
||||||
|
disabled={!appDeployed}
|
||||||
|
on:click={() => exportApp({ published: true })}
|
||||||
|
>
|
||||||
|
Export latest published app
|
||||||
|
</ActionButton>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.export-body {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-l);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,4 +1,5 @@
|
||||||
<script>
|
<script>
|
||||||
import { redirect } from "@roxi/routify"
|
import { redirect } from "@roxi/routify"
|
||||||
$redirect("../")
|
|
||||||
|
$redirect("../settings/automation-history")
|
||||||
</script>
|
</script>
|
|
@ -10,14 +10,22 @@
|
||||||
Icon,
|
Icon,
|
||||||
} from "@budibase/bbui"
|
} from "@budibase/bbui"
|
||||||
import { AppStatus } from "constants"
|
import { AppStatus } from "constants"
|
||||||
import { overview } from "stores/portal"
|
import { store } from "builderStore"
|
||||||
|
import { apps } from "stores/portal"
|
||||||
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
import UpdateAppModal from "components/start/UpdateAppModal.svelte"
|
||||||
|
import { API } from "api"
|
||||||
|
|
||||||
let updatingModal
|
let updatingModal
|
||||||
|
|
||||||
$: app = $overview.selectedApp
|
$: filteredApps = $apps.filter(app => app.devId == $store.appId)
|
||||||
|
$: app = filteredApps.length ? filteredApps[0] : {}
|
||||||
$: appUrl = `${window.origin}/app${app?.url}`
|
$: appUrl = `${window.origin}/app${app?.url}`
|
||||||
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
$: appDeployed = app?.status === AppStatus.DEPLOYED
|
||||||
|
|
||||||
|
const initialiseApp = async () => {
|
||||||
|
const applicationPkg = await API.fetchAppPackage(app.devId)
|
||||||
|
await store.actions.initialise(applicationPkg)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Layout noPadding>
|
<Layout noPadding>
|
||||||
|
@ -66,7 +74,12 @@
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
<Modal bind:this={updatingModal} padding={false} width="600px">
|
<Modal bind:this={updatingModal} padding={false} width="600px">
|
||||||
<UpdateAppModal app={$overview.selectedApp} />
|
<UpdateAppModal
|
||||||
|
{app}
|
||||||
|
onUpdateComplete={async () => {
|
||||||
|
await initialiseApp()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<style>
|
<style>
|
|
@ -100,7 +100,9 @@
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
open: "error",
|
open: "error",
|
||||||
})
|
})
|
||||||
$goto(`../overview/${appId}/automation-history?${params.toString()}`)
|
$goto(
|
||||||
|
`/builder/app/${appId}/settings/automation-history?${params.toString()}`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorCount = errors => {
|
const errorCount = errors => {
|
||||||
|
|
|
@ -1,252 +0,0 @@
|
||||||
<script>
|
|
||||||
import { url, isActive, goto } from "@roxi/routify"
|
|
||||||
import {
|
|
||||||
Page,
|
|
||||||
Layout,
|
|
||||||
Button,
|
|
||||||
Icon,
|
|
||||||
ActionMenu,
|
|
||||||
MenuItem,
|
|
||||||
Helpers,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import {
|
|
||||||
Content,
|
|
||||||
SideNav,
|
|
||||||
SideNavItem,
|
|
||||||
Breadcrumbs,
|
|
||||||
Breadcrumb,
|
|
||||||
Header,
|
|
||||||
} from "components/portal/page"
|
|
||||||
import { apps, overview } from "stores/portal"
|
|
||||||
import { AppStatus } from "constants"
|
|
||||||
import analytics, { Events, EventSource } from "analytics"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import EditableIcon from "components/common/EditableIcon.svelte"
|
|
||||||
import { API } from "api"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import ExportAppModal from "components/start/ExportAppModal.svelte"
|
|
||||||
import { syncURLToState } from "helpers/urlStateSync"
|
|
||||||
import * as routify from "@roxi/routify"
|
|
||||||
import { onDestroy } from "svelte"
|
|
||||||
|
|
||||||
// Keep URL and state in sync for selected app ID
|
|
||||||
const stopSyncing = syncURLToState({
|
|
||||||
urlParam: "appId",
|
|
||||||
stateKey: "selectedAppId",
|
|
||||||
validate: id => $apps.some(app => app.devId === id),
|
|
||||||
fallbackUrl: "../../",
|
|
||||||
store: overview,
|
|
||||||
routify,
|
|
||||||
})
|
|
||||||
|
|
||||||
let exportModal
|
|
||||||
let deletionModal
|
|
||||||
let exportPublishedVersion = false
|
|
||||||
let deletionConfirmationAppName
|
|
||||||
let loaded = false
|
|
||||||
|
|
||||||
$: app = $overview.selectedApp
|
|
||||||
$: appId = $overview.selectedAppId
|
|
||||||
$: initialiseApp(appId)
|
|
||||||
$: isPublished = app?.status === AppStatus.DEPLOYED
|
|
||||||
|
|
||||||
const initialiseApp = async appId => {
|
|
||||||
loaded = false
|
|
||||||
try {
|
|
||||||
const pkg = await API.fetchAppPackage(appId)
|
|
||||||
await store.actions.initialise(pkg)
|
|
||||||
await API.syncApp(appId)
|
|
||||||
loaded = true
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error("Error initialising app overview")
|
|
||||||
$goto("../../")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const viewApp = () => {
|
|
||||||
if (isPublished) {
|
|
||||||
analytics.captureEvent(Events.APP_VIEW_PUBLISHED, {
|
|
||||||
appId: $store.appId,
|
|
||||||
eventSource: EventSource.PORTAL,
|
|
||||||
})
|
|
||||||
window.open(`/app${app?.url}`, "_blank")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const editApp = () => {
|
|
||||||
$goto(`../../../app/${app.devId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const exportApp = opts => {
|
|
||||||
exportPublishedVersion = !!opts?.published
|
|
||||||
exportModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyAppId = async () => {
|
|
||||||
await Helpers.copyToClipboard(app.prodId)
|
|
||||||
notifications.success("App ID copied to clipboard")
|
|
||||||
}
|
|
||||||
|
|
||||||
const deleteApp = async () => {
|
|
||||||
try {
|
|
||||||
await API.deleteApp(app?.devId)
|
|
||||||
apps.load()
|
|
||||||
notifications.success("App deleted successfully")
|
|
||||||
$goto("../../")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error("Error deleting app")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(() => {
|
|
||||||
stopSyncing()
|
|
||||||
store.actions.reset()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#key appId}
|
|
||||||
<Page>
|
|
||||||
<Layout noPadding gap="L">
|
|
||||||
<Breadcrumbs>
|
|
||||||
<Breadcrumb url={$url("../")} text="Apps" />
|
|
||||||
<Breadcrumb text={app?.name} />
|
|
||||||
</Breadcrumbs>
|
|
||||||
<Header title={app?.name} wrap={false}>
|
|
||||||
<div slot="icon">
|
|
||||||
<EditableIcon
|
|
||||||
{app}
|
|
||||||
autoSave
|
|
||||||
size="XL"
|
|
||||||
name={app?.icon?.name || "Apps"}
|
|
||||||
color={app?.icon?.color}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div slot="buttons">
|
|
||||||
<span class="desktop">
|
|
||||||
<Button
|
|
||||||
size="M"
|
|
||||||
quiet
|
|
||||||
secondary
|
|
||||||
disabled={!isPublished}
|
|
||||||
on:click={viewApp}
|
|
||||||
>
|
|
||||||
View
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
<span class="desktop">
|
|
||||||
<Button size="M" cta on:click={editApp}>Edit</Button>
|
|
||||||
</span>
|
|
||||||
<ActionMenu align="right">
|
|
||||||
<span slot="control" class="app-overview-actions-icon">
|
|
||||||
<Icon hoverable name="More" />
|
|
||||||
</span>
|
|
||||||
<span class="mobile">
|
|
||||||
<MenuItem icon="Globe" disabled={!isPublished} on:click={viewApp}>
|
|
||||||
View
|
|
||||||
</MenuItem>
|
|
||||||
</span>
|
|
||||||
<span class="mobile">
|
|
||||||
<MenuItem icon="Edit" on:click={editApp}>Edit</MenuItem>
|
|
||||||
</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} icon="Copy">Copy app ID</MenuItem>
|
|
||||||
{/if}
|
|
||||||
{#if !isPublished}
|
|
||||||
<MenuItem on:click={deletionModal.show} icon="Delete">
|
|
||||||
Delete
|
|
||||||
</MenuItem>
|
|
||||||
{/if}
|
|
||||||
</ActionMenu>
|
|
||||||
</div>
|
|
||||||
</Header>
|
|
||||||
<Content showMobileNav>
|
|
||||||
<SideNav slot="side-nav">
|
|
||||||
<SideNavItem
|
|
||||||
text="Overview"
|
|
||||||
url={$url("./overview")}
|
|
||||||
active={$isActive("./overview")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Access"
|
|
||||||
url={$url("./access")}
|
|
||||||
active={$isActive("./access")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Automation History"
|
|
||||||
url={$url("./automation-history")}
|
|
||||||
active={$isActive("./automation-history")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Backups"
|
|
||||||
url={$url("./backups")}
|
|
||||||
active={$isActive("./backups")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Name and URL"
|
|
||||||
url={$url("./name-and-url")}
|
|
||||||
active={$isActive("./name-and-url")}
|
|
||||||
/>
|
|
||||||
<SideNavItem
|
|
||||||
text="Version"
|
|
||||||
url={$url("./version")}
|
|
||||||
active={$isActive("./version")}
|
|
||||||
/>
|
|
||||||
</SideNav>
|
|
||||||
{#if loaded}
|
|
||||||
<slot />
|
|
||||||
{/if}
|
|
||||||
</Content>
|
|
||||||
</Layout>
|
|
||||||
</Page>
|
|
||||||
|
|
||||||
<Modal bind:this={exportModal} padding={false}>
|
|
||||||
<ExportAppModal {app} published={exportPublishedVersion} />
|
|
||||||
</Modal>
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={deletionModal}
|
|
||||||
title="Delete app"
|
|
||||||
okText="Delete"
|
|
||||||
onOk={deleteApp}
|
|
||||||
onCancel={() => (deletionConfirmationAppName = null)}
|
|
||||||
disabled={deletionConfirmationAppName !== app?.name}
|
|
||||||
>
|
|
||||||
Are you sure you want to delete <b>{app?.name}</b>?
|
|
||||||
<br />
|
|
||||||
Please enter the app name below to confirm.
|
|
||||||
<br /><br />
|
|
||||||
<Input bind:value={deletionConfirmationAppName} placeholder={app?.name} />
|
|
||||||
</ConfirmDialog>
|
|
||||||
{/key}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.desktop {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
.mobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 640px) {
|
|
||||||
.desktop {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
.mobile {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,211 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
ModalContent,
|
|
||||||
PickerDropdown,
|
|
||||||
ActionButton,
|
|
||||||
Layout,
|
|
||||||
Icon,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { roles } from "stores/backend"
|
|
||||||
import { groups, users, licensing, apps } from "stores/portal"
|
|
||||||
import { Constants, RoleUtils, fetchData } from "@budibase/frontend-core"
|
|
||||||
import { API } from "api"
|
|
||||||
import { createEventDispatcher } from "svelte"
|
|
||||||
|
|
||||||
export let app
|
|
||||||
export let appUsers = []
|
|
||||||
export let showUsers = false
|
|
||||||
export let showGroups = false
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher()
|
|
||||||
const usersFetch = fetchData({
|
|
||||||
API,
|
|
||||||
datasource: {
|
|
||||||
type: "user",
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
query: {
|
|
||||||
email: "",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
let search = ""
|
|
||||||
let data = [{ id: "", role: "" }]
|
|
||||||
|
|
||||||
$: usersFetch.update({
|
|
||||||
query: {
|
|
||||||
email: search,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
$: fixedAppId = apps.getProdAppID(app.devId)
|
|
||||||
$: availableUsers = getAvailableUsers($usersFetch.rows, appUsers, data)
|
|
||||||
$: availableGroups = getAvailableGroups($groups, app.appId, search, data)
|
|
||||||
$: valid = data?.length && !data?.some(x => !x.id?.length || !x.role?.length)
|
|
||||||
$: optionSections = {
|
|
||||||
...(showGroups &&
|
|
||||||
$licensing.groupsEnabled &&
|
|
||||||
availableGroups.length && {
|
|
||||||
["User groups"]: {
|
|
||||||
data: availableGroups,
|
|
||||||
getLabel: group => group.name,
|
|
||||||
getValue: group => group._id,
|
|
||||||
getIcon: group => group.icon,
|
|
||||||
getColour: group => group.color,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
...(showUsers && {
|
|
||||||
users: {
|
|
||||||
data: availableUsers,
|
|
||||||
getLabel: user => user.email,
|
|
||||||
getValue: user => user._id,
|
|
||||||
getIcon: user => user.icon,
|
|
||||||
getColour: user => user.color,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
|
|
||||||
const addData = async appData => {
|
|
||||||
const gr_prefix = "gr"
|
|
||||||
const us_prefix = "us"
|
|
||||||
for (let data of appData) {
|
|
||||||
// Assign group
|
|
||||||
if (data.id.startsWith(gr_prefix)) {
|
|
||||||
const group = $groups.find(group => {
|
|
||||||
return group._id === data.id
|
|
||||||
})
|
|
||||||
if (!group) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
await groups.actions.addApp(group._id, fixedAppId, data.role)
|
|
||||||
}
|
|
||||||
// Assign user
|
|
||||||
else if (data.id.startsWith(us_prefix)) {
|
|
||||||
const user = await users.get(data.id)
|
|
||||||
await users.save({
|
|
||||||
...user,
|
|
||||||
roles: {
|
|
||||||
...user.roles,
|
|
||||||
[fixedAppId]: data.role,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh data when completed
|
|
||||||
await usersFetch.refresh()
|
|
||||||
dispatch("update")
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAvailableUsers = (allUsers, appUsers, newUsers) => {
|
|
||||||
return (allUsers || []).filter(user => {
|
|
||||||
// Filter out admin users
|
|
||||||
if (user?.admin?.global || user?.builder?.global) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out assigned users
|
|
||||||
if (appUsers.find(x => x._id === user._id)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out new users which are going to be assigned
|
|
||||||
return !newUsers.find(x => x.id === user._id)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAvailableGroups = (allGroups, appId, search, newGroups) => {
|
|
||||||
search = search?.toLowerCase()
|
|
||||||
return (allGroups || []).filter(group => {
|
|
||||||
// Filter out assigned groups
|
|
||||||
const appIds = groups.actions.getGroupAppIds(group)
|
|
||||||
if (appIds.includes(apps.getProdAppID(appId))) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out new groups which are going to be assigned
|
|
||||||
if (newGroups.find(x => x.id === group._id)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Match search string
|
|
||||||
return !search || group.name.toLowerCase().includes(search)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function addNewInput() {
|
|
||||||
data = [...data, { id: "", role: "" }]
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeItem = index => {
|
|
||||||
data = data.filter((x, idx) => idx !== index)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ModalContent
|
|
||||||
size="M"
|
|
||||||
title="Assign access to your app"
|
|
||||||
confirmText="Done"
|
|
||||||
cancelText="Cancel"
|
|
||||||
onConfirm={() => addData(data)}
|
|
||||||
showCloseIcon={false}
|
|
||||||
disabled={!valid}
|
|
||||||
>
|
|
||||||
{#if data.length}
|
|
||||||
<Layout noPadding gap="XS">
|
|
||||||
{#each data as input, index}
|
|
||||||
<div class="item">
|
|
||||||
<div class="picker">
|
|
||||||
<PickerDropdown
|
|
||||||
autocomplete
|
|
||||||
showClearIcon={false}
|
|
||||||
primaryOptions={optionSections}
|
|
||||||
secondaryOptions={$roles.filter(
|
|
||||||
x => x._id !== Constants.Roles.PUBLIC
|
|
||||||
)}
|
|
||||||
secondaryPlaceholder="Access"
|
|
||||||
bind:primaryValue={input.id}
|
|
||||||
bind:secondaryValue={input.role}
|
|
||||||
bind:searchTerm={search}
|
|
||||||
getPrimaryOptionLabel={group => group.name}
|
|
||||||
getPrimaryOptionValue={group => group.name}
|
|
||||||
getPrimaryOptionIcon={group => group.icon}
|
|
||||||
getPrimaryOptionColour={group => group.colour}
|
|
||||||
getSecondaryOptionLabel={role => role.name}
|
|
||||||
getSecondaryOptionValue={role => role._id}
|
|
||||||
getSecondaryOptionColour={role =>
|
|
||||||
RoleUtils.getRoleColour(role._id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="icon">
|
|
||||||
<Icon
|
|
||||||
name="Close"
|
|
||||||
hoverable
|
|
||||||
size="S"
|
|
||||||
on:click={() => removeItem(index)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/each}
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
<div>
|
|
||||||
<ActionButton on:click={addNewInput} icon="Add">Add more</ActionButton>
|
|
||||||
</div>
|
|
||||||
</ModalContent>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.item {
|
|
||||||
position: relative;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
.picker {
|
|
||||||
width: calc(100% - 30px);
|
|
||||||
}
|
|
||||||
.icon {
|
|
||||||
width: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,26 +0,0 @@
|
||||||
<script>
|
|
||||||
import RoleSelect from "components/common/RoleSelect.svelte"
|
|
||||||
import { getContext } from "svelte"
|
|
||||||
|
|
||||||
const rolesContext = getContext("roles")
|
|
||||||
|
|
||||||
export let value
|
|
||||||
export let row
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<RoleSelect
|
|
||||||
{value}
|
|
||||||
quiet
|
|
||||||
allowRemove
|
|
||||||
allowPublic={false}
|
|
||||||
on:change={e => rolesContext.updateRole(e.detail, row._id)}
|
|
||||||
on:remove={() => rolesContext.removeRole(row._id)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
div {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,271 +0,0 @@
|
||||||
<script>
|
|
||||||
import {
|
|
||||||
Layout,
|
|
||||||
Heading,
|
|
||||||
Body,
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
notifications,
|
|
||||||
Pagination,
|
|
||||||
Divider,
|
|
||||||
Table,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { onMount, setContext } from "svelte"
|
|
||||||
import { users, groups, apps, licensing, overview } from "stores/portal"
|
|
||||||
import AssignmentModal from "./_components/AssignmentModal.svelte"
|
|
||||||
import { roles } from "stores/backend"
|
|
||||||
import { API } from "api"
|
|
||||||
import { fetchData } from "@budibase/frontend-core"
|
|
||||||
import EditableRoleRenderer from "./_components/EditableRoleRenderer.svelte"
|
|
||||||
|
|
||||||
const userSchema = {
|
|
||||||
email: {
|
|
||||||
type: "string",
|
|
||||||
width: "1fr",
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
displayName: "Access",
|
|
||||||
width: "150px",
|
|
||||||
borderLeft: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const groupSchema = {
|
|
||||||
name: {
|
|
||||||
type: "string",
|
|
||||||
width: "1fr",
|
|
||||||
},
|
|
||||||
role: {
|
|
||||||
displayName: "Access",
|
|
||||||
width: "150px",
|
|
||||||
borderLeft: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
const customRenderers = [
|
|
||||||
{
|
|
||||||
column: "role",
|
|
||||||
component: EditableRoleRenderer,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
let assignmentModal
|
|
||||||
let appGroups
|
|
||||||
let appUsers
|
|
||||||
let showAddUsers = false
|
|
||||||
let showAddGroups = false
|
|
||||||
|
|
||||||
$: app = $overview.selectedApp
|
|
||||||
$: devAppId = app.devId
|
|
||||||
$: prodAppId = apps.getProdAppID(app.devId)
|
|
||||||
$: usersFetch = fetchData({
|
|
||||||
API,
|
|
||||||
datasource: {
|
|
||||||
type: "user",
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
query: {
|
|
||||||
appId: apps.getProdAppID(devAppId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
$: appUsers = getAppUsers($usersFetch.rows, prodAppId)
|
|
||||||
$: appGroups = getAppGroups($groups, prodAppId)
|
|
||||||
|
|
||||||
const getAppUsers = (users, appId) => {
|
|
||||||
return users.map(user => ({
|
|
||||||
...user,
|
|
||||||
role: user.roles[Object.keys(user.roles).find(x => x === appId)],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAppGroups = (allGroups, appId) => {
|
|
||||||
return allGroups
|
|
||||||
.filter(group => {
|
|
||||||
if (!group.roles) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return groups.actions.getGroupAppIds(group).includes(appId)
|
|
||||||
})
|
|
||||||
.map(group => ({
|
|
||||||
...group,
|
|
||||||
role: group.roles[
|
|
||||||
groups.actions.getGroupAppIds(group).find(x => x === appId)
|
|
||||||
],
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateRole = async (role, id) => {
|
|
||||||
// Check if this is a user or a group
|
|
||||||
if ($usersFetch.rows.some(user => user._id === id)) {
|
|
||||||
await updateUserRole(role, id)
|
|
||||||
} else {
|
|
||||||
await updateGroupRole(role, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeRole = async id => {
|
|
||||||
// Check if this is a user or a group
|
|
||||||
if ($usersFetch.rows.some(user => user._id === id)) {
|
|
||||||
await removeUserRole(id)
|
|
||||||
} else {
|
|
||||||
await removeGroupRole(id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateUserRole = async (role, userId) => {
|
|
||||||
const user = $usersFetch.rows.find(user => user._id === userId)
|
|
||||||
if (!user) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
user.roles[prodAppId] = role
|
|
||||||
await users.save(user)
|
|
||||||
await usersFetch.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeUserRole = async userId => {
|
|
||||||
const user = $usersFetch.rows.find(user => user._id === userId)
|
|
||||||
if (!user) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const filteredRoles = { ...user.roles }
|
|
||||||
delete filteredRoles[prodAppId]
|
|
||||||
await users.save({
|
|
||||||
...user,
|
|
||||||
roles: {
|
|
||||||
...filteredRoles,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
await usersFetch.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateGroupRole = async (role, groupId) => {
|
|
||||||
const group = $groups.find(group => group._id === groupId)
|
|
||||||
if (!group) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await groups.actions.addApp(group._id, prodAppId, role)
|
|
||||||
await usersFetch.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeGroupRole = async groupId => {
|
|
||||||
const group = $groups.find(group => group._id === groupId)
|
|
||||||
if (!group) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
await groups.actions.removeApp(group._id, prodAppId)
|
|
||||||
await usersFetch.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showUsersModal = () => {
|
|
||||||
showAddUsers = true
|
|
||||||
showAddGroups = false
|
|
||||||
assignmentModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
const showGroupsModal = () => {
|
|
||||||
showAddUsers = false
|
|
||||||
showAddGroups = true
|
|
||||||
assignmentModal.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
setContext("roles", {
|
|
||||||
updateRole,
|
|
||||||
removeRole,
|
|
||||||
})
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
await roles.fetch()
|
|
||||||
} catch (error) {
|
|
||||||
notifications.error(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Layout noPadding>
|
|
||||||
<Layout gap="XS" noPadding>
|
|
||||||
<Heading>Access</Heading>
|
|
||||||
<Body>Assign users to your app and set their access</Body>
|
|
||||||
</Layout>
|
|
||||||
<Divider />
|
|
||||||
<Layout noPadding gap="L">
|
|
||||||
{#if $usersFetch.loaded}
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div class="title">
|
|
||||||
<Heading size="S">Users</Heading>
|
|
||||||
<Button cta on:click={showUsersModal}>Assign user</Button>
|
|
||||||
</div>
|
|
||||||
<Table
|
|
||||||
customPlaceholder
|
|
||||||
data={appUsers}
|
|
||||||
schema={userSchema}
|
|
||||||
allowEditRows={false}
|
|
||||||
{customRenderers}
|
|
||||||
>
|
|
||||||
<div class="placeholder" slot="placeholder">
|
|
||||||
<Heading size="S">You have no users assigned yet</Heading>
|
|
||||||
</div>
|
|
||||||
</Table>
|
|
||||||
<div class="pagination">
|
|
||||||
<Pagination
|
|
||||||
page={$usersFetch.pageNumber + 1}
|
|
||||||
hasPrevPage={$usersFetch.hasPrevPage}
|
|
||||||
hasNextPage={$usersFetch.hasNextPage}
|
|
||||||
goToPrevPage={$usersFetch.loading ? null : usersFetch.prevPage}
|
|
||||||
goToNextPage={$usersFetch.loading ? null : usersFetch.nextPage}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $usersFetch.loaded && $licensing.groupsEnabled}
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div class="title">
|
|
||||||
<Heading size="S">Groups</Heading>
|
|
||||||
<Button cta on:click={showGroupsModal}>Assign group</Button>
|
|
||||||
</div>
|
|
||||||
<Table
|
|
||||||
customPlaceholder
|
|
||||||
data={appGroups}
|
|
||||||
schema={groupSchema}
|
|
||||||
allowEditRows={false}
|
|
||||||
{customRenderers}
|
|
||||||
>
|
|
||||||
<div class="placeholder" slot="placeholder">
|
|
||||||
<Heading size="S">You have no groups assigned yet</Heading>
|
|
||||||
</div>
|
|
||||||
</Table>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</Layout>
|
|
||||||
|
|
||||||
<Modal bind:this={assignmentModal}>
|
|
||||||
<AssignmentModal
|
|
||||||
{app}
|
|
||||||
{appUsers}
|
|
||||||
on:update={usersFetch.refresh}
|
|
||||||
showGroups={showAddGroups}
|
|
||||||
showUsers={showAddUsers}
|
|
||||||
/>
|
|
||||||
</Modal>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.title {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-end;
|
|
||||||
}
|
|
||||||
.placeholder {
|
|
||||||
flex: 1 1 auto;
|
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.pagination {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-end;
|
|
||||||
margin-top: calc(-1 * var(--spacing-s));
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,4 +0,0 @@
|
||||||
<script>
|
|
||||||
import { redirect } from "@roxi/routify"
|
|
||||||
$redirect("./overview")
|
|
||||||
</script>
|
|
|
@ -1,368 +0,0 @@
|
||||||
<script>
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
import DashCard from "components/common/DashCard.svelte"
|
|
||||||
import { AppStatus } from "constants"
|
|
||||||
import { goto } from "@roxi/routify"
|
|
||||||
import {
|
|
||||||
Icon,
|
|
||||||
Heading,
|
|
||||||
Link,
|
|
||||||
Layout,
|
|
||||||
Body,
|
|
||||||
notifications,
|
|
||||||
} from "@budibase/bbui"
|
|
||||||
import { store } from "builderStore"
|
|
||||||
import { processStringSync } from "@budibase/string-templates"
|
|
||||||
import { users, auth, apps, groups, overview } from "stores/portal"
|
|
||||||
import { fetchData, UserAvatar } from "@budibase/frontend-core"
|
|
||||||
import { API } from "api"
|
|
||||||
import GroupIcon from "../../users/groups/_components/GroupIcon.svelte"
|
|
||||||
import ConfirmDialog from "components/common/ConfirmDialog.svelte"
|
|
||||||
import { checkIncomingDeploymentStatus } from "components/deploy/utils"
|
|
||||||
|
|
||||||
let appEditor
|
|
||||||
let unpublishModal
|
|
||||||
let deployments
|
|
||||||
|
|
||||||
$: app = $overview.selectedApp
|
|
||||||
$: devAppId = app.devId
|
|
||||||
$: prodAppId = apps.getProdAppID(devAppId)
|
|
||||||
$: appUsersFetch = fetchData({
|
|
||||||
API,
|
|
||||||
datasource: {
|
|
||||||
type: "user",
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
query: {
|
|
||||||
appId: apps.getProdAppID(devAppId),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
$: updateAvailable = $store.upgradableVersion !== $store.version
|
|
||||||
$: isPublished = app?.status === AppStatus.DEPLOYED
|
|
||||||
$: appEditorId = !app?.updatedBy ? $auth.user._id : app?.updatedBy
|
|
||||||
$: appEditorText = appEditor?.firstName || appEditor?.email
|
|
||||||
$: fetchAppEditor(appEditorId)
|
|
||||||
$: appUsers = $appUsersFetch.rows || []
|
|
||||||
$: appGroups = $groups.filter(group => {
|
|
||||||
if (!group.roles) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return groups.actions.getGroupAppIds(group).includes(prodAppId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateDeploymentString = () => {
|
|
||||||
return deployments?.length
|
|
||||||
? processStringSync(
|
|
||||||
"Last published {{ duration time 'millisecond' }} ago",
|
|
||||||
{
|
|
||||||
time:
|
|
||||||
new Date().getTime() -
|
|
||||||
new Date(deployments[0].updatedAt).getTime(),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
// App is updating in the layout asynchronously
|
|
||||||
$: if ($store.appId?.length) {
|
|
||||||
fetchDeployments().then(resp => {
|
|
||||||
deployments = resp
|
|
||||||
})
|
|
||||||
}
|
|
||||||
$: deploymentString = updateDeploymentString(deployments)
|
|
||||||
|
|
||||||
async function fetchAppEditor(editorId) {
|
|
||||||
appEditor = await users.get(editorId)
|
|
||||||
}
|
|
||||||
|
|
||||||
const confirmUnpublishApp = async () => {
|
|
||||||
try {
|
|
||||||
await API.unpublishApp(app.prodId)
|
|
||||||
await apps.load()
|
|
||||||
notifications.success("App unpublished successfully")
|
|
||||||
} catch (err) {
|
|
||||||
notifications.error("Error unpublishing app")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
|
||||||
console.log(err)
|
|
||||||
notifications.error("Error fetching deployment history")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
deployments = await fetchDeployments()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="overview-tab">
|
|
||||||
<Layout noPadding gap="XL">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="status-text">
|
|
||||||
{#if isPublished}
|
|
||||||
{deploymentString}
|
|
||||||
- <Link on:click={unpublishModal.show}>Unpublish</Link>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if !deployments?.length}
|
|
||||||
-
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DashCard>
|
|
||||||
{#if appEditor}
|
|
||||||
<DashCard title={"Last Edited"}>
|
|
||||||
<div class="last-edited-content">
|
|
||||||
<div class="updated-by">
|
|
||||||
{#if appEditor}
|
|
||||||
<UserAvatar user={appEditor} showTooltip={false} />
|
|
||||||
<div class="editor-name">
|
|
||||||
{appEditor._id === $auth.user._id ? "You" : appEditorText}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
<div class="last-edit-text">
|
|
||||||
{#if app}
|
|
||||||
{processStringSync(
|
|
||||||
"Last edited {{ duration time 'millisecond' }} ago",
|
|
||||||
{
|
|
||||||
time:
|
|
||||||
new Date().getTime() - new Date(app?.updatedAt).getTime(),
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DashCard>
|
|
||||||
{/if}
|
|
||||||
<DashCard
|
|
||||||
title={"Version"}
|
|
||||||
showIcon={true}
|
|
||||||
action={() => {
|
|
||||||
$goto("./version")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="version-content">
|
|
||||||
<Heading size="XS">{$store.version}</Heading>
|
|
||||||
{#if updateAvailable}
|
|
||||||
<div class="version-status">
|
|
||||||
New version <strong>{$store.upgradableVersion}</strong> is
|
|
||||||
available -
|
|
||||||
<Link
|
|
||||||
on:click={() => {
|
|
||||||
$goto("./version")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Update
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<div class="version-status">You're running the latest!</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</DashCard>
|
|
||||||
{#if $appUsersFetch.loaded}
|
|
||||||
<DashCard
|
|
||||||
title={"Access"}
|
|
||||||
showIcon={true}
|
|
||||||
action={() => {
|
|
||||||
$goto("./access")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{#if appUsers.length || appGroups.length}
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<div class="access-tab-content">
|
|
||||||
{#if appUsers.length}
|
|
||||||
<div class="users">
|
|
||||||
<div class="list">
|
|
||||||
{#each appUsers.slice(0, 4) as user}
|
|
||||||
<UserAvatar {user} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div class="text">
|
|
||||||
{appUsers.length}
|
|
||||||
{appUsers.length > 1 ? "users" : "user"} assigned
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{#if appGroups.length}
|
|
||||||
<div class="groups">
|
|
||||||
<div class="list">
|
|
||||||
{#each appGroups.slice(0, 4) as group}
|
|
||||||
<GroupIcon {group} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
<div class="text">
|
|
||||||
{appGroups.length} user
|
|
||||||
{appGroups.length > 1 ? "groups" : "group"} assigned
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
{:else}
|
|
||||||
<Layout noPadding gap="S">
|
|
||||||
<Body>No users</Body>
|
|
||||||
<div class="users-text">
|
|
||||||
No users have been assigned to this app
|
|
||||||
</div>
|
|
||||||
</Layout>
|
|
||||||
{/if}
|
|
||||||
</DashCard>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
{#if false}
|
|
||||||
<div class="bottom">
|
|
||||||
<DashCard
|
|
||||||
title={"Automation History"}
|
|
||||||
action={() => {
|
|
||||||
$goto("../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"}
|
|
||||||
action={() => {
|
|
||||||
$goto("../backups")
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div class="backups-content">test</div>
|
|
||||||
</DashCard>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</Layout>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ConfirmDialog
|
|
||||||
bind:this={unpublishModal}
|
|
||||||
title="Confirm unpublish"
|
|
||||||
okText="Unpublish app"
|
|
||||||
onOk={confirmUnpublishApp}
|
|
||||||
>
|
|
||||||
Are you sure you want to unpublish the app <b>{app?.name}</b>?
|
|
||||||
</ConfirmDialog>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.overview-tab {
|
|
||||||
display: grid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-tab .top {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spectrum-alias-grid-gutter-medium);
|
|
||||||
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.access-tab-content {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: flex-start;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: var(--spacing-xl);
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.access-tab-content > * {
|
|
||||||
flex: 1 1 0;
|
|
||||||
}
|
|
||||||
.access-tab-content .list {
|
|
||||||
display: flex;
|
|
||||||
gap: 4px;
|
|
||||||
}
|
|
||||||
.access-tab-content .text {
|
|
||||||
color: var(--spectrum-global-color-gray-600);
|
|
||||||
margin-top: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
|
|
||||||
.overview-tab .bottom,
|
|
||||||
.automation-metrics {
|
|
||||||
display: grid;
|
|
||||||
grid-gap: var(--spectrum-alias-grid-gutter-large);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.version-status,
|
|
||||||
.last-edit-text,
|
|
||||||
.status-text {
|
|
||||||
padding-top: var(--spacing-xl);
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,15 +0,0 @@
|
||||||
<script>
|
|
||||||
import { groups } from "stores/portal"
|
|
||||||
import { onMount } from "svelte"
|
|
||||||
|
|
||||||
let loaded = false
|
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
await groups.actions.init()
|
|
||||||
loaded = true
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if loaded}
|
|
||||||
<slot />
|
|
||||||
{/if}
|
|
|
@ -277,7 +277,6 @@
|
||||||
allowClear={true}
|
allowClear={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if !isCloud}
|
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<Label size="L">Title</Label>
|
<Label size="L">Title</Label>
|
||||||
<Input
|
<Input
|
||||||
|
@ -290,7 +289,6 @@
|
||||||
disabled={!brandingEnabled || saving}
|
disabled={!brandingEnabled || saving}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
<div>
|
<div>
|
||||||
<Toggle
|
<Toggle
|
||||||
text={"Remove Budibase brand from emails"}
|
text={"Remove Budibase brand from emails"}
|
||||||
|
@ -305,7 +303,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !isCloud}
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">Login page</Heading>
|
<Heading size="S">Login page</Heading>
|
||||||
|
@ -352,7 +349,6 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
<Divider />
|
<Divider />
|
||||||
<Layout gap="XS" noPadding>
|
<Layout gap="XS" noPadding>
|
||||||
<Heading size="S">Application previews</Heading>
|
<Heading size="S">Application previews</Heading>
|
||||||
|
|
|
@ -141,7 +141,7 @@
|
||||||
customPlaceholder
|
customPlaceholder
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
customRenderers={customAppTableRenderers}
|
customRenderers={customAppTableRenderers}
|
||||||
on:click={e => $goto(`../../overview/${e.detail.devId}`)}
|
on:click={e => $goto(`/builder/app/${e.detail.devId}`)}
|
||||||
>
|
>
|
||||||
<div class="placeholder" slot="placeholder">
|
<div class="placeholder" slot="placeholder">
|
||||||
<Heading size="S">This group doesn't have access to any apps</Heading>
|
<Heading size="S">This group doesn't have access to any apps</Heading>
|
||||||
|
|
|
@ -346,7 +346,7 @@
|
||||||
customPlaceholder
|
customPlaceholder
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
customRenderers={customAppTableRenderers}
|
customRenderers={customAppTableRenderers}
|
||||||
on:click={e => $goto(`../../overview/${e.detail.devId}`)}
|
on:click={e => $goto(`/builder/app/${e.detail.devId}`)}
|
||||||
>
|
>
|
||||||
<div class="placeholder" slot="placeholder">
|
<div class="placeholder" slot="placeholder">
|
||||||
<Heading size="S">
|
<Heading size="S">
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
import PasswordCopyTableRenderer from "./PasswordCopyTableRenderer.svelte"
|
import PasswordCopyTableRenderer from "./PasswordCopyTableRenderer.svelte"
|
||||||
import { parseToCsv } from "helpers/data/utils"
|
import { parseToCsv } from "helpers/data/utils"
|
||||||
import { onMount } from "svelte"
|
import { onMount } from "svelte"
|
||||||
|
import InviteResponseRenderer from "./InviteResponseRenderer.svelte"
|
||||||
|
|
||||||
export let userData
|
export let userData
|
||||||
export let createUsersResponse
|
export let createUsersResponse
|
||||||
|
@ -96,7 +97,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ModalContent
|
<ModalContent
|
||||||
size="M"
|
size="L"
|
||||||
{title}
|
{title}
|
||||||
confirmText="Done"
|
confirmText="Done"
|
||||||
showCancelButton={false}
|
showCancelButton={false}
|
||||||
|
@ -113,6 +114,9 @@
|
||||||
allowEditColumns={false}
|
allowEditColumns={false}
|
||||||
allowEditRows={false}
|
allowEditRows={false}
|
||||||
allowSelectRows={false}
|
allowSelectRows={false}
|
||||||
|
customRenderers={[
|
||||||
|
{ column: "reason", component: InviteResponseRenderer },
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if hasSuccess}
|
{#if hasSuccess}
|
||||||
|
|
|
@ -10,7 +10,6 @@ export { licensing } from "./licensing"
|
||||||
export { groups } from "./groups"
|
export { groups } from "./groups"
|
||||||
export { plugins } from "./plugins"
|
export { plugins } from "./plugins"
|
||||||
export { backups } from "./backups"
|
export { backups } from "./backups"
|
||||||
export { overview } from "./overview"
|
|
||||||
export { environment } from "./environment"
|
export { environment } from "./environment"
|
||||||
export { menu } from "./menu"
|
export { menu } from "./menu"
|
||||||
export { auditLogs } from "./auditLogs"
|
export { auditLogs } from "./auditLogs"
|
||||||
|
|
|
@ -76,7 +76,13 @@ export const createLicensingStore = () => {
|
||||||
await actions.setQuotaUsage()
|
await actions.setQuotaUsage()
|
||||||
},
|
},
|
||||||
setNavigation: () => {
|
setNavigation: () => {
|
||||||
const upgradeUrl = `${get(admin).accountPortalUrl}/portal/upgrade`
|
const adminStore = get(admin)
|
||||||
|
const authStore = get(auth)
|
||||||
|
|
||||||
|
const upgradeUrl = authStore?.user?.accountPortalAccess
|
||||||
|
? `${adminStore.accountPortalUrl}/portal/upgrade`
|
||||||
|
: "/builder/portal/account/upgrade"
|
||||||
|
|
||||||
const goToUpgradePage = () => {
|
const goToUpgradePage = () => {
|
||||||
window.location.href = upgradeUrl
|
window.location.href = upgradeUrl
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,7 @@
|
||||||
|
|
||||||
let dataLoaded = false
|
let dataLoaded = false
|
||||||
let permissionError = false
|
let permissionError = false
|
||||||
|
let embedNoScreens = false
|
||||||
|
|
||||||
// Determine if we should show devtools or not
|
// Determine if we should show devtools or not
|
||||||
$: showDevTools = $devToolsEnabled && !$routeStore.queryParams?.peek
|
$: showDevTools = $devToolsEnabled && !$routeStore.queryParams?.peek
|
||||||
|
@ -68,6 +69,8 @@
|
||||||
// If the user is logged in but has no screens, they don't have
|
// If the user is logged in but has no screens, they don't have
|
||||||
// permission to use the app
|
// permission to use the app
|
||||||
permissionError = true
|
permissionError = true
|
||||||
|
} else if ($appStore.embedded) {
|
||||||
|
embedNoScreens = true
|
||||||
} else {
|
} else {
|
||||||
// If they have no screens and are not logged in, it probably means
|
// If they have no screens and are not logged in, it probably means
|
||||||
// they should log in to gain access
|
// they should log in to gain access
|
||||||
|
@ -86,7 +89,9 @@
|
||||||
if (get(builderStore).inBuilder) {
|
if (get(builderStore).inBuilder) {
|
||||||
builderStore.actions.notifyLoaded()
|
builderStore.actions.notifyLoaded()
|
||||||
} else {
|
} else {
|
||||||
builderStore.actions.analyticsPing({ source: "app" })
|
builderStore.actions.analyticsPing({
|
||||||
|
embedded: !!$appStore.embedded,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -158,6 +163,15 @@
|
||||||
</Body>
|
</Body>
|
||||||
</Layout>
|
</Layout>
|
||||||
</div>
|
</div>
|
||||||
|
{:else if embedNoScreens}
|
||||||
|
<div class="error">
|
||||||
|
<Layout justifyItems="center" gap="S">
|
||||||
|
{@html ErrorSVG}
|
||||||
|
<Heading size="L">
|
||||||
|
This Budibase app is not publicly accessible
|
||||||
|
</Heading>
|
||||||
|
</Layout>
|
||||||
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<CustomThemeWrapper>
|
<CustomThemeWrapper>
|
||||||
{#key $screenStore.activeLayout._id}
|
{#key $screenStore.activeLayout._id}
|
||||||
|
|
|
@ -34,6 +34,8 @@
|
||||||
export let navWidth
|
export let navWidth
|
||||||
export let pageWidth
|
export let pageWidth
|
||||||
|
|
||||||
|
export let embedded = false
|
||||||
|
|
||||||
const NavigationClasses = {
|
const NavigationClasses = {
|
||||||
Top: "top",
|
Top: "top",
|
||||||
Left: "left",
|
Left: "left",
|
||||||
|
@ -186,9 +188,11 @@
|
||||||
<Heading size="S">{title}</Heading>
|
<Heading size="S">{title}</Heading>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
{#if !embedded}
|
||||||
<div class="portal">
|
<div class="portal">
|
||||||
<Icon hoverable name="Apps" on:click={navigateToPortal} />
|
<Icon hoverable name="Apps" on:click={navigateToPortal} />
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mobile-click-handler"
|
class="mobile-click-handler"
|
||||||
|
|
|
@ -45,6 +45,11 @@ const loadBudibase = async () => {
|
||||||
// server rendered app HTML
|
// server rendered app HTML
|
||||||
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
appStore.actions.setAppId(window["##BUDIBASE_APP_ID##"])
|
||||||
|
|
||||||
|
// Set the flag used to determine if the app is being loaded via an iframe
|
||||||
|
appStore.actions.setAppEmbedded(
|
||||||
|
window["##BUDIBASE_APP_EMBEDDED##"] === "true"
|
||||||
|
)
|
||||||
|
|
||||||
// Fetch environment info
|
// Fetch environment info
|
||||||
if (!get(environmentStore)?.loaded) {
|
if (!get(environmentStore)?.loaded) {
|
||||||
await environmentStore.actions.fetchEnvironment()
|
await environmentStore.actions.fetchEnvironment()
|
||||||
|
|
|
@ -5,6 +5,7 @@ const initialState = {
|
||||||
appId: null,
|
appId: null,
|
||||||
isDevApp: false,
|
isDevApp: false,
|
||||||
clientLoadTime: window.INIT_TIME ? Date.now() - window.INIT_TIME : null,
|
clientLoadTime: window.INIT_TIME ? Date.now() - window.INIT_TIME : null,
|
||||||
|
embedded: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const createAppStore = () => {
|
const createAppStore = () => {
|
||||||
|
@ -46,9 +47,20 @@ const createAppStore = () => {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setAppEmbedded = embeddded => {
|
||||||
|
store.update(state => {
|
||||||
|
if (state) {
|
||||||
|
state.embedded = embeddded
|
||||||
|
} else {
|
||||||
|
state = { embeddded }
|
||||||
|
}
|
||||||
|
return state
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe: derivedStore.subscribe,
|
subscribe: derivedStore.subscribe,
|
||||||
actions: { setAppId, fetchAppDefinition },
|
actions: { setAppId, setAppEmbedded, fetchAppDefinition },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -53,9 +53,9 @@ const createBuilderStore = () => {
|
||||||
notifyLoaded: () => {
|
notifyLoaded: () => {
|
||||||
eventStore.actions.dispatchEvent("preview-loaded")
|
eventStore.actions.dispatchEvent("preview-loaded")
|
||||||
},
|
},
|
||||||
analyticsPing: async () => {
|
analyticsPing: async ({ embedded }) => {
|
||||||
try {
|
try {
|
||||||
await API.analyticsPing({ source: "app" })
|
await API.analyticsPing({ source: "app", embedded })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,7 +33,6 @@ const createScreenStore = () => {
|
||||||
]) => {
|
]) => {
|
||||||
let activeLayout, activeScreen
|
let activeLayout, activeScreen
|
||||||
let screens
|
let screens
|
||||||
|
|
||||||
if ($builderStore.inBuilder) {
|
if ($builderStore.inBuilder) {
|
||||||
// Use builder defined definitions if inside the builder preview
|
// Use builder defined definitions if inside the builder preview
|
||||||
activeScreen = Helpers.cloneDeep($builderStore.screen)
|
activeScreen = Helpers.cloneDeep($builderStore.screen)
|
||||||
|
@ -175,10 +174,10 @@ const createScreenStore = () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
...navigationSettings,
|
...navigationSettings,
|
||||||
|
embedded: $appStore.embedded,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { screens, activeLayout, activeScreen }
|
return { screens, activeLayout, activeScreen }
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
@ -7,11 +7,11 @@ export const buildAnalyticsEndpoints = API => ({
|
||||||
url: "/api/bbtel",
|
url: "/api/bbtel",
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
analyticsPing: async ({ source }) => {
|
analyticsPing: async ({ source, embedded }) => {
|
||||||
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
|
||||||
return await API.post({
|
return await API.post({
|
||||||
url: "/api/bbtel/ping",
|
url: "/api/bbtel/ping",
|
||||||
body: { source, timezone },
|
body: { source, timezone, embedded },
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
|
@ -12,6 +12,7 @@ export const isEnabled = async (ctx: any) => {
|
||||||
|
|
||||||
export const ping = async (ctx: any) => {
|
export const ping = async (ctx: any) => {
|
||||||
const body = ctx.request.body as AnalyticsPingRequest
|
const body = ctx.request.body as AnalyticsPingRequest
|
||||||
|
|
||||||
switch (body.source) {
|
switch (body.source) {
|
||||||
case PingSource.APP: {
|
case PingSource.APP: {
|
||||||
const db = context.getAppDB({ skip_setup: true })
|
const db = context.getAppDB({ skip_setup: true })
|
||||||
|
@ -21,7 +22,7 @@ export const ping = async (ctx: any) => {
|
||||||
if (isDevAppID(appId)) {
|
if (isDevAppID(appId)) {
|
||||||
await events.serve.servedAppPreview(appInfo, body.timezone)
|
await events.serve.servedAppPreview(appInfo, body.timezone)
|
||||||
} else {
|
} else {
|
||||||
await events.serve.servedApp(appInfo, body.timezone)
|
await events.serve.servedApp(appInfo, body.timezone, body.embedded)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,6 +100,9 @@ export const deleteObjects = async function (ctx: any) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const serveApp = async function (ctx: any) {
|
export const serveApp = async function (ctx: any) {
|
||||||
|
const bbHeaderEmbed =
|
||||||
|
ctx.request.get("x-budibase-embed")?.toLowerCase() === "true"
|
||||||
|
|
||||||
//Public Settings
|
//Public Settings
|
||||||
const { config } = await configs.getSettingsConfigDoc()
|
const { config } = await configs.getSettingsConfigDoc()
|
||||||
const branding = await pro.branding.getBrandingConfig(config)
|
const branding = await pro.branding.getBrandingConfig(config)
|
||||||
|
@ -140,6 +143,7 @@ export const serveApp = async function (ctx: any) {
|
||||||
body: html,
|
body: html,
|
||||||
style: css.code,
|
style: css.code,
|
||||||
appId,
|
appId,
|
||||||
|
embedded: bbHeaderEmbed,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
// just return the app info for jest to assert on
|
// just return the app info for jest to assert on
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
<!doctype html>
|
|
||||||
<html>
|
<html>
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
|
@ -7,6 +6,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
window["##BUDIBASE_APP_ID##"] = "{{appId}}"
|
window["##BUDIBASE_APP_ID##"] = "{{appId}}"
|
||||||
|
window["##BUDIBASE_APP_EMBEDDED##"] = "{{embedded}}"
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{{{body}}}
|
{{{body}}}
|
||||||
|
|
|
@ -55,7 +55,22 @@ describe("/static", () => {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
|
|
||||||
expect(events.serve.servedApp).toBeCalledTimes(1)
|
expect(events.serve.servedApp).toBeCalledTimes(1)
|
||||||
expect(events.serve.servedApp).toBeCalledWith(config.getProdApp(), timezone)
|
expect(events.serve.servedApp).toBeCalledWith(config.getProdApp(), timezone, undefined)
|
||||||
|
expect(events.serve.servedAppPreview).not.toBeCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should ping from an embedded app", async () => {
|
||||||
|
const headers = config.defaultHeaders()
|
||||||
|
headers[constants.Header.APP_ID] = config.prodAppId
|
||||||
|
|
||||||
|
await request
|
||||||
|
.post("/api/bbtel/ping")
|
||||||
|
.send({source: "app", timezone, embedded: true})
|
||||||
|
.set(headers)
|
||||||
|
.expect(200)
|
||||||
|
|
||||||
|
expect(events.serve.servedApp).toBeCalledTimes(1)
|
||||||
|
expect(events.serve.servedApp).toBeCalledWith(config.getProdApp(), timezone, true)
|
||||||
expect(events.serve.servedAppPreview).not.toBeCalled()
|
expect(events.serve.servedAppPreview).not.toBeCalled()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -86,12 +86,6 @@ export default async function builder(ctx: UserCtx) {
|
||||||
|
|
||||||
const referer = ctx.headers["referer"]
|
const referer = ctx.headers["referer"]
|
||||||
|
|
||||||
const overviewPath = "/builder/portal/overview/"
|
|
||||||
const overviewContext = !referer ? false : referer.includes(overviewPath)
|
|
||||||
if (overviewContext) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasAppId = !referer ? false : referer.includes(appId)
|
const hasAppId = !referer ? false : referer.includes(appId)
|
||||||
const editingApp = referer ? hasAppId : false
|
const editingApp = referer ? hasAppId : false
|
||||||
// check this is a builder call and editing
|
// check this is a builder call and editing
|
||||||
|
|
|
@ -6,4 +6,5 @@ export enum PingSource {
|
||||||
export interface AnalyticsPingRequest {
|
export interface AnalyticsPingRequest {
|
||||||
source: PingSource
|
source: PingSource
|
||||||
timezone: string
|
timezone: string
|
||||||
|
embedded?: boolean
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ export interface BuilderServedEvent extends BaseEvent {
|
||||||
export interface AppServedEvent extends BaseEvent {
|
export interface AppServedEvent extends BaseEvent {
|
||||||
appVersion: string
|
appVersion: string
|
||||||
timezone: string
|
timezone: string
|
||||||
|
embed?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AppPreviewServedEvent extends BaseEvent {
|
export interface AppPreviewServedEvent extends BaseEvent {
|
||||||
|
|
Loading…
Reference in New Issue