mirror of https://github.com/Budibase/budibase.git
49 changed files with 1867 additions and 571 deletions
@ -1,53 +0,0 @@ |
|||||
import { writable } from "svelte/store" |
|
||||
import { API } from "api" |
|
||||
import { notifications } from "@budibase/bbui" |
|
||||
|
|
||||
const INITIAL_HOSTING_UI_STATE = { |
|
||||
appUrl: "", |
|
||||
deployedApps: {}, |
|
||||
deployedAppNames: [], |
|
||||
deployedAppUrls: [], |
|
||||
} |
|
||||
|
|
||||
export const getHostingStore = () => { |
|
||||
const store = writable({ ...INITIAL_HOSTING_UI_STATE }) |
|
||||
store.actions = { |
|
||||
fetch: async () => { |
|
||||
try { |
|
||||
const urls = await API.getHostingURLs() |
|
||||
store.update(state => { |
|
||||
state.appUrl = urls.app |
|
||||
return state |
|
||||
}) |
|
||||
} catch (error) { |
|
||||
store.update(state => { |
|
||||
state.appUrl = "" |
|
||||
return state |
|
||||
}) |
|
||||
notifications.error("Error fetching hosting URLs") |
|
||||
} |
|
||||
}, |
|
||||
fetchDeployedApps: async () => { |
|
||||
try { |
|
||||
const deployments = await API.getDeployedApps() |
|
||||
store.update(state => { |
|
||||
state.deployedApps = deployments |
|
||||
state.deployedAppNames = Object.values(deployments).map( |
|
||||
app => app.name |
|
||||
) |
|
||||
state.deployedAppUrls = Object.values(deployments).map(app => app.url) |
|
||||
return state |
|
||||
}) |
|
||||
} catch (error) { |
|
||||
store.update(state => { |
|
||||
state.deployedApps = {} |
|
||||
state.deployedAppNames = [] |
|
||||
state.deployedAppUrls = [] |
|
||||
return state |
|
||||
}) |
|
||||
notifications.error("Failed detching deployed apps") |
|
||||
} |
|
||||
}, |
|
||||
} |
|
||||
return store |
|
||||
} |
|
||||
@ -1,119 +1,75 @@ |
|||||
<script> |
<script> |
||||
import { writable, get as svelteGet } from "svelte/store" |
import { writable, get as svelteGet } from "svelte/store" |
||||
import { |
import { notifications, Input, ModalContent, Body } from "@budibase/bbui" |
||||
notifications, |
|
||||
Input, |
|
||||
Modal, |
|
||||
ModalContent, |
|
||||
Body, |
|
||||
} from "@budibase/bbui" |
|
||||
import { hostingStore } from "builderStore" |
|
||||
import { apps } from "stores/portal" |
import { apps } from "stores/portal" |
||||
import { string, object } from "yup" |
|
||||
import { onMount } from "svelte" |
import { onMount } from "svelte" |
||||
import { capitalise } from "helpers" |
import { createValidationStore } from "helpers/validation/yup" |
||||
import { APP_NAME_REGEX } from "constants" |
import * as appValidation from "helpers/validation/yup/app" |
||||
|
|
||||
const values = writable({ name: null }) |
|
||||
const errors = writable({}) |
|
||||
const touched = writable({}) |
|
||||
const validator = { |
|
||||
name: string() |
|
||||
.trim() |
|
||||
.required("Your application must have a name") |
|
||||
.matches( |
|
||||
APP_NAME_REGEX, |
|
||||
"App name must be letters, numbers and spaces only" |
|
||||
), |
|
||||
} |
|
||||
|
|
||||
export let app |
export let app |
||||
|
|
||||
let modal |
const values = writable({ name: "", url: null }) |
||||
let valid = false |
const validation = createValidationStore() |
||||
let dirty = false |
$: validation.check($values) |
||||
$: checkValidity($values, validator) |
|
||||
$: { |
|
||||
// prevent validation by setting name to undefined without an app |
|
||||
if (app) { |
|
||||
$values.name = app?.name |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
onMount(async () => { |
onMount(async () => { |
||||
await hostingStore.actions.fetchDeployedApps() |
$values.name = app.name |
||||
const existingAppNames = svelteGet(hostingStore).deployedAppNames |
$values.url = app.url |
||||
validator.name = string() |
setupValidation() |
||||
.trim() |
|
||||
.required("Your application must have a name") |
|
||||
.matches( |
|
||||
APP_NAME_REGEX, |
|
||||
"App name must be letters, numbers and spaces only" |
|
||||
) |
|
||||
.test( |
|
||||
"non-existing-app-name", |
|
||||
"Another app with the same name already exists", |
|
||||
value => { |
|
||||
return !existingAppNames.some( |
|
||||
appName => dirty && appName?.toLowerCase() === value.toLowerCase() |
|
||||
) |
|
||||
} |
|
||||
) |
|
||||
}) |
}) |
||||
|
|
||||
const checkValidity = async (values, validator) => { |
const setupValidation = async () => { |
||||
const obj = object().shape(validator) |
const applications = svelteGet(apps) |
||||
Object.keys(validator).forEach(key => ($errors[key] = null)) |
appValidation.name(validation, { apps: applications, currentApp: app }) |
||||
try { |
appValidation.url(validation, { apps: applications, currentApp: app }) |
||||
await obj.validate(values, { abortEarly: false }) |
// init validation |
||||
} catch (validationErrors) { |
validation.check($values) |
||||
validationErrors.inner?.forEach(error => { |
|
||||
$errors[error.path] = capitalise(error.message) |
|
||||
}) |
|
||||
} |
|
||||
valid = await obj.isValid(values) |
|
||||
} |
} |
||||
|
|
||||
async function updateApp() { |
async function updateApp() { |
||||
try { |
try { |
||||
// Update App |
// Update App |
||||
await apps.update(app.instance._id, { name: $values.name.trim() }) |
const body = { |
||||
hide() |
name: $values.name.trim(), |
||||
|
} |
||||
|
if ($values.url) { |
||||
|
body.url = $values.url.trim() |
||||
|
} |
||||
|
await apps.update(app.instance._id, body) |
||||
} catch (error) { |
} catch (error) { |
||||
|
console.error(error) |
||||
notifications.error("Error updating app") |
notifications.error("Error updating app") |
||||
} |
} |
||||
} |
} |
||||
|
|
||||
export const show = () => { |
// auto add slash to url |
||||
modal.show() |
$: { |
||||
} |
if ($values.url && !$values.url.startsWith("/")) { |
||||
export const hide = () => { |
$values.url = `/${$values.url}` |
||||
modal.hide() |
} |
||||
} |
|
||||
|
|
||||
const onCancel = () => { |
|
||||
hide() |
|
||||
} |
|
||||
|
|
||||
const onShow = () => { |
|
||||
dirty = false |
|
||||
} |
} |
||||
</script> |
</script> |
||||
|
|
||||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}> |
<ModalContent |
||||
<ModalContent |
title={"Edit app"} |
||||
title={"Edit app"} |
confirmText={"Save"} |
||||
confirmText={"Save"} |
onConfirm={updateApp} |
||||
onConfirm={updateApp} |
disabled={!$validation.valid} |
||||
disabled={!(valid && dirty)} |
> |
||||
> |
<Body size="S">Update the name of your app.</Body> |
||||
<Body size="S">Update the name of your app.</Body> |
<Input |
||||
<Input |
bind:value={$values.name} |
||||
bind:value={$values.name} |
error={$validation.touched.name && $validation.errors.name} |
||||
error={$touched.name && $errors.name} |
on:blur={() => ($validation.touched.name = true)} |
||||
on:blur={() => ($touched.name = true)} |
label="Name" |
||||
on:change={() => (dirty = true)} |
/> |
||||
label="Name" |
<Input |
||||
/> |
bind:value={$values.url} |
||||
</ModalContent> |
error={$validation.touched.url && $validation.errors.url} |
||||
</Modal> |
on:blur={() => ($validation.touched.url = true)} |
||||
|
label="URL" |
||||
|
placeholder={$values.name |
||||
|
? "/" + encodeURIComponent($values.name).toLowerCase() |
||||
|
: "/"} |
||||
|
/> |
||||
|
</ModalContent> |
||||
|
|||||
@ -0,0 +1,83 @@ |
|||||
|
import { string, mixed } from "yup" |
||||
|
import { APP_NAME_REGEX, APP_URL_REGEX } from "constants" |
||||
|
|
||||
|
export const name = (validation, { apps, currentApp } = { apps: [] }) => { |
||||
|
validation.addValidator( |
||||
|
"name", |
||||
|
string() |
||||
|
.trim() |
||||
|
.required("Your application must have a name") |
||||
|
.matches( |
||||
|
APP_NAME_REGEX, |
||||
|
"App name must be letters, numbers and spaces only" |
||||
|
) |
||||
|
.test( |
||||
|
"non-existing-app-name", |
||||
|
"Another app with the same name already exists", |
||||
|
value => { |
||||
|
if (!value) { |
||||
|
// exit early, above validator will fail
|
||||
|
return true |
||||
|
} |
||||
|
if (currentApp) { |
||||
|
// filter out the current app if present
|
||||
|
apps = apps.filter(app => app.appId !== currentApp.appId) |
||||
|
} |
||||
|
return !apps |
||||
|
.map(app => app.name) |
||||
|
.some(appName => appName.toLowerCase() === value.toLowerCase()) |
||||
|
} |
||||
|
) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const url = (validation, { apps, currentApp } = { apps: [] }) => { |
||||
|
validation.addValidator( |
||||
|
"url", |
||||
|
string() |
||||
|
.nullable() |
||||
|
.matches(APP_URL_REGEX, "App URL must not contain spaces") |
||||
|
.test( |
||||
|
"non-existing-app-url", |
||||
|
"Another app with the same URL already exists", |
||||
|
value => { |
||||
|
// url is nullable
|
||||
|
if (!value) { |
||||
|
return true |
||||
|
} |
||||
|
if (currentApp) { |
||||
|
// filter out the current app if present
|
||||
|
apps = apps.filter(app => app.appId !== currentApp.appId) |
||||
|
} |
||||
|
return !apps |
||||
|
.map(app => app.url) |
||||
|
.some(appUrl => appUrl?.toLowerCase() === value.toLowerCase()) |
||||
|
} |
||||
|
) |
||||
|
.test("valid-url", "Not a valid URL", value => { |
||||
|
// url is nullable
|
||||
|
if (!value) { |
||||
|
return true |
||||
|
} |
||||
|
// make it clear that this is a url path and cannot be a full url
|
||||
|
return ( |
||||
|
value.startsWith("/") && |
||||
|
!value.includes("http") && |
||||
|
!value.includes("www") && |
||||
|
!value.includes(".") && |
||||
|
value.length > 1 // just '/' is not valid
|
||||
|
) |
||||
|
}) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
export const file = (validation, { template } = {}) => { |
||||
|
const templateToUse = |
||||
|
template && Object.keys(template).length === 0 ? null : template |
||||
|
validation.addValidator( |
||||
|
"file", |
||||
|
templateToUse?.fromFile |
||||
|
? mixed().required("Please choose a file to import") |
||||
|
: null |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,66 @@ |
|||||
|
import { capitalise } from "helpers" |
||||
|
import { object } from "yup" |
||||
|
import { writable, get } from "svelte/store" |
||||
|
import { notifications } from "@budibase/bbui" |
||||
|
|
||||
|
export const createValidationStore = () => { |
||||
|
const DEFAULT = { |
||||
|
errors: {}, |
||||
|
touched: {}, |
||||
|
valid: false, |
||||
|
} |
||||
|
|
||||
|
const validator = {} |
||||
|
const validation = writable(DEFAULT) |
||||
|
|
||||
|
const addValidator = (propertyName, propertyValidator) => { |
||||
|
if (!propertyValidator || !propertyName) { |
||||
|
return |
||||
|
} |
||||
|
validator[propertyName] = propertyValidator |
||||
|
} |
||||
|
|
||||
|
const check = async values => { |
||||
|
const obj = object().shape(validator) |
||||
|
// clear the previous errors
|
||||
|
const properties = Object.keys(validator) |
||||
|
properties.forEach(property => (get(validation).errors[property] = null)) |
||||
|
|
||||
|
let validationError = false |
||||
|
try { |
||||
|
await obj.validate(values, { abortEarly: false }) |
||||
|
} catch (error) { |
||||
|
if (!error.inner) { |
||||
|
notifications.error("Unexpected validation error", error) |
||||
|
validationError = true |
||||
|
} else { |
||||
|
error.inner.forEach(err => { |
||||
|
validation.update(store => { |
||||
|
store.errors[err.path] = capitalise(err.message) |
||||
|
return store |
||||
|
}) |
||||
|
}) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
let valid |
||||
|
if (properties.length && !validationError) { |
||||
|
valid = await obj.isValid(values) |
||||
|
} else { |
||||
|
// don't say valid until validators have been loaded
|
||||
|
valid = false |
||||
|
} |
||||
|
|
||||
|
validation.update(store => { |
||||
|
store.valid = valid |
||||
|
return store |
||||
|
}) |
||||
|
} |
||||
|
|
||||
|
return { |
||||
|
subscribe: validation.subscribe, |
||||
|
set: validation.set, |
||||
|
check, |
||||
|
addValidator, |
||||
|
} |
||||
|
} |
||||
File diff suppressed because it is too large
@ -1,5 +1,5 @@ |
|||||
{ |
{ |
||||
"watch": ["src", "../auth"], |
"watch": ["src", "../backend-core"], |
||||
"ext": "js,ts,json", |
"ext": "js,ts,json", |
||||
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], |
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], |
||||
"exec": "ts-node src/index.ts" |
"exec": "ts-node src/index.ts" |
||||
|
|||||
@ -1,22 +0,0 @@ |
|||||
const CouchDB = require("../../db") |
|
||||
const { getDeployedApps } = require("../../utilities/workerRequests") |
|
||||
const { getScopedConfig } = require("@budibase/backend-core/db") |
|
||||
const { Configs } = require("@budibase/backend-core/constants") |
|
||||
const { checkSlashesInUrl } = require("../../utilities") |
|
||||
|
|
||||
exports.fetchUrls = async ctx => { |
|
||||
const appId = ctx.appId |
|
||||
const db = new CouchDB(appId) |
|
||||
const settings = await getScopedConfig(db, { type: Configs.SETTINGS }) |
|
||||
let appUrl = "http://localhost:10000/app" |
|
||||
if (settings && settings["platformUrl"]) { |
|
||||
appUrl = checkSlashesInUrl(`${settings["platformUrl"]}/app`) |
|
||||
} |
|
||||
ctx.body = { |
|
||||
app: appUrl, |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
exports.getDeployedApps = async ctx => { |
|
||||
ctx.body = await getDeployedApps() |
|
||||
} |
|
||||
@ -1,13 +0,0 @@ |
|||||
const Router = require("@koa/router") |
|
||||
const controller = require("../controllers/hosting") |
|
||||
const authorized = require("../../middleware/authorized") |
|
||||
const { BUILDER } = require("@budibase/backend-core/permissions") |
|
||||
|
|
||||
const router = Router() |
|
||||
|
|
||||
router |
|
||||
.get("/api/hosting/urls", authorized(BUILDER), controller.fetchUrls) |
|
||||
// this isn't risky, doesn't return anything about apps other than names and URLs
|
|
||||
.get("/api/hosting/apps", controller.getDeployedApps) |
|
||||
|
|
||||
module.exports = router |
|
||||
@ -1,36 +0,0 @@ |
|||||
// mock out node fetch for this
|
|
||||
jest.mock("node-fetch") |
|
||||
|
|
||||
const { checkBuilderEndpoint } = require("./utilities/TestFunctions") |
|
||||
const setup = require("./utilities") |
|
||||
|
|
||||
describe("/hosting", () => { |
|
||||
let request = setup.getRequest() |
|
||||
let config = setup.getConfig() |
|
||||
let app |
|
||||
|
|
||||
afterAll(setup.afterAll) |
|
||||
|
|
||||
beforeEach(async () => { |
|
||||
app = await config.init() |
|
||||
}) |
|
||||
|
|
||||
describe("fetchUrls", () => { |
|
||||
it("should be able to fetch current app URLs", async () => { |
|
||||
const res = await request |
|
||||
.get(`/api/hosting/urls`) |
|
||||
.set(config.defaultHeaders()) |
|
||||
.expect("Content-Type", /json/) |
|
||||
.expect(200) |
|
||||
expect(res.body.app).toEqual(`http://localhost:10000/app`) |
|
||||
}) |
|
||||
|
|
||||
it("should apply authorization to endpoint", async () => { |
|
||||
await checkBuilderEndpoint({ |
|
||||
config, |
|
||||
method: "GET", |
|
||||
url: `/api/hosting/urls`, |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
}) |
|
||||
@ -1,3 +1,3 @@ |
|||||
{ |
{ |
||||
"watch": ["src", "../auth"] |
"watch": ["src", "../backend-core"] |
||||
} |
} |
||||
@ -1,30 +0,0 @@ |
|||||
const { |
|
||||
getAllApps, |
|
||||
getDeployedAppID, |
|
||||
isProdAppID, |
|
||||
} = require("@budibase/backend-core/db") |
|
||||
const CouchDB = require("../../db") |
|
||||
|
|
||||
const URL_REGEX_SLASH = /\/|\\/g |
|
||||
|
|
||||
exports.getApps = async ctx => { |
|
||||
const apps = await getAllApps(CouchDB, { all: true }) |
|
||||
const body = {} |
|
||||
for (let app of apps) { |
|
||||
let url = app.url || encodeURI(`${app.name}`) |
|
||||
url = `/${url.replace(URL_REGEX_SLASH, "")}` |
|
||||
const appId = app.appId, |
|
||||
isProd = isProdAppID(app.appId) |
|
||||
if (!body[url]) { |
|
||||
body[url] = { |
|
||||
appId: getDeployedAppID(appId), |
|
||||
name: app.name, |
|
||||
url, |
|
||||
deployed: isProd, |
|
||||
} |
|
||||
} else { |
|
||||
body[url].deployed = isProd || body[url].deployed |
|
||||
} |
|
||||
} |
|
||||
ctx.body = body |
|
||||
} |
|
||||
@ -1,8 +0,0 @@ |
|||||
const Router = require("@koa/router") |
|
||||
const controller = require("../controllers/app") |
|
||||
|
|
||||
const router = Router() |
|
||||
|
|
||||
router.get("/api/apps", controller.getApps) |
|
||||
|
|
||||
module.exports = router |
|
||||
File diff suppressed because it is too large
Loading…
Reference in new issue