mirror of https://github.com/Budibase/budibase.git
53 changed files with 719 additions and 199 deletions
@ -0,0 +1,75 @@ |
|||
<script> |
|||
import { Button, Heading, Body } from "@budibase/bbui" |
|||
import AppCard from "./AppCard.svelte" |
|||
import Spinner from "components/common/Spinner.svelte" |
|||
import api from "builderStore/api" |
|||
|
|||
export let onSelect |
|||
|
|||
async function fetchTemplates() { |
|||
const response = await api.get("/api/templates?type=app") |
|||
return await response.json() |
|||
} |
|||
|
|||
let templatesPromise = fetchTemplates() |
|||
</script> |
|||
|
|||
<div class="root"> |
|||
<Heading medium black>Start With a Template</Heading> |
|||
{#await templatesPromise} |
|||
<div class="spinner-container"> |
|||
<Spinner size="30" /> |
|||
</div> |
|||
{:then templates} |
|||
<div class="templates"> |
|||
{#each templates as template} |
|||
<div class="templates-card"> |
|||
<Heading black medium>{template.name}</Heading> |
|||
<Body medium grey>{template.category}</Body> |
|||
<Body small black>{template.description}</Body> |
|||
<div> |
|||
<img src={template.image} width="300" /> |
|||
</div> |
|||
<div class="card-footer"> |
|||
<Button secondary on:click={() => onSelect(template)}> |
|||
Create {template.name} |
|||
</Button> |
|||
</div> |
|||
</div> |
|||
{/each} |
|||
</div> |
|||
{:catch err} |
|||
<h1 style="color:red">{err}</h1> |
|||
{/await} |
|||
</div> |
|||
|
|||
<style> |
|||
.templates { |
|||
display: grid; |
|||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); |
|||
grid-gap: var(--layout-m); |
|||
justify-content: start; |
|||
} |
|||
|
|||
.templates-card { |
|||
background-color: var(--white); |
|||
padding: var(--spacing-xl); |
|||
border-radius: var(--border-radius-m); |
|||
border: var(--border-dark); |
|||
} |
|||
|
|||
.card-footer { |
|||
margin-top: var(--spacing-m); |
|||
} |
|||
|
|||
h3 { |
|||
font-size: var(--font-size-l); |
|||
font-weight: 600; |
|||
color: var(--ink); |
|||
text-transform: capitalize; |
|||
} |
|||
|
|||
.root { |
|||
margin: 20px 80px; |
|||
} |
|||
</style> |
|||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 105 KiB |
@ -0,0 +1,40 @@ |
|||
#!/usr/bin/env node
|
|||
const { exportTemplateFromApp } = require("../src/utilities/templates") |
|||
const yargs = require("yargs") |
|||
|
|||
// Script to export a chosen budibase app into a package
|
|||
// Usage: ./scripts/exportAppTemplate.js export --name=Funky --instanceId=someInstanceId --appId=appId
|
|||
|
|||
yargs |
|||
.command( |
|||
"export", |
|||
"Export an existing budibase application to the .budibase/templates directory", |
|||
{ |
|||
name: { |
|||
description: "The name of the newly exported template", |
|||
alias: "n", |
|||
type: "string", |
|||
}, |
|||
instanceId: { |
|||
description: "The instanceId to dump the database for", |
|||
alias: "inst", |
|||
type: "string", |
|||
}, |
|||
appId: { |
|||
description: "The appId of the application you want to export", |
|||
alias: "app", |
|||
type: "string", |
|||
}, |
|||
}, |
|||
async args => { |
|||
console.log("Exporting app..") |
|||
const exportPath = await exportTemplateFromApp({ |
|||
templateName: args.name, |
|||
instanceId: args.instanceId, |
|||
appId: args.appId, |
|||
}) |
|||
console.log(`Template ${args.name} exported to ${exportPath}`) |
|||
} |
|||
) |
|||
.help() |
|||
.alias("help", "h").argv |
|||
@ -0,0 +1,44 @@ |
|||
const fetch = require("node-fetch") |
|||
const { |
|||
downloadTemplate, |
|||
exportTemplateFromApp, |
|||
} = require("../../utilities/templates") |
|||
|
|||
const DEFAULT_TEMPLATES_BUCKET = |
|||
"prod-budi-templates.s3-eu-west-1.amazonaws.com" |
|||
|
|||
exports.fetch = async function(ctx) { |
|||
const { type = "app" } = ctx.query |
|||
|
|||
const response = await fetch( |
|||
`https://${DEFAULT_TEMPLATES_BUCKET}/manifest.json` |
|||
) |
|||
const json = await response.json() |
|||
ctx.body = Object.values(json.templates[type]) |
|||
} |
|||
|
|||
exports.downloadTemplate = async function(ctx) { |
|||
const { type, name } = ctx.params |
|||
|
|||
await downloadTemplate(type, name) |
|||
|
|||
ctx.body = { |
|||
message: `template ${type}:${name} downloaded successfully.`, |
|||
} |
|||
} |
|||
|
|||
exports.exportTemplateFromApp = async function(ctx) { |
|||
const { appId, instanceId } = ctx.user |
|||
const { templateName } = ctx.request.body |
|||
|
|||
await exportTemplateFromApp({ |
|||
appId, |
|||
instanceId, |
|||
templateName, |
|||
}) |
|||
|
|||
ctx.status = 200 |
|||
ctx.body = { |
|||
message: `Created template: ${templateName}`, |
|||
} |
|||
} |
|||
@ -0,0 +1,17 @@ |
|||
const Router = require("@koa/router") |
|||
const controller = require("../controllers/templates") |
|||
const authorized = require("../../middleware/authorized") |
|||
const { BUILDER } = require("../../utilities/accessLevels") |
|||
|
|||
const router = Router() |
|||
|
|||
router |
|||
.get("/api/templates", authorized(BUILDER), controller.fetch) |
|||
.get( |
|||
"/api/templates/:type/:name", |
|||
authorized(BUILDER), |
|||
controller.downloadTemplate |
|||
) |
|||
.post("/api/templates", authorized(BUILDER), controller.exportTemplateFromApp) |
|||
|
|||
module.exports = router |
|||
@ -0,0 +1,152 @@ |
|||
const newid = require("./newid") |
|||
|
|||
const DocumentTypes = { |
|||
MODEL: "model", |
|||
RECORD: "record", |
|||
USER: "user", |
|||
AUTOMATION: "automation", |
|||
LINK: "link", |
|||
APP: "app", |
|||
ACCESS_LEVEL: "accesslevel", |
|||
} |
|||
|
|||
exports.DocumentTypes = DocumentTypes |
|||
|
|||
const UNICODE_MAX = "\ufff0" |
|||
|
|||
/** |
|||
* If creating DB allDocs/query params with only a single top level ID this can be used, this |
|||
* is usually the case as most of our docs are top level e.g. models, automations, users and so on. |
|||
* More complex cases such as link docs and records which have multiple levels of IDs that their |
|||
* ID consists of need their own functions to build the allDocs parameters. |
|||
* @param {string} docType The type of document which input params are being built for, e.g. user, |
|||
* link, app, model and so on. |
|||
* @param {string|null} docId The ID of the document minus its type - this is only needed if looking |
|||
* for a singular document. |
|||
* @param {object} otherProps Add any other properties onto the request, e.g. include_docs. |
|||
* @returns {object} Parameters which can then be used with an allDocs request. |
|||
*/ |
|||
function getDocParams(docType, docId = null, otherProps = {}) { |
|||
if (docId == null) { |
|||
docId = "" |
|||
} |
|||
return { |
|||
...otherProps, |
|||
startkey: `${docType}:${docId}`, |
|||
endkey: `${docType}:${docId}${UNICODE_MAX}`, |
|||
} |
|||
} |
|||
|
|||
/** |
|||
* Gets parameters for retrieving models, this is a utility function for the getDocParams function. |
|||
*/ |
|||
exports.getModelParams = (modelId = null, otherProps = {}) => { |
|||
return getDocParams(DocumentTypes.MODEL, modelId, otherProps) |
|||
} |
|||
|
|||
/** |
|||
* Generates a new model ID. |
|||
* @returns {string} The new model ID which the model doc can be stored under. |
|||
*/ |
|||
exports.generateModelID = () => { |
|||
return `${DocumentTypes.MODEL}:${newid()}` |
|||
} |
|||
|
|||
/** |
|||
* Gets the DB allDocs/query params for retrieving a record. |
|||
* @param {string} modelId The model in which the records have been stored. |
|||
* @param {string|null} recordId The ID of the record which is being specifically queried for. This can be |
|||
* left null to get all the records in the model. |
|||
* @param {object} otherProps Any other properties to add to the request. |
|||
* @returns {object} Parameters which can then be used with an allDocs request. |
|||
*/ |
|||
exports.getRecordParams = (modelId, recordId = null, otherProps = {}) => { |
|||
if (modelId == null) { |
|||
throw "Cannot build params for records without a model ID" |
|||
} |
|||
const endOfKey = recordId == null ? `${modelId}:` : `${modelId}:${recordId}` |
|||
return getDocParams(DocumentTypes.RECORD, endOfKey, otherProps) |
|||
} |
|||
|
|||
/** |
|||
* Gets a new record ID for the specified model. |
|||
* @param {string} modelId The model which the record is being created for. |
|||
* @returns {string} The new ID which a record doc can be stored under. |
|||
*/ |
|||
exports.generateRecordID = modelId => { |
|||
return `${DocumentTypes.RECORD}:${modelId}:${newid()}` |
|||
} |
|||
|
|||
/** |
|||
* Gets parameters for retrieving users, this is a utility function for the getDocParams function. |
|||
*/ |
|||
exports.getUserParams = (username = null, otherProps = {}) => { |
|||
return getDocParams(DocumentTypes.USER, username, otherProps) |
|||
} |
|||
|
|||
/** |
|||
* Generates a new user ID based on the passed in username. |
|||
* @param {string} username The username which the ID is going to be built up of. |
|||
* @returns {string} The new user ID which the user doc can be stored under. |
|||
*/ |
|||
exports.generateUserID = username => { |
|||
return `${DocumentTypes.USER}:${username}` |
|||
} |
|||
|
|||
/** |
|||
* Gets parameters for retrieving automations, this is a utility function for the getDocParams function. |
|||
*/ |
|||
exports.getAutomationParams = (automationId = null, otherProps = {}) => { |
|||
return getDocParams(DocumentTypes.AUTOMATION, automationId, otherProps) |
|||
} |
|||
|
|||
/** |
|||
* Generates a new automation ID. |
|||
* @returns {string} The new automation ID which the automation doc can be stored under. |
|||
*/ |
|||
exports.generateAutomationID = () => { |
|||
return `${DocumentTypes.AUTOMATION}:${newid()}` |
|||
} |
|||
|
|||
/** |
|||
* Generates a new link doc ID. This is currently not usable with the alldocs call, |
|||
* instead a view is built to make walking to tree easier. |
|||
* @param {string} modelId1 The ID of the linker model. |
|||
* @param {string} modelId2 The ID of the linked model. |
|||
* @param {string} recordId1 The ID of the linker record. |
|||
* @param {string} recordId2 The ID of the linked record. |
|||
* @returns {string} The new link doc ID which the automation doc can be stored under. |
|||
*/ |
|||
exports.generateLinkID = (modelId1, modelId2, recordId1, recordId2) => { |
|||
return `${DocumentTypes.AUTOMATION}:${modelId1}:${modelId2}:${recordId1}:${recordId2}` |
|||
} |
|||
|
|||
/** |
|||
* Generates a new app ID. |
|||
* @returns {string} The new app ID which the app doc can be stored under. |
|||
*/ |
|||
exports.generateAppID = () => { |
|||
return `${DocumentTypes.APP}:${newid()}` |
|||
} |
|||
|
|||
/** |
|||
* Gets parameters for retrieving apps, this is a utility function for the getDocParams function. |
|||
*/ |
|||
exports.getAppParams = (appId = null, otherProps = {}) => { |
|||
return getDocParams(DocumentTypes.APP, appId, otherProps) |
|||
} |
|||
|
|||
/** |
|||
* Generates a new access level ID. |
|||
* @returns {string} The new access level ID which the access level doc can be stored under. |
|||
*/ |
|||
exports.generateAccessLevelID = () => { |
|||
return `${DocumentTypes.ACCESS_LEVEL}:${newid()}` |
|||
} |
|||
|
|||
/** |
|||
* Gets parameters for retrieving an access level, this is a utility function for the getDocParams function. |
|||
*/ |
|||
exports.getAccessLevelParams = (accessLevelId = null, otherProps = {}) => { |
|||
return getDocParams(DocumentTypes.ACCESS_LEVEL, accessLevelId, otherProps) |
|||
} |
|||
@ -0,0 +1,57 @@ |
|||
const path = require("path") |
|||
const fs = require("fs-extra") |
|||
const os = require("os") |
|||
const fetch = require("node-fetch") |
|||
const stream = require("stream") |
|||
const tar = require("tar-fs") |
|||
const zlib = require("zlib") |
|||
const { promisify } = require("util") |
|||
const streamPipeline = promisify(stream.pipeline) |
|||
const { budibaseAppsDir } = require("./budibaseDir") |
|||
const CouchDB = require("../db") |
|||
|
|||
const DEFAULT_TEMPLATES_BUCKET = |
|||
"prod-budi-templates.s3-eu-west-1.amazonaws.com" |
|||
|
|||
exports.downloadTemplate = async function(type, name) { |
|||
const templateUrl = `https://${DEFAULT_TEMPLATES_BUCKET}/templates/${type}/${name}.tar.gz` |
|||
const response = await fetch(templateUrl) |
|||
|
|||
if (!response.ok) { |
|||
throw new Error( |
|||
`Error downloading template ${type}:${name}: ${response.statusText}` |
|||
) |
|||
} |
|||
|
|||
// stream the response, unzip and extract
|
|||
await streamPipeline( |
|||
response.body, |
|||
zlib.Unzip(), |
|||
tar.extract(path.join(budibaseAppsDir(), "templates", type)) |
|||
) |
|||
|
|||
return path.join(budibaseAppsDir(), "templates", type, name) |
|||
} |
|||
|
|||
exports.exportTemplateFromApp = async function({ |
|||
appId, |
|||
templateName, |
|||
instanceId, |
|||
}) { |
|||
// Copy frontend files
|
|||
const appToExport = path.join(os.homedir(), ".budibase", appId, "pages") |
|||
const templatesDir = path.join(os.homedir(), ".budibase", "templates") |
|||
fs.ensureDirSync(templatesDir) |
|||
|
|||
const templateOutputPath = path.join(templatesDir, templateName) |
|||
fs.copySync(appToExport, `${templateOutputPath}/pages`) |
|||
|
|||
fs.ensureDirSync(path.join(templateOutputPath, "db")) |
|||
const writeStream = fs.createWriteStream(`${templateOutputPath}/db/dump.txt`) |
|||
|
|||
// perform couch dump
|
|||
const instanceDb = new CouchDB(instanceId) |
|||
|
|||
await instanceDb.dump(writeStream) |
|||
return templateOutputPath |
|||
} |
|||
Loading…
Reference in new issue