1
0
Fork 0
mirror of synced 2024-07-15 19:25:55 +12:00

Merge branch 'app-list' into admin/user-management-ui

This commit is contained in:
Keviin Åberg Kultalahti 2021-05-07 12:15:58 +02:00
commit ca06e5d16e
83 changed files with 557 additions and 684 deletions

View file

@ -16,6 +16,11 @@ static_resources:
- name: local_services - name: local_services
domains: ["*"] domains: ["*"]
routes: 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/" } - match: { prefix: "/db/" }
route: route:
cluster: couchdb-service cluster: couchdb-service
@ -33,7 +38,11 @@ static_resources:
route: route:
cluster: server-dev cluster: server-dev
- match: { prefix: "/" } - match: { path: "/" }
route:
cluster: builder-dev
- match: { prefix: "/builder/" }
route: route:
cluster: builder-dev cluster: builder-dev
@ -41,7 +50,7 @@ static_resources:
route: route:
cluster: builder-dev cluster: builder-dev
prefix_rewrite: "/builder/" prefix_rewrite: "/builder/"
# minio is on the default route because this works # minio is on the default route because this works
# best, minio + AWS SDK doesn't handle path proxy # best, minio + AWS SDK doesn't handle path proxy
- match: { prefix: "/" } - match: { prefix: "/" }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,62 +1,16 @@
<script> <script>
export let forAttr = "", import "@spectrum-css/fieldlabel/dist/index-vars.css"
extraSmall = false,
small = false, export let size = "M"
medium = false,
large = false,
extraLarge = false,
white = false,
grey = false,
black = false
</script> </script>
<label <label class={`spectrum-FieldLabel spectrum-FieldLabel--size${size}`}>
class="bb-label"
class:extraSmall
class:small
class:medium
class:large
class:extraLarge
class:white
class:grey
class:black
for={forAttr}
>
<slot /> <slot />
</label> </label>
<style> <style>
.bb-label { label {
font-family: var(--font-sans); padding: 0;
font-weight: 500; white-space: nowrap;
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);
} }
</style> </style>

View file

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

View file

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

View file

@ -3,11 +3,20 @@
export let size = "M" export let size = "M"
export let serif = false export let serif = false
export let noPadding = false
</script> </script>
<p <p
class="spectrum-Body class:spectrum-Body--size{size}" class:noPadding
class="spectrum-Body spectrum-Body--size{size}"
class:spectrum-Body--serif={serif} class:spectrum-Body--serif={serif}
> >
<slot /> <slot />
</p> </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> <script>
import { Input, Select, DatePicker, Toggle, TextArea } from "@budibase/bbui" import { Input, Select, DatePicker, Toggle, TextArea } from "@budibase/bbui"
import Dropzone from "components/common/Dropzone.svelte" import Dropzone from "components/common/Dropzone.svelte"
import { capitalise } from "../../../helpers" import { capitalise } from "helpers"
import LinkedRowSelector from "components/common/LinkedRowSelector.svelte" import LinkedRowSelector from "components/common/LinkedRowSelector.svelte"
export let defaultValue export let defaultValue

View file

