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
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/manage/users", heading: "Manage" },
{ title: "Groups", href: "/portal/manage/groups" },
{ title: "Auth", href: "/portal/manage/auth" },
{ title: "Email", href: "/portal/manage/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/manage/users", heading: "Manage" },
{ title: "Groups", href: "/builder/portal/manage/groups" },
{ title: "Auth", href: "/builder/portal/manage/auth" },
{ title: "Email", href: "/builder/portal/manage/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

@ -1,7 +1,8 @@
<script>
import GoogleLogo from "./logos/Google.svelte"
import GoogleLogo from "./_logos/Google.svelte"
import {
Button,
Page,
Heading,
Divider,
Label,
@ -58,48 +59,51 @@
})
</script>
<section>
<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">
<Page>
<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}
</section>
<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 {
@ -107,8 +111,4 @@
align-items: center;
gap: var(--spacing-s);
}
header {
margin-bottom: 42px;
}
</style>

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,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 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,3 +1,4 @@
export { organisation } from "./organisation"
export { users } from "./users"
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"),
},
],
},
}

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">