mirror of https://github.com/Budibase/budibase.git
142 changed files with 3887 additions and 2784 deletions
@ -0,0 +1,61 @@ |
|||||
|
#!/usr/bin/env node
|
||||
|
|
||||
|
const os = require("os") |
||||
|
const exec = require("child_process").exec |
||||
|
const fs = require("fs") |
||||
|
const platform = os.platform() |
||||
|
|
||||
|
const windows = platform === "win32" |
||||
|
const mac = platform === "darwin" |
||||
|
const linux = platform === "linux" |
||||
|
|
||||
|
function execute(command) { |
||||
|
return new Promise(resolve => { |
||||
|
exec(command, (err, stdout) => resolve(linux ? !!stdout : true)) |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
async function commandExistsUnix(command) { |
||||
|
const unixCmd = `command -v ${command} 2>/dev/null && { echo >&1 ${command}; exit 0; }` |
||||
|
return execute(command) |
||||
|
} |
||||
|
|
||||
|
async function commandExistsWindows(command) { |
||||
|
if (/[\x00-\x1f<>:"|?*]/.test(command)) { |
||||
|
return false |
||||
|
} |
||||
|
return execute(`where ${command}`) |
||||
|
} |
||||
|
|
||||
|
function commandExists(command) { |
||||
|
return windows ? commandExistsWindows(command) : commandExistsUnix(command) |
||||
|
} |
||||
|
|
||||
|
async function init() { |
||||
|
const docker = commandExists("docker") |
||||
|
const dockerCompose = commandExists("docker-compose") |
||||
|
if (docker && dockerCompose) { |
||||
|
console.log("Docker installed - continuing.") |
||||
|
return |
||||
|
} |
||||
|
if (mac) { |
||||
|
console.log( |
||||
|
"Please install docker by visiting: https://docs.docker.com/docker-for-mac/install/" |
||||
|
) |
||||
|
} else if (windows) { |
||||
|
console.log( |
||||
|
"Please install docker by visiting: https://docs.docker.com/docker-for-windows/install/" |
||||
|
) |
||||
|
} else if (linux) { |
||||
|
console.log("Beginning automated linux installation.") |
||||
|
await execute(`./hosting/scripts/linux/get-docker.sh`) |
||||
|
await execute(`./hosting/scripts/linux/get-docker-compose.sh`) |
||||
|
} else { |
||||
|
console.error( |
||||
|
"Platform unknown - please look online for information about installing docker for our OS." |
||||
|
) |
||||
|
} |
||||
|
console.log("Once installation complete please re-run the setup script.") |
||||
|
process.exit(-1) |
||||
|
} |
||||
|
init() |
||||
@ -0,0 +1,240 @@ |
|||||
|
const sanitize = require("sanitize-s3-objectkey") |
||||
|
const AWS = require("aws-sdk") |
||||
|
const stream = require("stream") |
||||
|
const fetch = require("node-fetch") |
||||
|
const tar = require("tar-fs") |
||||
|
const zlib = require("zlib") |
||||
|
const { promisify } = require("util") |
||||
|
const { join } = require("path") |
||||
|
const fs = require("fs") |
||||
|
const env = require("../environment") |
||||
|
const { budibaseTempDir, ObjectStoreBuckets } = require("./utils") |
||||
|
const { v4 } = require("uuid") |
||||
|
|
||||
|
const streamPipeline = promisify(stream.pipeline) |
||||
|
// use this as a temporary store of buckets that are being created
|
||||
|
const STATE = { |
||||
|
bucketCreationPromises: {}, |
||||
|
} |
||||
|
|
||||
|
const CONTENT_TYPE_MAP = { |
||||
|
html: "text/html", |
||||
|
css: "text/css", |
||||
|
js: "application/javascript", |
||||
|
} |
||||
|
const STRING_CONTENT_TYPES = [ |
||||
|
CONTENT_TYPE_MAP.html, |
||||
|
CONTENT_TYPE_MAP.css, |
||||
|
CONTENT_TYPE_MAP.js, |
||||
|
] |
||||
|
|
||||
|
function publicPolicy(bucketName) { |
||||
|
return { |
||||
|
Version: "2012-10-17", |
||||
|
Statement: [ |
||||
|
{ |
||||
|
Effect: "Allow", |
||||
|
Principal: { |
||||
|
AWS: ["*"], |
||||
|
}, |
||||
|
Action: "s3:GetObject", |
||||
|
Resource: [`arn:aws:s3:::${bucketName}/*`], |
||||
|
}, |
||||
|
], |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const PUBLIC_BUCKETS = [ObjectStoreBuckets.APPS, ObjectStoreBuckets.GLOBAL] |
||||
|
|
||||
|
/** |
||||
|
* Gets a connection to the object store using the S3 SDK. |
||||
|
* @param {string} bucket the name of the bucket which blobs will be uploaded/retrieved from. |
||||
|
* @return {Object} an S3 object store object, check S3 Nodejs SDK for usage. |
||||
|
* @constructor |
||||
|
*/ |
||||
|
exports.ObjectStore = bucket => { |
||||
|
AWS.config.update({ |
||||
|
accessKeyId: env.MINIO_ACCESS_KEY, |
||||
|
secretAccessKey: env.MINIO_SECRET_KEY, |
||||
|
}) |
||||
|
const config = { |
||||
|
s3ForcePathStyle: true, |
||||
|
signatureVersion: "v4", |
||||
|
params: { |
||||
|
Bucket: bucket, |
||||
|
}, |
||||
|
} |
||||
|
if (env.MINIO_URL) { |
||||
|
config.endpoint = env.MINIO_URL |
||||
|
} |
||||
|
return new AWS.S3(config) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Given an object store and a bucket name this will make sure the bucket exists, |
||||
|
* if it does not exist then it will create it. |
||||
|
*/ |
||||
|
exports.makeSureBucketExists = async (client, bucketName) => { |
||||
|
try { |
||||
|
await client |
||||
|
.headBucket({ |
||||
|
Bucket: bucketName, |
||||
|
}) |
||||
|
.promise() |
||||
|
} catch (err) { |
||||
|
const promises = STATE.bucketCreationPromises |
||||
|
if (promises[bucketName]) { |
||||
|
await promises[bucketName] |
||||
|
} else if (err.statusCode === 404) { |
||||
|
// bucket doesn't exist create it
|
||||
|
promises[bucketName] = client |
||||
|
.createBucket({ |
||||
|
Bucket: bucketName, |
||||
|
}) |
||||
|
.promise() |
||||
|
await promises[bucketName] |
||||
|
delete promises[bucketName] |
||||
|
// public buckets are quite hidden in the system, make sure
|
||||
|
// no bucket is set accidentally
|
||||
|
if (PUBLIC_BUCKETS.includes(bucketName)) { |
||||
|
await client |
||||
|
.putBucketPolicy({ |
||||
|
Bucket: bucketName, |
||||
|
Policy: JSON.stringify(publicPolicy(bucketName)), |
||||
|
}) |
||||
|
.promise() |
||||
|
} |
||||
|
} else { |
||||
|
throw err |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Uploads the contents of a file given the required parameters, useful when |
||||
|
* temp files in use (for example file uploaded as an attachment). |
||||
|
*/ |
||||
|
exports.upload = async ({ bucket, filename, path, type, metadata }) => { |
||||
|
const extension = [...filename.split(".")].pop() |
||||
|
const fileBytes = fs.readFileSync(path) |
||||
|
|
||||
|
const objectStore = exports.ObjectStore(bucket) |
||||
|
await exports.makeSureBucketExists(objectStore, bucket) |
||||
|
|
||||
|
const config = { |
||||
|
// windows file paths need to be converted to forward slashes for s3
|
||||
|
Key: sanitize(filename).replace(/\\/g, "/"), |
||||
|
Body: fileBytes, |
||||
|
ContentType: type || CONTENT_TYPE_MAP[extension.toLowerCase()], |
||||
|
} |
||||
|
if (metadata) { |
||||
|
config.Metadata = metadata |
||||
|
} |
||||
|
return objectStore.upload(config).promise() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Similar to the upload function but can be used to send a file stream |
||||
|
* through to the object store. |
||||
|
*/ |
||||
|
exports.streamUpload = async (bucket, filename, stream) => { |
||||
|
const objectStore = exports.ObjectStore(bucket) |
||||
|
await exports.makeSureBucketExists(objectStore, bucket) |
||||
|
|
||||
|
const params = { |
||||
|
Bucket: bucket, |
||||
|
Key: sanitize(filename).replace(/\\/g, "/"), |
||||
|
Body: stream, |
||||
|
} |
||||
|
return objectStore.upload(params).promise() |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* retrieves the contents of a file from the object store, if it is a known content type it |
||||
|
* will be converted, otherwise it will be returned as a buffer stream. |
||||
|
*/ |
||||
|
exports.retrieve = async (bucket, filepath) => { |
||||
|
const objectStore = exports.ObjectStore(bucket) |
||||
|
const params = { |
||||
|
Bucket: bucket, |
||||
|
Key: sanitize(filepath).replace(/\\/g, "/"), |
||||
|
} |
||||
|
const response = await objectStore.getObject(params).promise() |
||||
|
// currently these are all strings
|
||||
|
if (STRING_CONTENT_TYPES.includes(response.ContentType)) { |
||||
|
return response.Body.toString("utf8") |
||||
|
} else { |
||||
|
return response.Body |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Same as retrieval function but puts to a temporary file. |
||||
|
*/ |
||||
|
exports.retrieveToTmp = async (bucket, filepath) => { |
||||
|
const data = await exports.retrieve(bucket, filepath) |
||||
|
const outputPath = join(budibaseTempDir(), v4()) |
||||
|
fs.writeFileSync(outputPath, data) |
||||
|
return outputPath |
||||
|
} |
||||
|
|
||||
|
exports.deleteFolder = async (bucket, folder) => { |
||||
|
const client = exports.ObjectStore(bucket) |
||||
|
const listParams = { |
||||
|
Bucket: bucket, |
||||
|
Prefix: folder, |
||||
|
} |
||||
|
|
||||
|
let response = await client.listObjects(listParams).promise() |
||||
|
if (response.Contents.length === 0) { |
||||
|
return |
||||
|
} |
||||
|
const deleteParams = { |
||||
|
Bucket: bucket, |
||||
|
Delete: { |
||||
|
Objects: [], |
||||
|
}, |
||||
|
} |
||||
|
|
||||
|
response.Contents.forEach(content => { |
||||
|
deleteParams.Delete.Objects.push({ Key: content.Key }) |
||||
|
}) |
||||
|
|
||||
|
response = await client.deleteObjects(deleteParams).promise() |
||||
|
// can only empty 1000 items at once
|
||||
|
if (response.Deleted.length === 1000) { |
||||
|
return exports.deleteFolder(bucket, folder) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
exports.uploadDirectory = async (bucket, localPath, bucketPath) => { |
||||
|
let uploads = [] |
||||
|
const files = fs.readdirSync(localPath, { withFileTypes: true }) |
||||
|
for (let file of files) { |
||||
|
const path = join(bucketPath, file.name) |
||||
|
const local = join(localPath, file.name) |
||||
|
if (file.isDirectory()) { |
||||
|
uploads.push(exports.uploadDirectory(bucket, local, path)) |
||||
|
} else { |
||||
|
uploads.push( |
||||
|
exports.streamUpload(bucket, path, fs.createReadStream(local)) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
await Promise.all(uploads) |
||||
|
} |
||||
|
|
||||
|
exports.downloadTarball = async (url, bucket, path) => { |
||||
|
const response = await fetch(url) |
||||
|
if (!response.ok) { |
||||
|
throw new Error(`unexpected response ${response.statusText}`) |
||||
|
} |
||||
|
|
||||
|
const tmpPath = join(budibaseTempDir(), path) |
||||
|
await streamPipeline(response.body, zlib.Unzip(), tar.extract(tmpPath)) |
||||
|
if (!env.isTest()) { |
||||
|
await exports.uploadDirectory(bucket, tmpPath, path) |
||||
|
} |
||||
|
// return the temporary path incase there is a use for it
|
||||
|
return tmpPath |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
const { join } = require("path") |
||||
|
const { tmpdir } = require("os") |
||||
|
|
||||
|
exports.ObjectStoreBuckets = { |
||||
|
BACKUPS: "backups", |
||||
|
APPS: "prod-budi-app-assets", |
||||
|
TEMPLATES: "templates", |
||||
|
GLOBAL: "global", |
||||
|
} |
||||
|
|
||||
|
exports.budibaseTempDir = function () { |
||||
|
return join(tmpdir(), ".budibase") |
||||
|
} |
||||
@ -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> |
||||
|
|||||
@ -0,0 +1,90 @@ |
|||||
|
export const gradient = (node, config = {}) => { |
||||
|
const defaultConfig = { |
||||
|
points: 12, |
||||
|
saturation: 0.85, |
||||
|
lightness: 0.7, |
||||
|
softness: 0.9, |
||||
|
seed: null, |
||||
|
} |
||||
|
|
||||
|
// Applies a gradient background
|
||||
|
const createGradient = config => { |
||||
|
config = { |
||||
|
...defaultConfig, |
||||
|
...config, |
||||
|
} |
||||
|
const { saturation, lightness, softness, points } = config |
||||
|
const seed = config.seed || Math.random().toString(32).substring(2) |
||||
|
|
||||
|
// Hash function which returns a fixed hash between specified limits
|
||||
|
// for a given seed and a given version
|
||||
|
const rangeHash = (seed, min = 0, max = 100, version = 0) => { |
||||
|
const range = max - min |
||||
|
let hash = range + version |
||||
|
for (let i = 0; i < seed.length * 2 + version; i++) { |
||||
|
hash = (hash << 5) - hash + seed.charCodeAt(i % seed.length) |
||||
|
hash = ((hash & hash) % range) + version |
||||
|
} |
||||
|
return min + (hash % range) |
||||
|
} |
||||
|
|
||||
|
// Generates a random HSL colour using the options specified
|
||||
|
const randomHSL = (seed, version, alpha = 1) => { |
||||
|
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 = rangeHash(seed, 0, 360, version) |
||||
|
const sat = `${rangeHash( |
||||
|
seed, |
||||
|
lowerSaturation, |
||||
|
upperSaturation, |
||||
|
version |
||||
|
)}%` |
||||
|
const light = `${rangeHash( |
||||
|
seed, |
||||
|
lowerLightness, |
||||
|
upperLightness, |
||||
|
version |
||||
|
)}%` |
||||
|
return `hsla(${hue},${sat},${light},${alpha})` |
||||
|
} |
||||
|
|
||||
|
// Generates a radial gradient stop point
|
||||
|
const randomGradientPoint = (seed, version) => { |
||||
|
const lowerTransparency = Math.min(100, softness * 100) |
||||
|
const upperTransparency = Math.min(100, (softness + 0.2) * 100) |
||||
|
const transparency = rangeHash( |
||||
|
seed, |
||||
|
lowerTransparency, |
||||
|
upperTransparency, |
||||
|
version |
||||
|
) |
||||
|
return ( |
||||
|
`radial-gradient(at ` + |
||||
|
`${rangeHash(seed, 0, 100, version)}% ` + |
||||
|
`${rangeHash(seed, 0, 100, version + 1)}%,` + |
||||
|
`${randomHSL(seed, version, saturation)} 0,` + |
||||
|
`transparent ${transparency}%)` |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
let css = `opacity:0.9;background:${randomHSL(seed, 0, 0.7)};` |
||||
|
css += "background-image:" |
||||
|
for (let i = 0; i < points - 1; i++) { |
||||
|
css += `${randomGradientPoint(seed, i)},` |
||||
|
} |
||||
|
css += `${randomGradientPoint(seed, points)};` |
||||
|
node.style = css |
||||
|
} |
||||
|
|
||||
|
// Apply the initial gradient
|
||||
|
createGradient(config) |
||||
|
|
||||
|
return { |
||||
|
// Apply a new gradient
|
||||
|
update: config => { |
||||
|
createGradient(config) |
||||
|
}, |
||||
|
} |
||||
|
} |
||||
@ -1,72 +1,84 @@ |
|||||
<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, |
||||
import download from "downloadjs" |
Layout, |
||||
|
ActionMenu, |
||||
|
MenuItem, |
||||
|
Link, |
||||
|
} from "@budibase/bbui" |
||||
|
import { gradient } from "actions" |
||||
|
import { url } from "@roxi/routify" |
||||
|
|
||||
export let name, _id |
export let app |
||||
|
export let exportApp |
||||
let appExportLoading = false |
export let deleteApp |
||||
|
|
||||
async function exportApp() { |
|
||||
appExportLoading = true |
|
||||
try { |
|
||||
download( |
|
||||
`/api/backups/export?appId=${_id}&appname=${encodeURIComponent(name)}` |
|
||||
) |
|
||||
notifications.success("App Export Complete.") |
|
||||
} catch (err) { |
|
||||
console.error(err) |
|
||||
notifications.error("App Export Failed.") |
|
||||
} finally { |
|
||||
appExportLoading = false |
|
||||
} |
|
||||
} |
|
||||
</script> |
</script> |
||||
|
|
||||
<div class="apps-card"> |
<div class="wrapper"> |
||||
<Heading size="S">{name}</Heading> |
<Layout noPadding gap="XS" alignContent="start"> |
||||
<div class="card-footer" data-cy={`app-${name}`}> |
<div class="preview" use:gradient={{ seed: app.name }} /> |
||||
<ActionButton on:click={() => $goto(`/builder/${_id}`)}> |
<div class="title"> |
||||
Open |
<Link href={$url(`../../app/${app._id}`)}> |
||||
{name} |
<Heading size="XS"> |
||||
→ |
{app.name} |
||||
</ActionButton> |
</Heading> |
||||
{#if appExportLoading} |
</Link> |
||||
<Spinner size="10" /> |
<ActionMenu align="right"> |
||||
{:else} |
<Icon slot="control" name="More" hoverable /> |
||||
<ActionButton icon="Download" quiet /> |
<MenuItem on:click={() => exportApp(app)} icon="Download"> |
||||
{/if} |
Export |
||||
</div> |
</MenuItem> |
||||
|
<MenuItem on:click={() => deleteApp(app)} icon="Delete"> |
||||
|
Delete |
||||
|
</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> |
||||
|
</Layout> |
||||
</div> |
</div> |
||||
|
|
||||
<style> |
<style> |
||||
.apps-card { |
.wrapper { |
||||
background-color: var(--background); |
overflow: hidden; |
||||
padding: var(--spacing-xl) var(--spacing-xl) var(--spacing-xl) |
} |
||||
var(--spacing-xl); |
.preview { |
||||
max-width: 300px; |
height: 135px; |
||||
max-height: 150px; |
border-radius: var(--border-radius-s); |
||||
border-radius: var(--border-radius-m); |
margin-bottom: var(--spacing-s); |
||||
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; |
||||
cursor: pointer; |
flex: 1 1 auto; |
||||
transition: 0.2s all; |
width: 0; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
margin-right: var(--spacing-m); |
||||
} |
} |
||||
|
.title :global(h1) { |
||||
i:hover { |
overflow: hidden; |
||||
color: var(--blue); |
text-overflow: ellipsis; |
||||
|
} |
||||
|
.title :global(h1:hover) { |
||||
|
color: var(--spectrum-global-color-blue-600); |
||||
|
cursor: pointer; |
||||
|
transition: color 130ms ease; |
||||
} |
} |
||||
</style> |
</style> |
||||
|
|||||
@ -1,50 +0,0 @@ |
|||||
<script> |
|
||||
import AppCard from "./AppCard.svelte" |
|
||||
import { Heading, Divider } from "@budibase/bbui" |
|
||||
import Spinner from "components/common/Spinner.svelte" |
|
||||
import { get } from "builderStore/api" |
|
||||
|
|
||||
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) |
|
||||
} |
|
||||
} |
|
||||
</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> |
|
||||
|
|
||||
<style> |
|
||||
.root { |
|
||||
margin-top: 10px; |
|
||||
} |
|
||||
.apps { |
|
||||
margin-top: var(--layout-m); |
|
||||
display: grid; |
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
|
||||
grid-gap: var(--layout-s); |
|
||||
justify-content: start; |
|
||||
} |
|
||||
</style> |
|
||||
@ -0,0 +1,80 @@ |
|||||
|
<script> |
||||
|
import { gradient } from "actions" |
||||
|
import { |
||||
|
Heading, |
||||
|
Button, |
||||
|
Icon, |
||||
|
ActionMenu, |
||||
|
MenuItem, |
||||
|
Link, |
||||
|
} from "@budibase/bbui" |
||||
|
import { url } from "@roxi/routify" |
||||
|
|
||||
|
export let app |
||||
|
export let openApp |
||||
|
export let exportApp |
||||
|
export let deleteApp |
||||
|
export let last |
||||
|
</script> |
||||
|
|
||||
|
<div class="title" class:last> |
||||
|
<div class="preview" use:gradient={{ seed: app.name }} /> |
||||
|
<Link href={$url(`../../app/${app._id}`)}> |
||||
|
<Heading size="XS"> |
||||
|
{app.name} |
||||
|
</Heading> |
||||
|
</Link> |
||||
|
</div> |
||||
|
<div class:last> |
||||
|
Edited {Math.round(Math.random() * 10 + 1)} months ago |
||||
|
</div> |
||||
|
<div class:last> |
||||
|
{#if Math.random() < 0.33} |
||||
|
<div class="status status--open" /> |
||||
|
Open |
||||
|
{:else if Math.random() < 0.33} |
||||
|
<div class="status status--locked-other" /> |
||||
|
Locked by Will Wheaton |
||||
|
{:else} |
||||
|
<div class="status status--locked-you" /> |
||||
|
Locked by you |
||||
|
{/if} |
||||
|
</div> |
||||
|
<div class:last> |
||||
|
<Button on:click={() => openApp(app)} size="S" secondary>Open</Button> |
||||
|
<ActionMenu align="right"> |
||||
|
<Icon hoverable slot="control" name="More" /> |
||||
|
<MenuItem on:click={() => exportApp(app)} icon="Download">Export</MenuItem> |
||||
|
<MenuItem on:click={() => deleteApp(app)} icon="Delete">Delete</MenuItem> |
||||
|
</ActionMenu> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.preview { |
||||
|
height: 40px; |
||||
|
width: 40px; |
||||
|
border-radius: var(--border-radius-s); |
||||
|
} |
||||
|
.title :global(a) { |
||||
|
text-decoration: none; |
||||
|
} |
||||
|
.title :global(h1:hover) { |
||||
|
color: var(--spectrum-global-color-blue-600); |
||||
|
cursor: pointer; |
||||
|
transition: color 130ms ease; |
||||
|
} |
||||
|
.status { |
||||
|
height: 10px; |
||||
|
width: 10px; |
||||
|
border-radius: 50%; |
||||
|
} |
||||
|
.status--locked-you { |
||||
|
background-color: var(--spectrum-global-color-orange-600); |
||||
|
} |
||||
|
.status--locked-other { |
||||
|
background-color: var(--spectrum-global-color-red-600); |
||||
|
} |
||||
|
.status--open { |
||||
|
background-color: var(--spectrum-global-color-green-600); |
||||
|
} |
||||
|
</style> |
||||
@ -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> |
|
||||
@ -1,83 +0,0 @@ |
|||||
<script> |
|
||||
export let step, done, active |
|
||||
</script> |
|
||||
|
|
||||
<div class="container" class:active class:done> |
|
||||
<div class="circle" class:active class:done> |
|
||||
{#if done} |
|
||||
<svg |
|
||||
width="12" |
|
||||
height="10" |
|
||||
viewBox="0 0 12 10" |
|
||||
fill="none" |
|
||||
xmlns="http://www.w3.org/2000/svg" |
|
||||
> |
|
||||
<path |
|
||||
fill-rule="evenodd" |
|
||||
clip-rule="evenodd" |
|
||||
d="M10.1212 0.319527C10.327 0.115582 10.6047 0.000803464 10.8944 |
|
||||
4.20219e-06C11.1841 -0.00079506 11.4624 0.11245 11.6693 |
|
||||
0.315256C11.8762 0.518062 11.9949 0.794134 11.9998 1.08379C12.0048 |
|
||||
1.37344 11.8955 1.65339 11.6957 1.86313L5.82705 9.19893C5.72619 |
|
||||
9.30757 5.60445 9.39475 5.46913 9.45527C5.3338 9.51578 5.18766 9.54839 |
|
||||
5.03944 9.55113C4.89123 9.55388 4.74398 9.52671 4.60651 |
|
||||
9.47124C4.46903 9.41578 4.34416 9.33316 4.23934 9.22833L0.350925 |
|
||||
5.33845C0.242598 5.23751 0.155712 5.11578 0.0954499 4.98054C0.0351876 |
|
||||
4.84529 0.00278364 4.69929 0.00017159 4.55124C-0.00244046 4.4032 |
|
||||
0.024793 4.25615 0.0802466 4.11886C0.1357 3.98157 0.218238 3.85685 |
|
||||
0.322937 3.75215C0.427636 3.64746 0.55235 3.56492 0.68964 |
|
||||
3.50946C0.82693 3.45401 0.973983 3.42678 1.12203 3.42939C1.27007 3.432 |
|
||||
1.41607 3.46441 1.55132 3.52467C1.68657 3.58493 1.80829 3.67182 |
|
||||
1.90923 3.78014L4.98762 6.85706L10.0933 0.35187C10.1024 0.340482 |
|
||||
10.1122 0.329679 10.1227 0.319527H10.1212Z" |
|
||||
fill="var(--background)" |
|
||||
/> |
|
||||
</svg> |
|
||||
{:else}{step}{/if} |
|
||||
</div> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.container::before { |
|
||||
content: ""; |
|
||||
position: absolute; |
|
||||
top: -30px; |
|
||||
width: 1px; |
|
||||
height: 30px; |
|
||||
background: var(--grey-5); |
|
||||
} |
|
||||
.container:first-child::before { |
|
||||
display: none; |
|
||||
} |
|
||||
.container { |
|
||||
position: relative; |
|
||||
height: 45px; |
|
||||
display: grid; |
|
||||
place-items: center; |
|
||||
} |
|
||||
.container.active { |
|
||||
box-shadow: inset 3px 0 0 0 var(--blue); |
|
||||
} |
|
||||
.circle.active { |
|
||||
background: var(--blue); |
|
||||
color: white; |
|
||||
border: none; |
|
||||
} |
|
||||
.circle.done { |
|
||||
background: var(--grey-5); |
|
||||
color: white; |
|
||||
border: none; |
|
||||
} |
|
||||
.circle { |
|
||||
color: var(--grey-5); |
|
||||
font-size: 14px; |
|
||||
font-weight: 500; |
|
||||
display: grid; |
|
||||
place-items: center; |
|
||||
width: 30px; |
|
||||
height: 30px; |
|
||||
border-radius: 50%; |
|
||||
border: 1px solid var(--grey-5); |
|
||||
box-sizing: border-box; |
|
||||
} |
|
||||
</style> |
|
||||
@ -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> |
|
||||
@ -1,121 +0,0 @@ |
|||||
<script> |
|
||||
import { Label, Heading, Input, notifications } from "@budibase/bbui" |
|
||||
|
|
||||
const BYTES_IN_MB = 1000000 |
|
||||
const FILE_SIZE_LIMIT = BYTES_IN_MB * 5 |
|
||||
|
|
||||
export let template |
|
||||
export let values |
|
||||
export let errors |
|
||||
export let touched |
|
||||
|
|
||||
let blurred = { appName: false } |
|
||||
let file |
|
||||
|
|
||||
function handleFile(evt) { |
|
||||
const fileArray = Array.from(evt.target.files) |
|
||||
if (fileArray.some(file => file.size >= FILE_SIZE_LIMIT)) { |
|
||||
notifications.error( |
|
||||
`Files cannot exceed ${ |
|
||||
FILE_SIZE_LIMIT / BYTES_IN_MB |
|
||||
}MB. Please try again with smaller files.` |
|
||||
) |
|
||||
return |
|
||||
} |
|
||||
file = evt.target.files[0] |
|
||||
template.file = file |
|
||||
} |
|
||||
</script> |
|
||||
|
|
||||
<div class="container"> |
|
||||
{#if template?.fromFile} |
|
||||
<Heading size="L">Import your Web App</Heading> |
|
||||
{:else} |
|
||||
<Heading size="L">Create your Web App</Heading> |
|
||||
{/if} |
|
||||
{#if template?.fromFile} |
|
||||
<div class="template"> |
|
||||
<Label extraSmall grey>Import File</Label> |
|
||||
<div class="dropzone"> |
|
||||
<input |
|
||||
id="file-upload" |
|
||||
accept=".txt" |
|
||||
type="file" |
|
||||
on:change={handleFile} |
|
||||
/> |
|
||||
<label for="file-upload" class:uploaded={file}> |
|
||||
{#if file}{file.name}{:else}Import{/if} |
|
||||
</label> |
|
||||
</div> |
|
||||
</div> |
|
||||
{:else if template} |
|
||||
<div class="template"> |
|
||||
<Label extraSmall grey>Selected Template</Label> |
|
||||
<Heading size="S">{template.name}</Heading> |
|
||||
</div> |
|
||||
{/if} |
|
||||
<Input |
|
||||
on:change={() => ($touched.applicationName = true)} |
|
||||
bind:value={$values.applicationName} |
|
||||
label="Web App Name" |
|
||||
placeholder="Enter name of your web application" |
|
||||
error={$touched.applicationName && $errors.applicationName} |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.container { |
|
||||
display: grid; |
|
||||
grid-gap: var(--spacing-xl); |
|
||||
margin-top: var(--spacing-xl); |
|
||||
} |
|
||||
|
|
||||
.template :global(label) { |
|
||||
/* Fix layout due to LH 0 on heading */ |
|
||||
margin-bottom: 16px; |
|
||||
} |
|
||||
|
|
||||
.dropzone { |
|
||||
text-align: center; |
|
||||
display: flex; |
|
||||
align-items: center; |
|
||||
flex-direction: column; |
|
||||
border-radius: 10px; |
|
||||
transition: all 0.3s; |
|
||||
} |
|
||||
|
|
||||
.uploaded { |
|
||||
color: var(--blue); |
|
||||
} |
|
||||
|
|
||||
input[type="file"] { |
|
||||
display: none; |
|
||||
} |
|
||||
|
|
||||
label { |
|
||||
font-family: var(--font-sans); |
|
||||
cursor: pointer; |
|
||||
font-weight: 500; |
|
||||
box-sizing: border-box; |
|
||||
overflow: hidden; |
|
||||
border-radius: var(--border-radius-s); |
|
||||
color: var(--ink); |
|
||||
padding: var(--spacing-m) var(--spacing-l); |
|
||||
transition: all 0.2s ease 0s; |
|
||||
display: inline-flex; |
|
||||
text-rendering: optimizeLegibility; |
|
||||
min-width: auto; |
|
||||
outline: none; |
|
||||
font-feature-settings: "case" 1, "rlig" 1, "calt" 0; |
|
||||
-webkit-box-align: center; |
|
||||
user-select: none; |
|
||||
flex-shrink: 0; |
|
||||
align-items: center; |
|
||||
justify-content: center; |
|
||||
width: 100%; |
|
||||
background-color: var(--grey-2); |
|
||||
font-size: var(--font-size-xs); |
|
||||
line-height: normal; |
|
||||
border: var(--border-transparent); |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,29 +0,0 @@ |
|||||
<script> |
|
||||
import { Select, Heading } from "@budibase/bbui" |
|
||||
|
|
||||
export let values |
|
||||
export let errors |
|
||||
export let touched |
|
||||
</script> |
|
||||
|
|
||||
<div class="container"> |
|
||||
<Heading size="L">What's your role for this app?</Heading> |
|
||||
<Select |
|
||||
bind:value={$values.roleId} |
|
||||
label="Role" |
|
||||
options={[ |
|
||||
{ label: "Admin", value: "ADMIN" }, |
|
||||
{ label: "Power User", value: "POWER_USER" }, |
|
||||
]} |
|
||||
getOptionLabel={option => option.label} |
|
||||
getOptionValue={option => option.value} |
|
||||
error={$errors.roleId} |
|
||||
/> |
|
||||
</div> |
|
||||
|
|
||||
<style> |
|
||||
.container { |
|
||||
display: grid; |
|
||||
grid-gap: var(--spacing-xl); |
|
||||
} |
|
||||
</style> |
|
||||
@ -1,2 +0,0 @@ |
|||||
export { default as Info } from "./Info.svelte" |
|
||||
export { default as User } from "./User.svelte" |
|
||||
@ -0,0 +1,20 @@ |
|||||
|
import { writable } from 'svelte/store' |
||||
|
import api from "builderStore/api" |
||||
|
|
||||
|
export function fetchData (url) { |
||||
|
const store = writable({status: 'LOADING', data: {}, error: {}}) |
||||
|
|
||||
|
async function get() { |
||||
|
store.update(u => ({...u, status: 'SUCCESS'})) |
||||
|
try { |
||||
|
const response = await api.get(url) |
||||
|
store.set({data: await response.json(), status: 'SUCCESS'}) |
||||
|
} catch(e) { |
||||
|
store.set({data: {}, error: e, status: 'ERROR'}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
get() |
||||
|
|
||||
|
return {subscribe: store.subscribe, refresh: get} |
||||
|
} |
||||
@ -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} |
|
||||
@ -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> |
|
||||
@ -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 |
||||
</script> |
$: hasAdminUser = !!$admin?.checklist?.adminUser |
||||
|
|
||||
{#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} |
|
||||
|
|
||||
<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 { |
onMount(async () => { |
||||
grid-column: 2; |
await admin.init() |
||||
overflow: auto; |
await auth.checkAuth() |
||||
} |
loaded = true |
||||
|
}) |
||||
|
|
||||
.ui-nav { |
// Force creation of an admin user if one doesn't exist |
||||
grid-column: 1; |
$: { |
||||
background-color: var(--background); |
if (loaded && !hasAdminUser) { |
||||
padding: 20px; |
$goto("./admin") |
||||
display: flex; |
} |
||||
flex-direction: column; |
|
||||
border-right: var(--border-light); |
|
||||
} |
} |
||||
|
|
||||
.home-logo { |
// Redirect to log in at any time if the user isn't authenticated |
||||
cursor: pointer; |
$: { |
||||
height: 40px; |
if (loaded && hasAdminUser && !$auth.user) { |
||||
margin-bottom: 20px; |
$goto("./auth/login") |
||||
} |
} |
||||
|
|
||||
.home-logo img { |
|
||||
height: 40px; |
|
||||
} |
|
||||
|
|
||||
.nav-section { |
|
||||
margin: 20px 0 0 0; |
|
||||
display: flex; |
|
||||
flex-direction: column; |
|
||||
justify-content: space-between; |
|
||||
height: 100%; |
|
||||
} |
} |
||||
|
</script> |
||||
|
|
||||
.nav-bottom :global(> *) { |
{#if loaded} |
||||
margin-top: 5px; |
<slot /> |
||||
} |
{/if} |
||||
</style> |
|
||||
|
|||||
@ -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> |
||||
@ -0,0 +1,4 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@roxi/routify" |
||||
|
$goto("../portal") |
||||
|
</script> |
||||
@ -0,0 +1,4 @@ |
|||||
|
<script> |
||||
|
import { goto } from "@roxi/routify" |
||||
|
$goto("./login") |
||||
|
</script> |
||||
@ -0,0 +1,5 @@ |
|||||
|
<script> |
||||
|
import LoginForm from "components/login/LoginForm.svelte" |
||||
|
</script> |
||||
|
|
||||
|
<LoginForm /> |
||||
@ -0,0 +1,231 @@ |
|||||
|
<script> |
||||
|
import { |
||||
|
Heading, |
||||
|
Layout, |
||||
|
Button, |
||||
|
ActionButton, |
||||
|
ActionGroup, |
||||
|
ButtonGroup, |
||||
|
Select, |
||||
|
Modal, |
||||
|
ModalContent, |
||||
|
Page, |
||||
|
notifications, |
||||
|
Body, |
||||
|
} from "@budibase/bbui" |
||||
|
import CreateAppModal from "components/start/CreateAppModal.svelte" |
||||
|
import api, { del } from "builderStore/api" |
||||
|
import analytics from "analytics" |
||||
|
import { onMount } from "svelte" |
||||
|
import { apps } from "stores/portal" |
||||
|
import download from "downloadjs" |
||||
|
import { goto } from "@roxi/routify" |
||||
|
import ConfirmDialog from "components/common/ConfirmDialog.svelte" |
||||
|
import AppCard from "components/start/AppCard.svelte" |
||||
|
import AppRow from "components/start/AppRow.svelte" |
||||
|
|
||||
|
let layout = "grid" |
||||
|
let template |
||||
|
let appToDelete |
||||
|
let creationModal |
||||
|
let deletionModal |
||||
|
let creatingApp = false |
||||
|
let loaded = false |
||||
|
|
||||
|
const checkKeys = async () => { |
||||
|
const response = await api.get(`/api/keys/`) |
||||
|
const keys = await response.json() |
||||
|
if (keys.userId) { |
||||
|
analytics.identify(keys.userId) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const initiateAppCreation = () => { |
||||
|
creationModal.show() |
||||
|
creatingApp = true |
||||
|
} |
||||
|
|
||||
|
const initiateAppImport = () => { |
||||
|
template = { fromFile: true } |
||||
|
creationModal.show() |
||||
|
creatingApp = true |
||||
|
} |
||||
|
|
||||
|
const stopAppCreation = () => { |
||||
|
template = null |
||||
|
creatingApp = false |
||||
|
} |
||||
|
|
||||
|
const openApp = app => { |
||||
|
$goto(`../../app/${app._id}`) |
||||
|
} |
||||
|
|
||||
|
const exportApp = app => { |
||||
|
try { |
||||
|
download( |
||||
|
`/api/backups/export?appId=${app._id}&appname=${encodeURIComponent( |
||||
|
app.name |
||||
|
)}` |
||||
|
) |
||||
|
notifications.success("App export complete") |
||||
|
} catch (err) { |
||||
|
console.error(err) |
||||
|
notifications.error("App export failed") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
const deleteApp = app => { |
||||
|
appToDelete = app |
||||
|
deletionModal.show() |
||||
|
} |
||||
|
|
||||
|
const confirmDeleteApp = async () => { |
||||
|
if (!appToDelete) { |
||||
|
return |
||||
|
} |
||||
|
await del(`/api/applications/${appToDelete?._id}`) |
||||
|
await apps.load() |
||||
|
appToDelete = null |
||||
|
} |
||||
|
|
||||
|
onMount(async () => { |
||||
|
checkKeys() |
||||
|
await apps.load() |
||||
|
loaded = true |
||||
|
}) |
||||
|
</script> |
||||
|
|
||||
|
<Page wide> |
||||
|
{#if $apps.length} |
||||
|
<Layout noPadding> |
||||
|
<div class="title"> |
||||
|
<Heading>Apps</Heading> |
||||
|
<ButtonGroup> |
||||
|
<Button secondary on:click={initiateAppImport}>Import app</Button> |
||||
|
<Button cta on:click={initiateAppCreation}>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> |
||||
|
<div |
||||
|
class:appGrid={layout === "grid"} |
||||
|
class:appTable={layout === "table"} |
||||
|
> |
||||
|
{#each $apps as app, idx (app._id)} |
||||
|
<svelte:component |
||||
|
this={layout === "grid" ? AppCard : AppRow} |
||||
|
{app} |
||||
|
{openApp} |
||||
|
{exportApp} |
||||
|
{deleteApp} |
||||
|
last={idx === $apps.length - 1} |
||||
|
/> |
||||
|
{/each} |
||||
|
</div> |
||||
|
</Layout> |
||||
|
{/if} |
||||
|
{#if !$apps.length && !creatingApp && loaded} |
||||
|
<div class="empty-wrapper"> |
||||
|
<Modal inline> |
||||
|
<ModalContent |
||||
|
title="Create your first app" |
||||
|
confirmText="Create app" |
||||
|
showCancelButton={false} |
||||
|
showCloseIcon={false} |
||||
|
onConfirm={initiateAppCreation} |
||||
|
size="M" |
||||
|
> |
||||
|
<div slot="footer"> |
||||
|
<Button on:click={initiateAppImport} secondary>Import app</Button> |
||||
|
</div> |
||||
|
<Body size="S"> |
||||
|
The purpose of the Budibase builder is to help you build beautiful, |
||||
|
powerful applications quickly and easily. |
||||
|
</Body> |
||||
|
</ModalContent> |
||||
|
</Modal> |
||||
|
</div> |
||||
|
{/if} |
||||
|
</Page> |
||||
|
<Modal |
||||
|
bind:this={creationModal} |
||||
|
padding={false} |
||||
|
width="600px" |
||||
|
on:hide={stopAppCreation} |
||||
|
> |
||||
|
<CreateAppModal {template} /> |
||||
|
</Modal> |
||||
|
<ConfirmDialog |
||||
|
bind:this={deletionModal} |
||||
|
title="Confirm deletion" |
||||
|
okText="Delete app" |
||||
|
onOk={confirmDeleteApp} |
||||
|
> |
||||
|
Are you sure you want to delete the app <b>{appToDelete?.name}</b>? |
||||
|
</ConfirmDialog> |
||||
|
|
||||
|
<style> |
||||
|
.title, |
||||
|
.filter { |
||||
|
display: flex; |
||||
|
flex-direction: row; |
||||
|
justify-content: space-between; |
||||
|
align-items: center; |
||||
|
} |
||||
|
|
||||
|
.select { |
||||
|
width: 120px; |
||||
|
} |
||||
|
|
||||
|
.appGrid { |
||||
|
display: grid; |
||||
|
grid-gap: 50px; |
||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
||||
|
} |
||||
|
.appTable { |
||||
|
display: grid; |
||||
|
grid-template-rows: auto; |
||||
|
grid-template-columns: 1fr 1fr 1fr auto; |
||||
|
align-items: center; |
||||
|
} |
||||
|
.appTable :global(> div) { |
||||
|
height: 70px; |
||||
|
display: grid; |
||||
|
align-items: center; |
||||
|
gap: var(--spacing-xl); |
||||
|
grid-template-columns: auto 1fr; |
||||
|
white-space: nowrap; |
||||
|
overflow: hidden; |
||||
|
text-overflow: ellipsis; |
||||
|
padding: 0 var(--spacing-s); |
||||
|
} |
||||
|
.appTable :global(> div:not(.last)) { |
||||
|
border-bottom: var(--border-light); |
||||
|
} |
||||
|
|
||||
|
.empty-wrapper { |
||||
|
flex: 1 1 auto; |
||||
|
height: 100%; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
justify-content: center; |
||||
|
align-items: center; |
||||
|
} |
||||
|
</style> |
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue