Merge pull request #1455 from Budibase/app-list

Portal as default home screen and app management
This commit is contained in:
Andrew Kingston 2021-05-07 08:56:34 +01:00 committed by GitHub
commit 7481f129aa
86 changed files with 595 additions and 3674 deletions

View File

@ -16,6 +16,11 @@ static_resources:
- name: local_services
domains: ["*"]
routes:
# special case to redirect specifically the route path
# to the builder, if this were a prefix then it would break minio
- match: { path: "/" }
redirect: { path_redirect: "/builder/" }
- match: { prefix: "/db/" }
route:
cluster: couchdb-service
@ -33,7 +38,11 @@ static_resources:
route:
cluster: server-dev
- match: { prefix: "/" }
- match: { path: "/" }
route:
cluster: builder-dev
- match: { prefix: "/builder/" }
route:
cluster: builder-dev

View File

@ -21,7 +21,6 @@ static_resources:
cluster: app-service
prefix_rewrite: "/"
# special case for presenting our static self hosting page
- match: { path: "/" }
route:
cluster: app-service

View File

@ -4,6 +4,7 @@
import Menu from "../Menu/Menu.svelte"
export let disabled = false
export let align = "left"
let anchor
let dropdown
@ -31,7 +32,7 @@
<div use:getAnchor on:click={openMenu}>
<slot name="control" />
</div>
<Popover bind:this={dropdown} {anchor} align="left">
<Popover bind:this={dropdown} {anchor} {align}>
<Menu>
<slot />
</Menu>

View File

@ -19,6 +19,7 @@
export let getOptionValue = option => option
export let open = false
export let readonly = false
export let quiet = false
const dispatch = createEventDispatcher()
const onClick = e => {
@ -33,6 +34,7 @@
<button
{id}
class="spectrum-Picker spectrum-Picker--sizeM"
class:spectrum-Picker--quiet={quiet}
{disabled}
class:is-invalid={!!error}
class:is-open={open}

View File

@ -11,6 +11,7 @@
export let getOptionLabel = option => option
export let getOptionValue = option => option
export let readonly = false
export let quiet = false
const dispatch = createEventDispatcher()
let open = false
@ -43,6 +44,7 @@
<Picker
on:click
bind:open
{quiet}
{id}
{error}
{disabled}

View File

@ -13,6 +13,7 @@
export let options = []
export let getOptionLabel = option => extractProperty(option, "label")
export let getOptionValue = option => extractProperty(option, "value")
export let quiet = false
const dispatch = createEventDispatcher()
const onChange = e => {
@ -29,6 +30,7 @@
<Field {label} {labelPosition} {disabled} {error}>
<Select
{quiet}
{error}
{disabled}
{readonly}

View File

@ -1,62 +1,16 @@
<script>
export let forAttr = "",
extraSmall = false,
small = false,
medium = false,
large = false,
extraLarge = false,
white = false,
grey = false,
black = false
import "@spectrum-css/fieldlabel/dist/index-vars.css"
export let size = "M"
</script>
<label
class="bb-label"
class:extraSmall
class:small
class:medium
class:large
class:extraLarge
class:white
class:grey
class:black
for={forAttr}
>
<label class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
<slot />
</label>
<style>
.bb-label {
font-family: var(--font-sans);
font-weight: 500;
text-rendering: var(--text-render);
color: var(--ink);
font-size: var(--font-size-s);
margin-bottom: var(--spacing-s);
display: block;
}
.extraSmall {
font-size: var(--font-size-xs);
}
.small {
font-size: var(--font-size-s);
}
.medium {
font-size: var(--font-size-m);
}
.large {
font-size: var(--font-size-l);
}
.extraLarge {
font-size: var(--font-size-xl);
}
.white {
color: white;
}
.grey {
color: var(--grey-6);
}
.black {
color: var(--ink);
label {
padding: 0;
white-space: nowrap;
}
</style>

View File

@ -44,6 +44,9 @@
padding-top: var(--spacing-l);
padding-bottom: var(--spacing-l);
}
.gap-XS {
grid-gap: var(--spacing-s);
}
.gap-S {
grid-gap: var(--spectrum-alias-grid-gutter-xsmall);
}

View File

@ -27,7 +27,7 @@
>
{#if icon}
<svg
class="spectrum-Icon spectrum-Icon--sizeM spectrum-Menu-itemIcon"
class="spectrum-Icon spectrum-Icon--sizeS spectrum-Menu-itemIcon"
focusable="false"
aria-hidden="true"
aria-label={icon}
@ -37,3 +37,9 @@
{/if}
<span class="spectrum-Menu-itemLabel"><slot /></span>
</li>
<style>
.spectrum-Menu-itemIcon {
align-self: center;
}
</style>

View File

@ -3,11 +3,20 @@
export let size = "M"
export let serif = false
export let noPadding = false
</script>
<p
class="spectrum-Body class:spectrum-Body--size{size}"
class:noPadding
class="spectrum-Body spectrum-Body--size{size}"
class:spectrum-Body--serif={serif}
>
<slot />
</p>
<style>
.noPadding {
padding: 0;
margin: 0;
}
</style>

View File

@ -0,0 +1,64 @@
export const gradient = (node, config = {}) => {
const defaultConfig = {
points: 10,
saturation: 0.8,
lightness: 0.75,
softness: 0.8,
}
// Applies a gradient background
const createGradient = config => {
config = {
...defaultConfig,
...config,
}
const { saturation, lightness, softness, points } = config
// Generates a random number between min and max
const rand = (min, max) => {
return Math.round(min + Math.random() * (max - min))
}
// Generates a random HSL colour using the options specified
const randomHSL = () => {
const lowerSaturation = Math.min(100, saturation * 100)
const upperSaturation = Math.min(100, (saturation + 0.2) * 100)
const lowerLightness = Math.min(100, lightness * 100)
const upperLightness = Math.min(100, (lightness + 0.2) * 100)
const hue = rand(0, 360)
const sat = `${rand(lowerSaturation, upperSaturation)}%`
const light = `${rand(lowerLightness, upperLightness)}%`
return `hsl(${hue},${sat},${light})`
}
// Generates a radial gradient stop point
const randomGradientPoint = () => {
const lowerTransparency = Math.min(100, softness * 100)
const upperTransparency = Math.min(100, (softness + 0.2) * 100)
const transparency = rand(lowerTransparency, upperTransparency)
return (
`radial-gradient(` +
`at ${rand(10, 90)}% ${rand(10, 90)}%,` +
`${randomHSL()} 0,` +
`transparent ${transparency}%)`
)
}
let css = `opacity:0.9;background-color:${randomHSL()};background-image:`
for (let i = 0; i < points - 1; i++) {
css += `${randomGradientPoint()},`
}
css += `${randomGradientPoint()};`
node.style = css
}
// Apply the initial gradient
createGradient(config)
return {
// Apply a new gradient
update: config => {
createGradient(config)
},
}
}

View File

@ -1,7 +1,7 @@
<script>
import { Input, Select, DatePicker, Toggle, TextArea } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers"
import { capitalise } from "helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let defaultValue

View File

@ -59,7 +59,7 @@
const selectRelationship = ({ tableId, rowId, fieldName }) => {
$goto(
`/builder/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
`/builder/app/${$params.application}/data/table/${tableId}/relationship/${rowId}/${fieldName}`
)
}

View File

@ -8,7 +8,7 @@
Body,
ModalContent,
} from "@budibase/bbui"
import { capitalise } from "../../../../helpers"
import { capitalise } from "helpers"
export let resourceId
export let permissions

View File

@ -1,7 +1,7 @@
<script>
import { Label, Input, Layout } from "@budibase/bbui"
import KeyValueBuilder from "components/integration/KeyValueBuilder.svelte"
import { capitalise } from "../../../../helpers"
import { capitalise } from "helpers"
export let integration
export let schema

View File

@ -14,7 +14,7 @@
getBindableProperties,
readableToRuntimeBinding,
} from "builderStore/dataBinding"
import { currentAsset, store } from "../../../builderStore"
import { currentAsset, store } from "builderStore"
import { handlebarsCompletions } from "constants/completions"
import { addToText } from "./utils"

View File

@ -10,7 +10,7 @@
import { createEventDispatcher } from "svelte"
import { isValid } from "@budibase/string-templates"
import { handlebarsCompletions } from "constants/completions"
import { readableToRuntimeBinding } from "../../../builderStore/dataBinding"
import { readableToRuntimeBinding } from "builderStore/dataBinding"
import { addToText } from "./utils"
const dispatch = createEventDispatcher()

View File

@ -3,7 +3,7 @@
import { store, currentAsset, selectedComponent } from "builderStore"
import iframeTemplate from "./iframeTemplate"
import { Screen } from "builderStore/store/screenTemplates/utils/Screen"
import { FrontendTypes } from "../../../constants"
import { FrontendTypes } from "constants"
let iframe
let layout
@ -82,7 +82,8 @@
style="height: 100%; width: 100%"
title="componentPreview"
bind:this={iframe}
srcdoc={template} />
srcdoc={template}
/>
</div>
<style>

View File

@ -7,7 +7,7 @@
runtimeToReadableBinding,
} from "builderStore/dataBinding"
import BindingPanel from "components/common/bindings/BindingPanel.svelte"
import { capitalise } from "../../../../helpers"
import { capitalise } from "helpers"
export let label = ""
export let bindable = true

View File

@ -1,7 +1,7 @@
<script>
import { goto } from "@roxi/routify"
import {
notifications,
Button,
Link,
Input,
Modal,
@ -18,27 +18,18 @@
username,
password,
})
notifications.success("Logged in successfully.")
notifications.success("Logged in successfully")
$goto("../portal")
} catch (err) {
console.error(err)
notifications.error("Invalid credentials")
}
}
async function createTestUser() {
try {
await auth.firstUser()
notifications.success("Test user created")
} catch (err) {
console.error(err)
notifications.error("Could not create test user")
}
}
</script>
<Modal fixed>
<ModalContent
size="L"
size="M"
title="Log In"
onConfirm={login}
confirmText="Log In"
@ -51,7 +42,6 @@
<Link target="_blank" href="/api/admin/auth/google">
Sign In With Google
</Link>
<Button secondary on:click={createTestUser}>Create Test User</Button>
</div>
</ModalContent>
</Modal>

View File

@ -11,7 +11,7 @@
const id = $params.application
await del(`/api/applications/${id}`)
loading = false
$goto("/builder/")
$goto("/builder")
}
</script>

View File

@ -1,11 +1,19 @@
<script>
import { goto } from "@roxi/routify"
import { ActionButton, Heading } from "@budibase/bbui"
import { notifications } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import {
Heading,
Icon,
Body,
Layout,
ActionMenu,
MenuItem,
Link,
notifications,
} from "@budibase/bbui"
import download from "downloadjs"
import { gradient } from "actions"
export let name, _id
export let name
export let _id
let appExportLoading = false
@ -15,58 +23,60 @@
download(
`/api/backups/export?appId=${_id}&appname=${encodeURIComponent(name)}`
)
notifications.success("App Export Complete.")
notifications.success("App export complete")
} catch (err) {
console.error(err)
notifications.error("App Export Failed.")
notifications.error("App export failed")
} finally {
appExportLoading = false
}
}
</script>
<div class="apps-card">
<Heading size="S">{name}</Heading>
<div class="card-footer" data-cy={`app-${name}`}>
<ActionButton on:click={() => $goto(`/builder/${_id}`)}>
Open
{name}
</ActionButton>
{#if appExportLoading}
<Spinner size="10" />
{:else}
<ActionButton icon="Download" quiet />
<Layout noPadding gap="XS">
<div class="preview" use:gradient />
<div class="title">
<Link href={`/builder/app/${_id}`}>
<Heading size="XS">
{name}
</Heading>
</Link>
<ActionMenu>
<Icon slot="control" name="More" hoverable />
<MenuItem on:click={exportApp} icon="Download">Export</MenuItem>
</ActionMenu>
</div>
<div class="status">
<Body noPadding size="S">
Edited {Math.floor(1 + Math.random() * 10)} months ago
</Body>
{#if Math.random() > 0.5}
<Icon name="LockClosed" />
{/if}
</div>
</div>
</Layout>
<style>
.apps-card {
background-color: var(--background);
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-xl)
var(--spacing-xl);
max-width: 300px;
max-height: 150px;
border-radius: var(--border-radius-m);
border: var(--border-dark);
.preview {
height: 135px;
border-radius: var(--border-radius-s);
margin-bottom: var(--spacing-s);
}
.card-footer {
.title,
.status {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
margin-top: var(--spacing-m);
align-items: center;
}
i {
font-size: var(--font-size-l);
.title :global(a) {
text-decoration: none;
}
.title :global(h1:hover) {
color: var(--spectrum-global-color-blue-600);
cursor: pointer;
transition: 0.2s all;
}
i:hover {
color: var(--blue);
transition: color 130ms ease;
}
</style>

View File

@ -1,50 +1,25 @@
<script>
import { onMount } from "svelte"
import AppCard from "./AppCard.svelte"
import { Heading, Divider } from "@budibase/bbui"
import Spinner from "components/common/Spinner.svelte"
import { get } from "builderStore/api"
import { apps } from "stores/portal"
let promise = getApps()
async function getApps() {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok) {
return json
} else {
throw new Error(json)
}
}
onMount(apps.load)
</script>
<div class="root">
<Heading size="M">Your Apps</Heading>
<Divider size="M" />
{#await promise}
<div class="spinner-container">
<Spinner size="30" />
</div>
{:then apps}
<div class="apps">
{#each apps as app}
<AppCard {...app} />
{/each}
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
</div>
{#if $apps.length}
<div class="appList">
{#each $apps as app}
<AppCard {...app} />
{/each}
</div>
{:else}
<div>No apps found.</div>
{/if}
<style>
.root {
margin-top: 10px;
}
.apps {
margin-top: var(--layout-m);
.appList {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-gap: var(--layout-s);
justify-content: start;
}
</style>

View File

@ -1,15 +0,0 @@
<script>
import { Button, Modal } from "@budibase/bbui"
import BuilderSettingsModal from "./BuilderSettingsModal.svelte"
let modal
</script>
<div>
<Button primary quiet icon="Settings" text on:click={modal.show}>
Settings
</Button>
</div>
<Modal bind:this={modal} width="30%">
<BuilderSettingsModal />
</Modal>

View File

@ -112,7 +112,7 @@
}
const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json()
$goto(`./${appJson._id}`)
window.location = `/builder/app/${appJson._id}`
} catch (error) {
console.error(error)
notifications.error(error)

View File

@ -1,6 +0,0 @@
<script>
import { Button } from "@budibase/bbui"
import { auth } from "stores/backend"
</script>
<Button primary quiet text icon="LogOut" on:click={auth.logout}>Log Out</Button>

View File

@ -1,29 +0,0 @@
<script>
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import {
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
import { admin } from "stores/portal"
import LoginForm from "components/login/LoginForm.svelte"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import LogoutButton from "components/start/LogoutButton.svelte"
import Logo from "/assets/budibase-logo.svg"
import api from "builderStore/api"
let checklist
onMount(async () => {
await admin.init()
if (!$admin?.checklist?.adminUser) {
$goto("./admin")
} else {
$goto("./portal")
}
})
</script>
{#if $admin.checklist}
<slot />
{/if}

View File

@ -1,69 +0,0 @@
<script>
import {
Button,
Heading,
Label,
notifications,
Layout,
Input,
Body,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import { onMount } from "svelte"
import api from "builderStore/api"
let adminUser = {}
async function save() {
try {
// Save the admin user
const response = await api.post(`/api/admin/users/init`, adminUser)
const json = await response.json()
if (response.status !== 200) throw new Error(json.message)
notifications.success(`Admin user created.`)
$goto("../portal")
} catch (err) {
notifications.error(`Failed to create admin user.`)
}
}
</script>
<section>
<div class="container">
<header>
<Heading size="M">Create an admin user</Heading>
<Body size="S">The admin user has access to everything in budibase.</Body>
</header>
<div class="config-form">
<Layout gap="S">
<Input label="email" bind:value={adminUser.email} />
<Input
label="password"
type="password"
bind:value={adminUser.password}
/>
<Button cta on:click={save}>Create super admin user</Button>
</Layout>
</div>
</div>
</section>
<style>
section {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
header {
text-align: center;
width: 80%;
margin: 0 auto;
}
.config-form {
margin-bottom: 42px;
}
</style>

View File

@ -1,116 +1,33 @@
<script>
import {
SideNavigation as Navigation,
SideNavigationItem as Item,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { goto } from "@roxi/routify"
import { auth } from "stores/backend"
import LoginForm from "components/login/LoginForm.svelte"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import LogoutButton from "components/start/LogoutButton.svelte"
import Logo from "/assets/budibase-logo.svg"
import { admin } from "stores/portal"
let modal
let loaded = false
$: hasAdminUser = !!$admin?.checklist?.adminUser
onMount(async () => {
await admin.init()
await auth.checkAuth()
loaded = true
})
// Force creation of an admin user if one doesn't exist
$: {
if (loaded && !hasAdminUser) {
$goto("./admin")
}
}
// Redirect to log in at any time if the user isn't authenticated
$: {
if (loaded && hasAdminUser && !$auth.user) {
$goto("./auth/login")
}
}
</script>
{#if $auth}
{#if $auth.user}
<div class="root">
<div class="ui-nav">
<div class="home-logo">
<img src={Logo} alt="Budibase icon" />
</div>
<div class="nav-section">
<div class="nav-top">
<Navigation>
<Item href="/builder/" icon="Apps" selected>Apps</Item>
<Item external href="https://portal.budi.live/" icon="Servers">
Hosting
</Item>
<Item external href="https://docs.budibase.com/" icon="Book">
Documentation
</Item>
<Item
external
href="https://github.com/Budibase/budibase/discussions"
icon="PeopleGroup"
>
Community
</Item>
<Item
external
href="https://github.com/Budibase/budibase/issues/new/choose"
icon="Bug"
>
Raise an issue
</Item>
</Navigation>
</div>
<div class="nav-bottom">
<BuilderSettingsButton />
<LogoutButton />
</div>
</div>
</div>
<div class="main">
<slot />
</div>
</div>
{:else}
<section class="login">
<LoginForm />
</section>
{/if}
{#if loaded}
<slot />
{/if}
<style>
.root {
display: grid;
grid-template-columns: 260px 1fr;
height: 100%;
width: 100%;
}
.login {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
}
.main {
grid-column: 2;
overflow: auto;
}
.ui-nav {
grid-column: 1;
background-color: var(--background);
padding: 20px;
display: flex;
flex-direction: column;
border-right: var(--border-light);
}
.home-logo {
cursor: pointer;
height: 40px;
margin-bottom: 20px;
}
.home-logo img {
height: 40px;
}
.nav-section {
margin: 20px 0 0 0;
display: flex;
flex-direction: column;
justify-content: space-between;
height: 100%;
}
.nav-bottom :global(> *) {
margin-top: 5px;
}
</style>

View File

@ -0,0 +1,78 @@
<script>
import {
Button,
Heading,
notifications,
Layout,
Input,
Body,
} from "@budibase/bbui"
import { goto } from "@roxi/routify"
import api from "builderStore/api"
import { admin } from "stores/portal"
let adminUser = {}
async function save() {
try {
// Save the admin user
const response = await api.post(`/api/admin/users/init`, adminUser)
const json = await response.json()
if (response.status !== 200) {
throw new Error(json.message)
}
notifications.success(`Admin user created`)
await admin.init()
$goto("../portal")
} catch (err) {
notifications.error(`Failed to create admin user`)
}
}
</script>
<section>
<div class="container">
<Layout gap="XS">
<img src="https://i.imgur.com/ZKyklgF.png" />
</Layout>
<div class="center">
<Layout gap="XS">
<Heading size="M">Create an admin user</Heading>
<Body size="M"
>The admin user has access to everything in Budibase.</Body
>
</Layout>
</div>
<Layout gap="XS">
<Input label="Email" bind:value={adminUser.email} />
<Input label="Password" type="password" bind:value={adminUser.password} />
</Layout>
<Layout gap="S">
<Button cta on:click={save}>Create super admin user</Button>
</Layout>
</div>
</section>
<style>
section {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.container {
margin: 0 auto;
width: 260px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: stretch;
}
.center {
text-align: center;
}
img {
width: 40px;
margin: 0 auto;
}
</style>

View File

@ -6,9 +6,9 @@
import ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
import { get } from "builderStore/api"
import { isActive, goto, layout, params } from "@roxi/routify"
import { isActive, goto, layout } from "@roxi/routify"
import Logo from "/assets/bb-logo.svg"
import { capitalise } from "../../../helpers"
import { capitalise } from "helpers"
// Get Package and set store
export let application
@ -60,7 +60,7 @@
<img
src={Logo}
alt="budibase icon"
on:click={() => $goto(`/builder/`)}
on:click={() => $goto(`../../portal/`)}
/>
</button>

View File

@ -5,7 +5,7 @@
import { notifications } from "@budibase/bbui"
import IntegrationConfigForm from "components/backend/DatasourceNavigator/TableIntegrationMenu/IntegrationConfigForm.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons"
import { capitalise } from "../../../../../../helpers"
import { capitalise } from "helpers"
let unsaved = false

View File

@ -0,0 +1,4 @@
<script>
import { goto } from "@roxi/routify"
$goto("../portal")
</script>

View File

@ -0,0 +1,4 @@
<script>
import { goto } from "@roxi/routify"
$goto("./login")
</script>

View File

@ -0,0 +1,5 @@
<script>
import LoginForm from "components/login/LoginForm.svelte"
</script>
<LoginForm />

View File

@ -1,123 +1,4 @@
<script>
import api from "builderStore/api"
import AppList from "components/start/AppList.svelte"
import { get } from "builderStore/api"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import { Button, Heading, Modal, ButtonGroup } from "@budibase/bbui"
import TemplateList from "components/start/TemplateList.svelte"
import analytics from "analytics"
import Banner from "/assets/orange-landscape.png"
let hasKey
let template
let modal
async function getApps() {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok) {
return json
} else {
throw new Error(json)
}
}
async function fetchKeys() {
const response = await api.get(`/api/keys/`)
return await response.json()
}
async function checkIfKeysAndApps() {
const keys = await fetchKeys()
const apps = await getApps()
if (keys.userId) {
hasKey = true
analytics.identify(keys.userId)
}
}
function selectTemplate(newTemplate) {
template = newTemplate
modal.show()
}
function initiateAppImport() {
template = { fromFile: true }
modal.show()
}
function closeModal() {
template = null
modal.hide()
}
checkIfKeysAndApps()
import { goto } from "@roxi/routify"
$goto("./portal")
</script>
<div class="container">
<div class="header">
<Heading size="M">Welcome to the Budibase Beta</Heading>
<ButtonGroup>
<Button secondary on:click={initiateAppImport}>Import Web App</Button>
<Button cta on:click={modal.show}>Create New Web App</Button>
</ButtonGroup>
</div>
<div class="banner">
<img src={Banner} alt="rocket" />
<div class="banner-content">
Every accomplishment starts with a decision to try.
</div>
</div>
<!-- <TemplateList onSelect={selectTemplate} /> -->
<AppList />
</div>
<Modal bind:this={modal} padding={false} width="600px" on:hide={closeModal}>
<CreateAppModal {hasKey} {template} />
</Modal>
<style>
.container {
display: grid;
gap: var(--spacing-xl);
margin: 40px 80px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
}
.banner {
display: flex;
align-items: center;
justify-content: center;
position: relative;
text-align: center;
color: white;
border-radius: 16px;
}
.banner img {
height: 250px;
width: 100%;
border-radius: 5px;
}
.banner-content {
position: absolute;
font-size: 24px;
color: white;
font-weight: 500;
}
.button-group {
display: flex;
flex-direction: row;
}
</style>

View File

@ -1,49 +1,53 @@
<script>
import { isActive, url, goto } from "@roxi/routify"
import { isActive, goto } from "@roxi/routify"
import { onMount } from "svelte"
import {
ActionMenu,
Checkbox,
MenuItem,
Icon,
Heading,
Avatar,
Search,
Layout,
ProgressCircle,
SideNavigation as Navigation,
SideNavigationItem as Item,
ActionMenu,
MenuItem,
Modal,
} from "@budibase/bbui"
import api from "builderStore/api"
import ConfigChecklist from "components/common/ConfigChecklist.svelte"
import { organisation, admin } from "stores/portal"
import { organisation, apps } from "stores/portal"
import { auth } from "stores/backend"
import BuilderSettingsModal from "components/start/BuilderSettingsModal.svelte"
organisation.init()
apps.load()
let orgName
let orgLogo
let user
let oldSettingsModal
async function getInfo() {
// fetch orgInfo
orgName = "ACME Inc."
orgLogo = "https://via.placeholder.com/150"
user = { name: "John Doe" }
}
onMount(getInfo)
let menu = [
{ title: "Apps", href: "/portal/apps" },
{ title: "Drafts", href: "/portal/drafts" },
{ title: "Users", href: "/portal/users", heading: "Manage" },
{ title: "Groups", href: "/portal/groups" },
{ title: "Auth", href: "/portal/oauth" },
{ title: "Email", href: "/portal/email" },
{ title: "General", href: "/portal/settings/general", heading: "Settings" },
{ title: "Theming", href: "/portal/theming" },
{ title: "Account", href: "/portal/account" },
{ title: "Apps", href: "/builder/portal/apps" },
{ title: "Drafts", href: "/builder/portal/drafts" },
{ title: "Users", href: "/builder/portal/users", heading: "Manage" },
{ title: "Groups", href: "/builder/portal/groups" },
{ title: "Auth", href: "/builder/portal/oauth" },
{ title: "Email", href: "/builder/portal/email" },
{
title: "General",
href: "/builder/portal/settings/general",
heading: "Settings",
},
{ title: "Theming", href: "/builder/portal/theming" },
{ title: "Account", href: "/builder/portal/account" },
]
</script>
@ -51,7 +55,7 @@
<div class="nav">
<Layout paddingX="L" paddingY="L">
<div class="branding">
<div class="name">
<div class="name" on:click={() => $goto("./apps")}>
<img
src={$organisation?.logoUrl || "https://i.imgur.com/ZKyklgF.png"}
alt="Logotype"
@ -74,30 +78,42 @@
<div class="main">
<div class="toolbar">
<Search placeholder="Global search" />
<div class="avatar">
<Avatar size="M" name="John Doe" />
<Icon size="XL" name="ChevronDown" />
</div>
<ActionMenu align="right">
<div slot="control" class="avatar">
<Avatar size="M" name="John Doe" />
<Icon size="XL" name="ChevronDown" />
</div>
<MenuItem icon="Settings" on:click={oldSettingsModal.show}>
Old settings
</MenuItem>
<MenuItem icon="LogOut" on:click={auth.logout}>Log out</MenuItem>
</ActionMenu>
</div>
<div>
<div class="content">
<slot />
</div>
</div>
</div>
<Modal bind:this={oldSettingsModal} width="30%">
<BuilderSettingsModal />
</Modal>
<style>
.container {
min-height: 100vh;
height: 100%;
display: grid;
grid-template-columns: 250px 1fr;
align-items: stretch;
}
.nav {
background: var(--background);
border-right: var(--border-light);
overflow: auto;
}
.main {
display: grid;
grid-template-rows: auto 1fr;
overflow: hidden;
}
.branding {
display: grid;
@ -112,6 +128,9 @@
grid-gap: var(--spacing-m);
align-items: center;
}
.name:hover {
cursor: pointer;
}
.avatar {
display: grid;
grid-template-columns: auto auto;
@ -129,6 +148,7 @@
grid-template-columns: 250px auto;
justify-content: space-between;
padding: var(--spacing-m) calc(var(--spacing-xl) * 2);
align-items: center;
}
img {
width: 28px;
@ -139,4 +159,7 @@
text-overflow: ellipsis;
font-weight: 500;
}
.content {
overflow: auto;
}
</style>

View File

@ -0,0 +1,92 @@
<script>
import {
Heading,
Layout,
Button,
ActionButton,
ActionGroup,
ButtonGroup,
Select,
Modal,
} from "@budibase/bbui"
import AppList from "components/start/AppList.svelte"
import CreateAppModal from "components/start/CreateAppModal.svelte"
import api from "builderStore/api"
import analytics from "analytics"
import { onMount } from "svelte"
let layout = "grid"
let modal
let template
async function checkKeys() {
const response = await api.get(`/api/keys/`)
const keys = await response.json()
if (keys.userId) {
analytics.identify(keys.userId)
}
}
function initiateAppImport() {
template = { fromFile: true }
modal.show()
}
onMount(checkKeys)
</script>
<Layout noPadding>
<div class="title">
<Heading>Apps</Heading>
<ButtonGroup>
<Button secondary on:click={initiateAppImport}>Import app</Button>
<Button cta on:click={modal.show}>Create new app</Button>
</ButtonGroup>
</div>
<div class="filter">
<div class="select">
<Select quiet placeholder="Filter by groups" />
</div>
<ActionGroup>
<ActionButton
on:click={() => (layout = "grid")}
selected={layout === "grid"}
quiet
icon="ClassicGridView"
/>
<ActionButton
on:click={() => (layout = "table")}
selected={layout === "table"}
quiet
icon="ViewRow"
/>
</ActionGroup>
</div>
{#if layout === "grid"}
<AppList />
{:else}
Table view.
{/if}
</Layout>
<Modal
bind:this={modal}
padding={false}
width="600px"
on:hide={() => (template = null)}
>
<CreateAppModal {template} />
</Modal>
<style>
.title,
.filter {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.select {
width: 110px;
}
</style>

View File

@ -60,47 +60,50 @@
</script>
<Page>
<header>
<Heading size="M">OAuth</Heading>
<Body size="S">
Every budibase app comes with basic authentication (email/password)
included. You can add additional authentication methods from the options
below.
</Body>
</header>
<Divider />
{#if google}
<div class="config-form">
<Layout gap="S">
<Layout noPadding>
<div>
<Heading size="M">OAuth</Heading>
<Body>
Every budibase app comes with basic authentication (email/password)
included. You can add additional authentication methods from the options
below.
</Body>
</div>
<Divider />
{#if google}
<div>
<Heading size="S">
<span>
<GoogleLogo />
Google
</span>
</Heading>
{#each ConfigFields.Google as field}
<div class="form-row">
<Label>{field}</Label>
<Input bind:value={google.config[field]} />
</div>
{/each}
</Layout>
<Button primary on:click={() => save(google)}>Save</Button>
</div>
<Divider />
{/if}
<Body>
To allow users to authenticate using their Google accounts, fill out
the fields below.
</Body>
</div>
{#each ConfigFields.Google as field}
<div class="form-row">
<Label size="L">{field}</Label>
<Input bind:value={google.config[field]} />
</div>
{/each}
<div>
<Button primary on:click={() => save(google)}>Save</Button>
</div>
<Divider />
{/if}
</Layout>
</Page>
<style>
.config-form {
margin-top: 42px;
margin-bottom: 42px;
}
.form-row {
display: grid;
grid-template-columns: 20% 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
span {
@ -108,8 +111,4 @@
align-items: center;
gap: var(--spacing-s);
}
header {
margin-bottom: 42px;
}
</style>

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -45,12 +45,12 @@
<Layout noPadding>
<div class="intro">
<Heading size="M">General</Heading>
<Body
>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Hic vero, aut
<Body>
Lorem ipsum, dolor sit amet consectetur adipisicing elit. Hic vero, aut
culpa provident sunt ratione! Voluptas doloremque, dicta nisi velit
perspiciatis, ratione vel blanditiis totam, nam voluptate repellat
aperiam fuga!</Body
>
aperiam fuga!
</Body>
</div>
<Divider size="S" />
<div class="information">
@ -58,7 +58,7 @@
<Body>Here you can update your logo and organization name.</Body>
<div class="fields">
<div class="field">
<Label>Organization name</Label>
<Label size="L">Organization name</Label>
<Input thin bind:value={company} />
</div>
<!-- <div class="field">
@ -72,13 +72,13 @@
<Divider size="S" />
<div class="analytics">
<Heading size="S">Analytics</Heading>
<Body
>If you would like to send analytics that help us make Budibase better,
please let us know below.</Body
>
<Body>
If you would like to send analytics that help us make Budibase better,
please let us know below.
</Body>
<div class="fields">
<div class="field">
<Label>Send Analytics to Budibase</Label>
<Label size="L">Send Analytics to Budibase</Label>
<Toggle text="" value={!analyticsDisabled} />
</div>
</div>
@ -97,7 +97,8 @@
}
.field {
display: grid;
grid-template-columns: 30% 1fr;
grid-template-columns: 32% 1fr;
align-items: center;
}
.file {
max-width: 30ch;

View File

@ -1 +1,4 @@
Index route
<script>
import { goto } from "@roxi/routify"
$goto("./builder")
</script>

View File

@ -1,27 +0,0 @@
<script>
import { Heading, Layout } from "@budibase/bbui"
</script>
<Layout noPadding>
<div>
<Heading>Apps</Heading>
</div>
<div class="appList">
{#each new Array(10) as _}
<div class="app" />
{/each}
</div>
</Layout>
<style>
.appList {
display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, 300px);
}
.app {
height: 130px;
border-radius: 4px;
background-color: var(--spectrum-global-color-gray-200);
}
</style>

View File

@ -1,28 +1,25 @@
import { writable } from "svelte/store"
import api from "../../builderStore/api"
async function checkAuth() {
const response = await api.get("/api/self")
const user = await response.json()
if (response.status === 200) return user
return null
}
export function createAuthStore() {
const { subscribe, set } = writable(null)
checkAuth()
.then(user => set({ user }))
.catch(() => set({ user: null }))
const store = writable({ user: null })
return {
subscribe,
subscribe: store.subscribe,
checkAuth: async () => {
const response = await api.get("/api/self")
const user = await response.json()
if (response.status === 200) {
store.update(state => ({ ...state, user }))
} else {
store.update(state => ({ ...state, user: null }))
}
},
login: async creds => {
const response = await api.post(`/api/admin/auth`, creds)
const json = await response.json()
if (response.status === 200) {
set({ user: json.user })
store.update(state => ({ ...state, user: json.user }))
} else {
throw "Invalid credentials"
}
@ -34,7 +31,7 @@ export function createAuthStore() {
throw "Unable to create logout"
}
await response.json()
set({ user: null })
store.update(state => ({ ...state, user: null }))
},
createUser: async user => {
const response = await api.post(`/api/admin/users`, user)
@ -43,13 +40,6 @@ export function createAuthStore() {
}
await response.json()
},
firstUser: async () => {
const response = await api.post(`/api/admin/users/first`)
if (response.status !== 200) {
throw "Unable to create test user"
}
await response.json()
},
}
}

View File

@ -0,0 +1,27 @@
import { writable } from "svelte/store"
import { get } from "builderStore/api"
export function createAppStore() {
const store = writable([])
async function load() {
try {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok && Array.isArray(json)) {
store.set(json)
} else {
store.set([])
}
} catch (error) {
store.set([])
}
}
return {
subscribe: store.subscribe,
load,
}
}
export const apps = createAppStore()

View File

@ -1,2 +1,3 @@
export { organisation } from "./organisation"
export { admin } from "./admin"
export { apps } from "./apps"

View File

@ -6,7 +6,7 @@ import path from "path"
export default ({ mode }) => {
const isProduction = mode === "production"
return {
base: "/",
base: "/builder/",
build: {
minify: isProduction,
outDir: "../server/builder",
@ -52,6 +52,14 @@ export default ({ mode }) => {
find: "analytics",
replacement: path.resolve("./src/analytics"),
},
{
find: "actions",
replacement: path.resolve("./src/actions"),
},
{
find: "helpers",
replacement: path.resolve("./src/helpers"),
},
],
},
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
<script>
import { getContext } from "svelte"
const { styleable } = getContext("sdk")
const { styleable, linkable } = getContext("sdk")
const component = getContext("component")
export const className = ""
@ -41,6 +41,7 @@
<footer>
<p class="subtext">{subtext}</p>
<a
use:linkable
style="--linkColor: {linkColor}; --linkHoverColor: {linkHoverColor}"
href={linkUrl || "/"}>{linkText}</a
>

View File

@ -1,7 +1,7 @@
<script>
import { getContext } from "svelte"
const { styleable } = getContext("sdk")
const { styleable, linkable } = getContext("sdk")
const component = getContext("component")
export let imageUrl = ""
@ -13,7 +13,7 @@
</script>
<div class="container" use:styleable={$component.styles}>
<a href={destinationUrl}>
<a use:linkable href={destinationUrl}>
<div class="stackedlist">
{#if showImage}
<div class="image-block">

File diff suppressed because it is too large Load Diff