Merge remote-tracking branch 'origin/develop' into feature/app-settings-section
This commit is contained in:
commit
3843b98cf8
|
@ -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",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue