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",
"packages": [
"packages/backend-core",

View File

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

View File

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

View File

@ -5,6 +5,7 @@ import { notifications } from "@budibase/bbui"
export const createValidationStore = () => {
const DEFAULT = {
values: {},
errors: {},
touched: {},
valid: false,
@ -33,6 +34,9 @@ export const createValidationStore = () => {
case "email":
propertyValidator = string().email().nullable()
break
case "password":
propertyValidator = string().nullable()
break
default:
propertyValidator = string().nullable()
}
@ -41,9 +45,68 @@ export const createValidationStore = () => {
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
}
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 obj = object().shape(validator)
// clear the previous errors
@ -87,5 +150,6 @@ export const createValidationStore = () => {
check,
addValidator,
addValidatorType,
observe,
}
}

View File

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

View File

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