@ -59,7 +59,7 @@
const selectRelationship = ({ tableId, rowId, fieldName }) => { const selectRelationship = ({ tableId, rowId, fieldName }) => {
$goto( $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, Body,
ModalContent, ModalContent,
} from "@budibase/bbui" } from "@budibase/bbui"
import { capitalise } from "../../../../helpers" import { capitalise } from "helpers"
export let resourceId export let resourceId
export let permissions export let permissions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,50 +1,25 @@
<script> <script>
import { onMount } from "svelte"
import AppCard from "./AppCard.svelte" import AppCard from "./AppCard.svelte"
import { Heading, Divider } from "@budibase/bbui" import { apps } from "stores/portal"
import Spinner from "components/common/Spinner.svelte"
import { get } from "builderStore/api"
let promise = getApps() onMount(apps.load)
async function getApps() {
const res = await get("/api/applications")
const json = await res.json()
if (res.ok) {
return json
} else {
throw new Error(json)
}
}
</script> </script>
<div class="root"> {#if $apps.length}
<Heading size="M">Your Apps</Heading> <div class="appList">
<Divider size="M" /> {#each $apps as app}
{#await promise} <AppCard {...app} />
<div class="spinner-container"> {/each}
<Spinner size="30" /> </div>
</div> {:else}
{:then apps} <div>No apps found.</div>
<div class="apps"> {/if}
{#each apps as app}
<AppCard {...app} />
{/each}
</div>
{:catch err}
<h1 style="color:red">{err}</h1>
{/await}
</div>
<style> <style>
.root { .appList {
margin-top: 10px;
}
.apps {
margin-top: var(--layout-m);
display: grid; display: grid;
grid-gap: 50px;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
grid-gap: var(--layout-s);
justify-content: start;
} }
</style> </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) const userResp = await api.post(`/api/users/metadata/self`, user)
await userResp.json() await userResp.json()
$goto(`./${appJson._id}`) window.location = `/builder/app/${appJson._id}`
} catch (error) { } catch (error) {
console.error(error) console.error(error)
notifications.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> <script>
import { import { onMount } from "svelte"
SideNavigation as Navigation, import { goto } from "@roxi/routify"
SideNavigationItem as Item,
} from "@budibase/bbui"
import { auth } from "stores/backend" import { auth } from "stores/backend"
import LoginForm from "components/login/LoginForm.svelte" import { admin } from "stores/portal"
import BuilderSettingsButton from "components/start/BuilderSettingsButton.svelte"
import LogoutButton from "components/start/LogoutButton.svelte"
import Logo from "/assets/budibase-logo.svg"
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> </script>
{#if $auth} {#if loaded}
{#if $auth.user} <slot />
<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} {/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 ThemeEditorDropdown from "components/settings/ThemeEditorDropdown.svelte"
import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte" import FeedbackNavLink from "components/feedback/FeedbackNavLink.svelte"
import { get } from "builderStore/api" 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 Logo from "/assets/bb-logo.svg"
import { capitalise } from "../../../helpers" import { capitalise } from "helpers"
// Get Package and set store // Get Package and set store
export let application export let application
@ -60,7 +60,7 @@
<img <img
src={Logo} src={Logo}
alt="budibase icon" alt="budibase icon"
on:click={() => $goto(`/builder/`)} on:click={() => $goto(`../../portal/`)}
/> />
</button> </button>

View file

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

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

View file

@ -45,12 +45,12 @@
<Layout noPadding> <Layout noPadding>
<div class="intro"> <div class="intro">
<Heading size="M">General</Heading> <Heading size="M">General</Heading>
<Body <Body>
>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Hic vero, aut Lorem ipsum, dolor sit amet consectetur adipisicing elit. Hic vero, aut
culpa provident sunt ratione! Voluptas doloremque, dicta nisi velit culpa provident sunt ratione! Voluptas doloremque, dicta nisi velit
perspiciatis, ratione vel blanditiis totam, nam voluptate repellat perspiciatis, ratione vel blanditiis totam, nam voluptate repellat
aperiam fuga!</Body aperiam fuga!
> </Body>
</div> </div>
<Divider size="S" /> <Divider size="S" />
<div class="information"> <div class="information">
@ -58,7 +58,7 @@
<Body>Here you can update your logo and organization name.</Body> <Body>Here you can update your logo and organization name.</Body>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label>Organization name</Label> <Label size="L">Organization name</Label>
<Input thin bind:value={company} /> <Input thin bind:value={company} />
</div> </div>
<!-- <div class="field"> <!-- <div class="field">
@ -72,13 +72,13 @@
<Divider size="S" /> <Divider size="S" />
<div class="analytics"> <div class="analytics">
<Heading size="S">Analytics</Heading> <Heading size="S">Analytics</Heading>
<Body <Body>
>If you would like to send analytics that help us make Budibase better, If you would like to send analytics that help us make Budibase better,
please let us know below.</Body please let us know below.
> </Body>
<div class="fields"> <div class="fields">
<div class="field"> <div class="field">
<Label>Send Analytics to Budibase</Label> <Label size="L">Send Analytics to Budibase</Label>
<Toggle text="" value={!analyticsDisabled} /> <Toggle text="" value={!analyticsDisabled} />
</div> </div>
</div> </div>
@ -97,7 +97,8 @@
} }
.field { .field {
display: grid; display: grid;
grid-template-columns: 30% 1fr; grid-template-columns: 32% 1fr;
align-items: center;
} }
.file { .file {
max-width: 30ch; 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,43 +0,0 @@
<svg
width="18"
height="18"
viewBox="0 0 268 268"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clip-path="url(#clip0)">
<path
d="M58.8037 109.043C64.0284 93.2355 74.1116 79.4822 87.615 69.7447C101.118
60.0073 117.352 54.783 134 54.8172C152.872 54.8172 169.934 61.5172 183.334
72.4828L222.328 33.5C198.566 12.7858 168.114 0 134 0C81.1817 0 35.711
30.1277 13.8467 74.2583L58.8037 109.043Z"
fill="#EA4335"
/>
<path
d="M179.113 201.145C166.942 208.995 151.487 213.183 134 213.183C117.418
213.217 101.246 208.034 87.7727 198.369C74.2993 188.703 64.2077 175.044
58.9265 159.326L13.8132 193.574C24.8821 215.978 42.012 234.828 63.2572
247.984C84.5024 261.14 109.011 268.075 134 268C166.752 268 198.041 256.353
221.48 234.5L179.125 201.145H179.113Z"
fill="#34A853"
/>
<path
d="M221.48 234.5C245.991 211.631 261.903 177.595 261.903 134C261.903
126.072 260.686 117.552 258.866 109.634H134V161.414H205.869C202.329
178.823 192.804 192.301 179.125 201.145L221.48 234.5Z"
fill="#4A90E2"
/>
<path
d="M58.9265 159.326C56.1947 151.162 54.8068 142.609 54.8172 134C54.8172
125.268 56.213 116.882 58.8037 109.043L13.8467 74.2584C4.64957 92.825
-0.0915078 113.28 1.86708e-05 134C1.86708e-05 155.44 4.96919 175.652
13.8132 193.574L58.9265 159.326Z"
fill="#FBBC05"
/>
</g>
<defs>
<clipPath id="clip0">
<rect width="268" height="268" fill="white" />
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

View file

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

View file

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

View file

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

View file

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