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> |
|||
import { writable, get as svelteGet } from "svelte/store" |
|||
import { |
|||
notifications, |
|||
Input, |
|||
Modal, |
|||
ModalContent, |
|||
Body, |
|||
} from "@budibase/bbui" |
|||
import { hostingStore } from "builderStore" |
|||
import { notifications, Input, ModalContent, Body } from "@budibase/bbui" |
|||
import { apps } from "stores/portal" |
|||
import { string, object } from "yup" |
|||
import { onMount } from "svelte" |
|||
import { capitalise } from "helpers" |
|||
import { APP_NAME_REGEX } from "constants" |
|||
|
|||
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" |
|||
), |
|||
} |
|||
import { createValidationStore } from "helpers/validation/yup" |
|||
import * as appValidation from "helpers/validation/yup/app" |
|||
|
|||
export let app |
|||
|
|||
let modal |
|||
let valid = false |
|||
let dirty = false |
|||
$: checkValidity($values, validator) |
|||
$: { |
|||
// prevent validation by setting name to undefined without an app |
|||
if (app) { |
|||
$values.name = app?.name |
|||
} |
|||
} |
|||
const values = writable({ name: "", url: null }) |
|||
const validation = createValidationStore() |
|||
$: validation.check($values) |
|||
|
|||
onMount(async () => { |
|||
await hostingStore.actions.fetchDeployedApps() |
|||
const existingAppNames = svelteGet(hostingStore).deployedAppNames |
|||
validator.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 => { |
|||
return !existingAppNames.some( |
|||
appName => dirty && appName?.toLowerCase() === value.toLowerCase() |
|||
) |
|||
} |
|||
) |
|||
$values.name = app.name |
|||
$values.url = app.url |
|||
setupValidation() |
|||
}) |
|||
|
|||
const checkValidity = async (values, validator) => { |
|||
const obj = object().shape(validator) |
|||
Object.keys(validator).forEach(key => ($errors[key] = null)) |
|||
try { |
|||
await obj.validate(values, { abortEarly: false }) |
|||
} catch (validationErrors) { |
|||
validationErrors.inner?.forEach(error => { |
|||
$errors[error.path] = capitalise(error.message) |
|||
}) |
|||
} |
|||
valid = await obj.isValid(values) |
|||
const setupValidation = async () => { |
|||
const applications = svelteGet(apps) |
|||
appValidation.name(validation, { apps: applications, currentApp: app }) |
|||
appValidation.url(validation, { apps: applications, currentApp: app }) |
|||
// init validation |
|||
validation.check($values) |
|||
} |
|||
|
|||
async function updateApp() { |
|||
try { |
|||
// Update App |
|||
await apps.update(app.instance._id, { name: $values.name.trim() }) |
|||
hide() |
|||
const body = { |
|||
name: $values.name.trim(), |
|||
} |
|||
if ($values.url) { |
|||
body.url = $values.url.trim() |
|||
} |
|||
await apps.update(app.instance._id, body) |
|||
} catch (error) { |
|||
console.error(error) |
|||
notifications.error("Error updating app") |
|||
} |
|||
} |
|||
|
|||
export const show = () => { |
|||
modal.show() |
|||
} |
|||
export const hide = () => { |
|||
modal.hide() |
|||
} |
|||
|
|||
const onCancel = () => { |
|||
hide() |
|||
} |
|||
|
|||
const onShow = () => { |
|||
dirty = false |
|||
// auto add slash to url |
|||
$: { |
|||
if ($values.url && !$values.url.startsWith("/")) { |
|||
$values.url = `/${$values.url}` |
|||
} |
|||
} |
|||
</script> |
|||
|
|||
<Modal bind:this={modal} on:hide={onCancel} on:show={onShow}> |
|||
<ModalContent |
|||
title={"Edit app"} |
|||
confirmText={"Save"} |
|||
onConfirm={updateApp} |
|||
disabled={!(valid && dirty)} |
|||
> |
|||
<Body size="S">Update the name of your app.</Body> |
|||
<Input |
|||
bind:value={$values.name} |
|||
error={$touched.name && $errors.name} |
|||
on:blur={() => ($touched.name = true)} |
|||
on:change={() => (dirty = true)} |
|||
label="Name" |
|||
/> |
|||
</ModalContent> |
|||
</Modal> |
|||
<ModalContent |
|||
title={"Edit app"} |
|||
confirmText={"Save"} |
|||
onConfirm={updateApp} |
|||
disabled={!$validation.valid} |
|||
> |
|||
<Body size="S">Update the name of your app.</Body> |
|||
<Input |
|||
bind:value={$values.name} |
|||
error={$validation.touched.name && $validation.errors.name} |
|||
on:blur={() => ($validation.touched.name = true)} |
|||
label="Name" |
|||
/> |
|||
<Input |
|||
bind:value={$values.url} |
|||
error={$validation.touched.url && $validation.errors.url} |
|||
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", |
|||
"ignore": ["src/**/*.spec.ts", "src/**/*.spec.js"], |
|||
"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