Merge remote-tracking branch 'origin/develop' into feature/app-settings-section

This commit is contained in:
Dean 2023-06-15 12:51:25 +01:00
commit 3843b98cf8
6 changed files with 184 additions and 19 deletions

View File

@ -1,5 +1,5 @@
{ {
"version": "2.7.20-alpha.0", "version": "2.7.20-alpha.1",
"npmClient": "yarn", "npmClient": "yarn",
"packages": [ "packages": [
"packages/backend-core", "packages/backend-core",

View File

@ -4,20 +4,60 @@
Toggle, Toggle,
Body, Body,
InlineAlert, InlineAlert,
Input,
notifications, notifications,
} from "@budibase/bbui" } from "@budibase/bbui"
import { createValidationStore } from "helpers/validation/yup"
export let app export let app
export let published export let published
let excludeRows = false let includeInternalTablesRows = true
let encypt = true
$: title = published ? "Export published app" : "Export latest app" let password = null
$: confirmText = published ? "Export published" : "Export latest" const validation = createValidationStore()
validation.addValidatorType("password", "password", true)
$: validation.observe("password", password)
const Step = { CONFIG: "config", SET_PASSWORD: "set_password" }
let currentStep = Step.CONFIG
$: exportButtonText = published ? "Export published" : "Export latest"
$: stepConfig = {
[Step.CONFIG]: {
title: published ? "Export published app" : "Export latest app",
confirmText: encypt ? "Continue" : exportButtonText,
onConfirm: () => {
if (!encypt) {
exportApp()
} else {
currentStep = Step.SET_PASSWORD
return false
}
},
isValid: true,
},
[Step.SET_PASSWORD]: {
title: "Add password to encrypt your export",
confirmText: exportButtonText,
onConfirm: async () => {
await validation.check({ password })
if (!$validation.valid) {
return false
}
exportApp(password)
},
isValid: $validation.valid,
},
}
const exportApp = async () => { const exportApp = async () => {
const id = published ? app.prodId : app.devId const id = published ? app.prodId : app.devId
const url = `/api/backups/export?appId=${id}` const url = `/api/backups/export?appId=${id}`
await downloadFile(url, { excludeRows }) await downloadFile(url, {
excludeRows: !includeInternalTablesRows,
encryptPassword: password,
})
} }
async function downloadFile(url, body) { async function downloadFile(url, body) {
@ -56,13 +96,33 @@
} }
</script> </script>
<ModalContent {title} {confirmText} onConfirm={exportApp}> <ModalContent
<InlineAlert title={stepConfig[currentStep].title}
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys." confirmText={stepConfig[currentStep].confirmText}
/> onConfirm={stepConfig[currentStep].onConfirm}
<Body disabled={!stepConfig[currentStep].isValid}
>Apps can be exported with or without data that is within internal tables - >
select this below.</Body {#if currentStep === Step.CONFIG}
> <Body>
<Toggle text="Exclude Rows" bind:value={excludeRows} /> <Toggle
text="Export rows from internal tables"
bind:value={includeInternalTablesRows}
/>
<Toggle text="Encrypt my export" bind:value={encypt} />
</Body>
{#if !encypt}
<InlineAlert
header="Do not share your budibase application exports publicly as they may contain sensitive information such as database credentials or secret keys."
/>
{/if}
{/if}
{#if currentStep === Step.SET_PASSWORD}
<Input
type="password"
label="Password"
placeholder="Type here..."
bind:value={password}
error={$validation.errors.password}
/>
{/if}
</ModalContent> </ModalContent>

View File

@ -6,7 +6,6 @@ export function createValidationStore(initialValue, ...validators) {
let touched = false let touched = false
const value = writable(initialValue || "") const value = writable(initialValue || "")
const error = derived(value, $v => validate($v, validators))
const touchedStore = derived(value, () => { const touchedStore = derived(value, () => {
if (!touched) { if (!touched) {
touched = true touched = true
@ -14,6 +13,10 @@ export function createValidationStore(initialValue, ...validators) {
} }
return touched return touched
}) })
const error = derived(
[value, touchedStore],
([$v, $t]) => $t && validate($v, validators)
)
return [value, error, touchedStore] return [value, error, touchedStore]
} }

View File

@ -5,6 +5,7 @@ import { notifications } from "@budibase/bbui"
export const createValidationStore = () => { export const createValidationStore = () => {
const DEFAULT = { const DEFAULT = {
values: {},
errors: {}, errors: {},
touched: {}, touched: {},
valid: false, valid: false,
@ -33,6 +34,9 @@ export const createValidationStore = () => {
case "email": case "email":
propertyValidator = string().email().nullable() propertyValidator = string().email().nullable()
break break
case "password":
propertyValidator = string().nullable()
break
default: default:
propertyValidator = string().nullable() propertyValidator = string().nullable()
} }
@ -41,9 +45,68 @@ export const createValidationStore = () => {
propertyValidator = propertyValidator.required() propertyValidator = propertyValidator.required()
} }
// We want to do this after the possible required validation, to prioritise the required error
switch (type) {
case "password":
propertyValidator = propertyValidator.min(8)
break
}
validator[propertyName] = propertyValidator validator[propertyName] = propertyValidator
} }
const observe = async (propertyName, value) => {
const values = get(validation).values
let fieldIsValid
if (!values.hasOwnProperty(propertyName)) {
// Initial setup
values[propertyName] = value
return
}
if (value === values[propertyName]) {
return
}
const obj = object().shape(validator)
try {
validation.update(store => {
store.errors[propertyName] = null
return store
})
await obj.validateAt(propertyName, { [propertyName]: value })
fieldIsValid = true
} catch (error) {
const [fieldError] = error.errors
if (fieldError) {
validation.update(store => {
store.errors[propertyName] = capitalise(fieldError)
store.valid = false
return store
})
}
}
if (fieldIsValid) {
// Validate the rest of the fields
try {
await obj.validate(
{ ...values, [propertyName]: value },
{ abortEarly: false }
)
validation.update(store => {
store.valid = true
return store
})
} catch {
validation.update(store => {
store.valid = false
return store
})
}
}
}
const check = async values => { const check = async values => {
const obj = object().shape(validator) const obj = object().shape(validator)
// clear the previous errors // clear the previous errors
@ -87,5 +150,6 @@ export const createValidationStore = () => {
check, check,
addValidator, addValidator,
addValidatorType, addValidatorType,
observe,
} }
} }

View File

@ -22,6 +22,7 @@
import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte" import ImportRestQueriesModal from "components/backend/DatasourceNavigator/modals/ImportRestQueriesModal.svelte"
import { API } from "api" import { API } from "api"
import { DatasourceFeature } from "@budibase/types" import { DatasourceFeature } from "@budibase/types"
import Spinner from "components/common/Spinner.svelte"
const querySchema = { const querySchema = {
name: {}, name: {},
@ -33,6 +34,7 @@
let isValid = true let isValid = true
let integration, baseDatasource, datasource let integration, baseDatasource, datasource
let queryList let queryList
let loading = false
$: baseDatasource = $datasources.selected $: baseDatasource = $datasources.selected
$: queryList = $queries.list.filter( $: queryList = $queries.list.filter(
@ -65,9 +67,11 @@
} }
const saveDatasource = async () => { const saveDatasource = async () => {
loading = true
if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) { if (integration.features?.[DatasourceFeature.CONNECTION_CHECKING]) {
const valid = await validateConfig() const valid = await validateConfig()
if (!valid) { if (!valid) {
loading = false
return false return false
} }
} }
@ -82,6 +86,8 @@
baseDatasource = cloneDeep(datasource) baseDatasource = cloneDeep(datasource)
} catch (err) { } catch (err) {
notifications.error(`Error saving datasource: ${err}`) notifications.error(`Error saving datasource: ${err}`)
} finally {
loading = false
} }
} }
@ -119,8 +125,17 @@
<Divider /> <Divider />
<div class="config-header"> <div class="config-header">
<Heading size="S">Configuration</Heading> <Heading size="S">Configuration</Heading>
<Button disabled={!changed || !isValid} cta on:click={saveDatasource}> <Button
Save disabled={!changed || !isValid || loading}
cta
on:click={saveDatasource}
>
<div class="save-button-content">
{#if loading}
<Spinner size="10">Save</Spinner>
{/if}
Save
</div>
</Button> </Button>
</div> </div>
<IntegrationConfigForm <IntegrationConfigForm
@ -216,4 +231,10 @@
flex-direction: column; flex-direction: column;
gap: var(--spacing-m); gap: var(--spacing-m);
} }
.save-button-content {
display: flex;
align-items: center;
gap: var(--spacing-s);
}
</style> </style>

View File

@ -20,7 +20,7 @@ import Sql from "./base/sql"
import { PostgresColumn } from "./base/types" import { PostgresColumn } from "./base/types"
import { escapeDangerousCharacters } from "../utilities" import { escapeDangerousCharacters } from "../utilities"
import { Client, types } from "pg" import { Client, ClientConfig, types } from "pg"
// Return "date" and "timestamp" types as plain strings. // Return "date" and "timestamp" types as plain strings.
// This lets us reference the original stored timezone. // This lets us reference the original stored timezone.
@ -42,6 +42,8 @@ interface PostgresConfig {
schema: string schema: string
ssl?: boolean ssl?: boolean
ca?: string ca?: string
clientKey?: string
clientCert?: string
rejectUnauthorized?: boolean rejectUnauthorized?: boolean
} }
@ -98,6 +100,19 @@ const SCHEMA: Integration = {
required: false, required: false,
}, },
ca: { ca: {
display: "Server CA",
type: DatasourceFieldType.LONGFORM,
default: false,
required: false,
},
clientKey: {
display: "Client key",
type: DatasourceFieldType.LONGFORM,
default: false,
required: false,
},
clientCert: {
display: "Client cert",
type: DatasourceFieldType.LONGFORM, type: DatasourceFieldType.LONGFORM,
default: false, default: false,
required: false, required: false,
@ -144,12 +159,14 @@ class PostgresIntegration extends Sql implements DatasourcePlus {
super(SqlClient.POSTGRES) super(SqlClient.POSTGRES)
this.config = config this.config = config
let newConfig = { let newConfig: ClientConfig = {
...this.config, ...this.config,
ssl: this.config.ssl ssl: this.config.ssl
? { ? {
rejectUnauthorized: this.config.rejectUnauthorized, rejectUnauthorized: this.config.rejectUnauthorized,
ca: this.config.ca, ca: this.config.ca,
key: this.config.clientKey,
cert: this.config.clientCert,
} }
: undefined, : undefined,
} }