Merge remote-tracking branch 'origin/develop' into feature/delete-multiple-button-action
This commit is contained in:
commit
ccb82e5575
|
@ -28,3 +28,4 @@ BB_ADMIN_USER_PASSWORD=
|
|||
|
||||
# A path that is watched for plugin bundles. Any bundles found are imported automatically/
|
||||
PLUGINS_DIR=
|
||||
ROLLING_LOG_MAX_SIZE=
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"version": "2.8.6-alpha.3",
|
||||
"version": "2.8.10-alpha.0",
|
||||
"npmClient": "yarn",
|
||||
"packages": [
|
||||
"packages/*"
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"pouchdb": "7.3.0",
|
||||
"pouchdb-find": "7.2.2",
|
||||
"redlock": "4.2.0",
|
||||
"rotating-file-stream": "3.1.0",
|
||||
"sanitize-s3-objectkey": "0.0.1",
|
||||
"semver": "7.3.7",
|
||||
"tar-fs": "2.1.1",
|
||||
|
|
|
@ -433,6 +433,9 @@ export class QueryBuilder<T> {
|
|||
if (!value) {
|
||||
return null
|
||||
}
|
||||
if (typeof value === "boolean") {
|
||||
return `(*:* AND !${key}:${value})`
|
||||
}
|
||||
return `!${key}:${builder.preprocess(value, allPreProcessingOpts)}`
|
||||
})
|
||||
}
|
||||
|
|
|
@ -47,7 +47,10 @@ function httpLogging() {
|
|||
return process.env.HTTP_LOGGING
|
||||
}
|
||||
|
||||
function findVersion() {
|
||||
function getPackageJsonFields(): {
|
||||
VERSION: string
|
||||
SERVICE_NAME: string
|
||||
} {
|
||||
function findFileInAncestors(
|
||||
fileName: string,
|
||||
currentDir: string
|
||||
|
@ -69,10 +72,14 @@ function findVersion() {
|
|||
try {
|
||||
const packageJsonFile = findFileInAncestors("package.json", process.cwd())
|
||||
const content = readFileSync(packageJsonFile!, "utf-8")
|
||||
return JSON.parse(content).version
|
||||
const parsedContent = JSON.parse(content)
|
||||
return {
|
||||
VERSION: parsedContent.version,
|
||||
SERVICE_NAME: parsedContent.name,
|
||||
}
|
||||
} catch {
|
||||
// throwing an error here is confusing/causes backend-core to be hard to import
|
||||
return undefined
|
||||
return { VERSION: "", SERVICE_NAME: "" }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -154,13 +161,14 @@ const environment = {
|
|||
ENABLE_SSO_MAINTENANCE_MODE: selfHosted
|
||||
? process.env.ENABLE_SSO_MAINTENANCE_MODE
|
||||
: false,
|
||||
VERSION: findVersion(),
|
||||
...getPackageJsonFields(),
|
||||
DISABLE_PINO_LOGGER: process.env.DISABLE_PINO_LOGGER,
|
||||
_set(key: any, value: any) {
|
||||
process.env[key] = value
|
||||
// @ts-ignore
|
||||
environment[key] = value
|
||||
},
|
||||
ROLLING_LOG_MAX_SIZE: process.env.ROLLING_LOG_MAX_SIZE || "10M",
|
||||
}
|
||||
|
||||
// clean up any environment variable edge cases
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export * as correlation from "./correlation/correlation"
|
||||
export { logger } from "./pino/logger"
|
||||
export * from "./alerts"
|
||||
export * as system from "./system"
|
||||
|
||||
// turn off or on context logging i.e. tenantId, appId etc
|
||||
export let LOG_CONTEXT = true
|
||||
|
|
|
@ -1,10 +1,15 @@
|
|||
import env from "../../environment"
|
||||
import pino, { LoggerOptions } from "pino"
|
||||
import pinoPretty from "pino-pretty"
|
||||
|
||||
import { IdentityType } from "@budibase/types"
|
||||
|
||||
import env from "../../environment"
|
||||
import * as context from "../../context"
|
||||
import * as correlation from "../correlation"
|
||||
import { IdentityType } from "@budibase/types"
|
||||
import { LOG_CONTEXT } from "../index"
|
||||
|
||||
import { localFileDestination } from "../system"
|
||||
|
||||
// LOGGER
|
||||
|
||||
let pinoInstance: pino.Logger | undefined
|
||||
|
@ -16,22 +21,27 @@ if (!env.DISABLE_PINO_LOGGER) {
|
|||
return { level: label.toUpperCase() }
|
||||
},
|
||||
bindings: () => {
|
||||
return {}
|
||||
return {
|
||||
service: env.SERVICE_NAME,
|
||||
}
|
||||
},
|
||||
},
|
||||
timestamp: () => `,"timestamp":"${new Date(Date.now()).toISOString()}"`,
|
||||
}
|
||||
|
||||
const destinations: pino.DestinationStream[] = []
|
||||
|
||||
if (env.isDev()) {
|
||||
pinoOptions.transport = {
|
||||
target: "pino-pretty",
|
||||
options: {
|
||||
singleLine: true,
|
||||
},
|
||||
}
|
||||
destinations.push(pinoPretty({ singleLine: true }))
|
||||
}
|
||||
|
||||
pinoInstance = pino(pinoOptions)
|
||||
if (env.SELF_HOSTED) {
|
||||
destinations.push(localFileDestination())
|
||||
}
|
||||
|
||||
pinoInstance = destinations.length
|
||||
? pino(pinoOptions, pino.multistream(destinations))
|
||||
: pino(pinoOptions)
|
||||
|
||||
// CONSOLE OVERRIDES
|
||||
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
import fs from "fs"
|
||||
import path from "path"
|
||||
import * as rfs from "rotating-file-stream"
|
||||
|
||||
import env from "../environment"
|
||||
import { budibaseTempDir } from "../objectStore"
|
||||
|
||||
const logsFileName = `budibase.log`
|
||||
const budibaseLogsHistoryFileName = "budibase-logs-history.txt"
|
||||
|
||||
const logsPath = path.join(budibaseTempDir(), "systemlogs")
|
||||
|
||||
function getFullPath(fileName: string) {
|
||||
return path.join(logsPath, fileName)
|
||||
}
|
||||
|
||||
export function getSingleFileMaxSizeInfo(totalMaxSize: string) {
|
||||
const regex = /(\d+)([A-Za-z])/
|
||||
const match = totalMaxSize?.match(regex)
|
||||
if (!match) {
|
||||
console.warn(`totalMaxSize does not have a valid value`, {
|
||||
totalMaxSize,
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
const size = +match[1]
|
||||
const unit = match[2]
|
||||
if (size === 1) {
|
||||
switch (unit) {
|
||||
case "B":
|
||||
return { size: `${size}B`, totalHistoryFiles: 1 }
|
||||
case "K":
|
||||
return { size: `${(size * 1000) / 2}B`, totalHistoryFiles: 1 }
|
||||
case "M":
|
||||
return { size: `${(size * 1000) / 2}K`, totalHistoryFiles: 1 }
|
||||
case "G":
|
||||
return { size: `${(size * 1000) / 2}M`, totalHistoryFiles: 1 }
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
if (size % 2 === 0) {
|
||||
return { size: `${size / 2}${unit}`, totalHistoryFiles: 1 }
|
||||
}
|
||||
|
||||
return { size: `1${unit}`, totalHistoryFiles: size - 1 }
|
||||
}
|
||||
|
||||
export function localFileDestination() {
|
||||
const fileInfo = getSingleFileMaxSizeInfo(env.ROLLING_LOG_MAX_SIZE)
|
||||
const outFile = rfs.createStream(logsFileName, {
|
||||
// As we have a rolling size, we want to half the max size
|
||||
size: fileInfo?.size,
|
||||
path: logsPath,
|
||||
maxFiles: fileInfo?.totalHistoryFiles || 1,
|
||||
immutable: true,
|
||||
history: budibaseLogsHistoryFileName,
|
||||
initialRotation: false,
|
||||
})
|
||||
|
||||
return outFile
|
||||
}
|
||||
|
||||
export function getLogReadStream() {
|
||||
const streams = []
|
||||
const historyFile = getFullPath(budibaseLogsHistoryFileName)
|
||||
if (fs.existsSync(historyFile)) {
|
||||
const fileContent = fs.readFileSync(historyFile, "utf-8")
|
||||
const historyFiles = fileContent.split("\n")
|
||||
for (const historyFile of historyFiles.filter(x => x)) {
|
||||
streams.push(fs.readFileSync(historyFile))
|
||||
}
|
||||
}
|
||||
|
||||
streams.push(fs.readFileSync(getFullPath(logsFileName)))
|
||||
|
||||
const combinedContent = Buffer.concat(streams)
|
||||
return combinedContent
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import { getSingleFileMaxSizeInfo } from "../system"
|
||||
|
||||
describe("system", () => {
|
||||
describe("getSingleFileMaxSizeInfo", () => {
|
||||
it.each([
|
||||
["100B", "50B"],
|
||||
["200K", "100K"],
|
||||
["20M", "10M"],
|
||||
["4G", "2G"],
|
||||
])(
|
||||
"Halving even number (%s) returns halved size and 1 history file (%s)",
|
||||
(totalValue, expectedMaxSize) => {
|
||||
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||
expect(result).toEqual({
|
||||
size: expectedMaxSize,
|
||||
totalHistoryFiles: 1,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
["5B", "1B", 4],
|
||||
["17K", "1K", 16],
|
||||
["21M", "1M", 20],
|
||||
["3G", "1G", 2],
|
||||
])(
|
||||
"Halving an odd number (%s) returns as many files as size (-1) (%s)",
|
||||
(totalValue, expectedMaxSize, totalHistoryFiles) => {
|
||||
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||
expect(result).toEqual({
|
||||
size: expectedMaxSize,
|
||||
totalHistoryFiles,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
["1B", "1B"],
|
||||
["1K", "500B"],
|
||||
["1M", "500K"],
|
||||
["1G", "500M"],
|
||||
])(
|
||||
"Halving '%s' returns halved unit (%s)",
|
||||
(totalValue, expectedMaxSize) => {
|
||||
const result = getSingleFileMaxSizeInfo(totalValue)
|
||||
expect(result).toEqual({
|
||||
size: expectedMaxSize,
|
||||
totalHistoryFiles: 1,
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each([[undefined], [""], ["50"], ["wrongvalue"]])(
|
||||
"Halving wrongly formatted value ('%s') returns undefined",
|
||||
totalValue => {
|
||||
const result = getSingleFileMaxSizeInfo(totalValue!)
|
||||
expect(result).toBeUndefined()
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
|
@ -17,7 +17,7 @@
|
|||
$: text = getText(filters)
|
||||
|
||||
const getText = filters => {
|
||||
const count = filters?.length
|
||||
const count = filters?.filter(filter => filter.field)?.length
|
||||
return count ? `Filter (${count})` : "Filter"
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<script>
|
||||
import { Icon, Heading } from "@budibase/bbui"
|
||||
|
||||
export let showClose = false
|
||||
export let onClose = () => {}
|
||||
export let heading = ""
|
||||
</script>
|
||||
|
||||
<section class="page">
|
||||
<div class="closeButton">
|
||||
{#if showClose}
|
||||
<Icon hoverable name="Close" on:click={onClose} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="heading">
|
||||
<Heading weight="light">{heading}</Heading>
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.heading {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -70,8 +70,9 @@
|
|||
} set`
|
||||
</script>
|
||||
|
||||
<div class="action-count">{actionText}</div>
|
||||
<ActionButton on:click={openDrawer}>Define actions</ActionButton>
|
||||
<div class="action-editor">
|
||||
<ActionButton on:click={openDrawer}>{actionText}</ActionButton>
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={drawer} title={"Actions"}>
|
||||
<svelte:fragment slot="description">
|
||||
|
@ -89,9 +90,7 @@
|
|||
</Drawer>
|
||||
|
||||
<style>
|
||||
.action-count {
|
||||
padding-top: 6px;
|
||||
padding-bottom: var(--spacing-s);
|
||||
font-weight: 600;
|
||||
.action-editor :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
let drawer
|
||||
let boundValue
|
||||
|
||||
$: text = getText(value)
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchema($currentAsset, datasource)
|
||||
$: options = allowCellEditing
|
||||
|
@ -31,6 +32,17 @@
|
|||
allowLinks: true,
|
||||
})
|
||||
|
||||
const getText = value => {
|
||||
if (!value?.length) {
|
||||
return "All columns"
|
||||
}
|
||||
let text = `${value.length} column`
|
||||
if (value.length !== 1) {
|
||||
text += "s"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const getSchema = (asset, datasource) => {
|
||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||
|
||||
|
@ -76,7 +88,7 @@
|
|||
</script>
|
||||
|
||||
<div class="column-editor">
|
||||
<ActionButton on:click={open}>Configure columns</ActionButton>
|
||||
<ActionButton on:click={open}>{text}</ActionButton>
|
||||
</div>
|
||||
<Drawer bind:this={drawer} title="Columns">
|
||||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
|
|
|
@ -12,25 +12,36 @@
|
|||
export let componentInstance
|
||||
export let value = []
|
||||
|
||||
const convertOldColumnFormat = oldColumns => {
|
||||
if (typeof oldColumns?.[0] === "string") {
|
||||
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
||||
}
|
||||
}
|
||||
|
||||
$: convertOldColumnFormat(value)
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let drawer
|
||||
let boundValue
|
||||
|
||||
$: text = getText(value)
|
||||
$: convertOldColumnFormat(value)
|
||||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchema($currentAsset, datasource)
|
||||
$: options = Object.keys(schema || {})
|
||||
$: sanitisedValue = getValidColumns(value, options)
|
||||
$: updateBoundValue(sanitisedValue)
|
||||
|
||||
const getText = value => {
|
||||
if (!value?.length) {
|
||||
return "All fields"
|
||||
}
|
||||
let text = `${value.length} field`
|
||||
if (value.length !== 1) {
|
||||
text += "s"
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
const convertOldColumnFormat = oldColumns => {
|
||||
if (typeof oldColumns?.[0] === "string") {
|
||||
value = oldColumns.map(field => ({ name: field, displayName: field }))
|
||||
}
|
||||
}
|
||||
|
||||
const getSchema = (asset, datasource) => {
|
||||
const schema = getSchemaForDatasource(asset, datasource).schema
|
||||
|
||||
|
@ -75,7 +86,10 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<ActionButton on:click={open}>Configure fields</ActionButton>
|
||||
<div class="field-configuration">
|
||||
<ActionButton on:click={open}>{text}</ActionButton>
|
||||
</div>
|
||||
|
||||
<Drawer bind:this={drawer} title="Form Fields">
|
||||
<svelte:fragment slot="description">
|
||||
Configure the fields in your form.
|
||||
|
@ -83,3 +97,9 @@
|
|||
<Button cta slot="buttons" on:click={save}>Save</Button>
|
||||
<ColumnDrawer slot="body" bind:columns={boundValue} {options} {schema} />
|
||||
</Drawer>
|
||||
|
||||
<style>
|
||||
.field-configuration :global(.spectrum-ActionButton) {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
$: datasource = getDatasourceForProvider($currentAsset, componentInstance)
|
||||
$: schema = getSchemaForDatasource($currentAsset, datasource)?.schema
|
||||
$: schemaFields = Object.values(schema || {})
|
||||
$: text = getText(value)
|
||||
$: text = getText(value?.filter(filter => filter.field))
|
||||
|
||||
async function saveFilter() {
|
||||
dispatch("change", tempValue)
|
||||
|
|
|
@ -15,8 +15,6 @@
|
|||
import { createValidationStore } from "helpers/validation/yup"
|
||||
import * as appValidation from "helpers/validation/yup/app"
|
||||
import TemplateCard from "components/common/TemplateCard.svelte"
|
||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||
import { Roles } from "constants/backend"
|
||||
import { lowercase } from "helpers"
|
||||
|
||||
export let template
|
||||
|
@ -142,21 +140,6 @@
|
|||
// Create user
|
||||
await auth.setInitInfo({})
|
||||
|
||||
// Create a default home screen if no template was selected
|
||||
if (template == null) {
|
||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||
defaultScreenTemplate.routing.route = "/home"
|
||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||
try {
|
||||
await store.actions.screens.save(defaultScreenTemplate)
|
||||
} catch (err) {
|
||||
console.error("Could not create a default application screen", err)
|
||||
notifications.warning(
|
||||
"Encountered an issue creating the default screen."
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$goto(`/builder/app/${createdApp.instance._id}`)
|
||||
} catch (error) {
|
||||
creating = false
|
||||
|
|
|
@ -7,12 +7,13 @@
|
|||
} from "stores/backend"
|
||||
|
||||
import { hasData } from "stores/selectors"
|
||||
import { Icon, notifications, Heading, Body } from "@budibase/bbui"
|
||||
import { notifications, Body } from "@budibase/bbui"
|
||||
import { params, goto } from "@roxi/routify"
|
||||
import CreateExternalDatasourceModal from "./_components/CreateExternalDatasourceModal/index.svelte"
|
||||
import CreateInternalTableModal from "./_components/CreateInternalTableModal.svelte"
|
||||
import DatasourceOption from "./_components/DatasourceOption.svelte"
|
||||
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
|
||||
import CreationPage from "components/common/CreationPage.svelte"
|
||||
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
|
||||
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
|
||||
|
||||
|
@ -46,16 +47,11 @@
|
|||
bind:this={externalDatasourceModal}
|
||||
/>
|
||||
|
||||
<div class="page">
|
||||
<div class="closeButton">
|
||||
{#if hasData($datasources, $tables)}
|
||||
<Icon hoverable name="Close" on:click={$goto("./table")} />
|
||||
{/if}
|
||||
</div>
|
||||
<div class="heading">
|
||||
<Heading weight="light">Add new data source</Heading>
|
||||
</div>
|
||||
|
||||
<CreationPage
|
||||
showClose={hasData($datasources, $tables)}
|
||||
onClose={() => $goto("./table")}
|
||||
heading="Add new data source"
|
||||
>
|
||||
<div class="subHeading">
|
||||
<Body>Get started with our Budibase DB</Body>
|
||||
<div
|
||||
|
@ -113,30 +109,13 @@
|
|||
</DatasourceOption>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</CreationPage>
|
||||
|
||||
<style>
|
||||
.page {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
height: 38px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.subHeading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
<script>
|
||||
import { tables } from "stores/backend"
|
||||
import { ModalContent, Body, Layout, Icon, Heading } from "@budibase/bbui"
|
||||
import blankScreenPreview from "./blankScreenPreview.png"
|
||||
import listScreenPreview from "./listScreenPreview.png"
|
||||
|
||||
export let onConfirm
|
||||
export let onCancel
|
||||
|
||||
let listScreenModeKey = "autoCreate"
|
||||
let blankScreenModeKey = "blankScreen"
|
||||
|
||||
let selectedScreenMode
|
||||
|
||||
const confirmScreenSelection = async () => {
|
||||
await onConfirm(selectedScreenMode)
|
||||
}
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<ModalContent
|
||||
title="Add screens"
|
||||
confirmText="Continue"
|
||||
cancelText="Cancel"
|
||||
onConfirm={confirmScreenSelection}
|
||||
{onCancel}
|
||||
disabled={!selectedScreenMode}
|
||||
size="M"
|
||||
>
|
||||
<Layout noPadding gap="S">
|
||||
<div
|
||||
class="screen-type item blankView"
|
||||
class:selected={selectedScreenMode == blankScreenModeKey}
|
||||
on:click={() => {
|
||||
selectedScreenMode = blankScreenModeKey
|
||||
}}
|
||||
>
|
||||
<div class="content screen-type-wrap">
|
||||
<img
|
||||
alt="blank screen preview"
|
||||
class="preview"
|
||||
src={blankScreenPreview}
|
||||
/>
|
||||
<div class="screen-type-text">
|
||||
<Heading size="XS">Blank screen</Heading>
|
||||
<Body size="S">Add an empty blank screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="color: var(--spectrum-global-color-green-600); float: right"
|
||||
>
|
||||
<div
|
||||
class={`checkmark-spacing ${
|
||||
selectedScreenMode == blankScreenModeKey ? "visible" : ""
|
||||
}`}
|
||||
>
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="listViewTitle">
|
||||
<Heading size="XS">Quickly create a screen from your data</Heading>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="screen-type item"
|
||||
class:selected={selectedScreenMode == listScreenModeKey}
|
||||
on:click={() => {
|
||||
selectedScreenMode = listScreenModeKey
|
||||
}}
|
||||
class:disabled={!$tables.list.filter(table => table._id !== "ta_users")
|
||||
.length}
|
||||
>
|
||||
<div class="content screen-type-wrap">
|
||||
<img
|
||||
alt="list screen preview"
|
||||
class="preview"
|
||||
src={listScreenPreview}
|
||||
/>
|
||||
<div class="screen-type-text">
|
||||
<Heading size="XS">List view</Heading>
|
||||
<Body size="S">
|
||||
Create, edit and view your data in a list view screen with side
|
||||
panel
|
||||
</Body>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="color: var(--spectrum-global-color-green-600); float: right"
|
||||
>
|
||||
<div
|
||||
class={`checkmark-spacing ${
|
||||
selectedScreenMode == listScreenModeKey ? "visible" : ""
|
||||
}`}
|
||||
>
|
||||
<Icon size="S" name="CheckmarkCircle" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
</ModalContent>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.screen-type-wrap {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
.disabled {
|
||||
opacity: 0.3;
|
||||
pointer-events: none;
|
||||
}
|
||||
.checkmark-spacing {
|
||||
margin-right: var(--spacing-m);
|
||||
opacity: 0;
|
||||
}
|
||||
.content {
|
||||
letter-spacing: 0px;
|
||||
}
|
||||
.item {
|
||||
cursor: pointer;
|
||||
grid-gap: var(--spectrum-alias-grid-margin-xsmall);
|
||||
background: var(--spectrum-alias-background-color-secondary);
|
||||
transition: 0.3s all;
|
||||
border: 1px solid var(--spectrum-global-color-gray-300);
|
||||
border-radius: 4px;
|
||||
border-width: 1px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.item:hover,
|
||||
.selected {
|
||||
background: var(--spectrum-alias-background-color-tertiary);
|
||||
}
|
||||
.screen-type-wrap .screen-type-text {
|
||||
padding-left: var(--spectrum-alias-item-padding-xl);
|
||||
}
|
||||
.screen-type-wrap .screen-type-text :global(h1) {
|
||||
padding-bottom: var(--spacing-xs);
|
||||
}
|
||||
.screen-type-wrap :global(.spectrum-Icon) {
|
||||
min-width: var(--spectrum-icon-size-m);
|
||||
}
|
||||
.screen-type-wrap :global(.spectrum-Heading) {
|
||||
padding-bottom: var(--spectrum-alias-item-padding-s);
|
||||
}
|
||||
.preview {
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.listViewTitle {
|
||||
margin-top: 35px;
|
||||
}
|
||||
|
||||
.blankView {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.visible {
|
||||
opacity: 1;
|
||||
}
|
||||
</style>
|
|
@ -9,7 +9,7 @@
|
|||
Helpers,
|
||||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
|
||||
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
import { makeComponentUnique } from "builderStore/componentUtils"
|
||||
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
<script>
|
||||
import { Search, Layout, Select, Body, Button } from "@budibase/bbui"
|
||||
import Panel from "components/design/Panel.svelte"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { roles } from "stores/backend"
|
||||
import { store, sortedScreens, userSelectedResourceMap } from "builderStore"
|
||||
import NavItem from "components/common/NavItem.svelte"
|
||||
import ScreenDropdownMenu from "./ScreenDropdownMenu.svelte"
|
||||
import ScreenWizard from "./ScreenWizard.svelte"
|
||||
import RoleIndicator from "./RoleIndicator.svelte"
|
||||
import { RoleUtils } from "@budibase/frontend-core"
|
||||
|
||||
let searchString
|
||||
let accessRole = "all"
|
||||
let showNewScreenModal
|
||||
|
||||
$: filteredScreens = getFilteredScreens(
|
||||
$sortedScreens,
|
||||
|
@ -31,7 +30,7 @@
|
|||
|
||||
<Panel title="Screens" borderRight>
|
||||
<Layout paddingX="L" paddingY="XL" gap="S">
|
||||
<Button on:click={showNewScreenModal} cta>Add screen</Button>
|
||||
<Button on:click={() => $goto("../../new")} cta>Add screen</Button>
|
||||
<Search
|
||||
placeholder="Search"
|
||||
value={searchString}
|
||||
|
@ -74,5 +73,3 @@
|
|||
</Layout>
|
||||
{/if}
|
||||
</Panel>
|
||||
|
||||
<ScreenWizard bind:showModal={showNewScreenModal} />
|
||||
|
|
Binary file not shown.
Before Width: | Height: | Size: 69 KiB |
Binary file not shown.
Before Width: | Height: | Size: 106 KiB |
|
@ -1,6 +1,5 @@
|
|||
<script>
|
||||
import ScreenDetailsModal from "./ScreenDetailsModal.svelte"
|
||||
import NewScreenModal from "./NewScreenModal.svelte"
|
||||
import ScreenDetailsModal from "components/design/ScreenDetailsModal.svelte"
|
||||
import DatasourceModal from "./DatasourceModal.svelte"
|
||||
import ScreenRoleModal from "./ScreenRoleModal.svelte"
|
||||
import sanitizeUrl from "builderStore/store/screenTemplates/utils/sanitizeUrl"
|
||||
|
@ -11,11 +10,11 @@
|
|||
import { tables } from "stores/backend"
|
||||
import { Roles } from "constants/backend"
|
||||
import { capitalise } from "helpers"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
let pendingScreen
|
||||
|
||||
// Modal refs
|
||||
let newScreenModal
|
||||
let screenDetailsModal
|
||||
let datasourceModal
|
||||
let screenAccessRoleModal
|
||||
|
@ -26,16 +25,6 @@
|
|||
let blankScreenUrl = null
|
||||
let screenMode = null
|
||||
|
||||
// External handler to show the screen wizard
|
||||
export const showModal = () => {
|
||||
selectedTemplates = null
|
||||
blankScreenUrl = null
|
||||
screenMode = null
|
||||
pendingScreen = null
|
||||
screenAccessRole = Roles.BASIC
|
||||
newScreenModal.show()
|
||||
}
|
||||
|
||||
// Creates an array of screens, checking and sanitising their URLs
|
||||
const createScreens = async ({ screens, screenAccessRole }) => {
|
||||
if (!screens?.length) {
|
||||
|
@ -43,6 +32,8 @@
|
|||
}
|
||||
|
||||
try {
|
||||
let screenId
|
||||
|
||||
for (let screen of screens) {
|
||||
// Check we aren't clashing with an existing URL
|
||||
if (hasExistingUrl(screen.routing.route)) {
|
||||
|
@ -64,7 +55,8 @@
|
|||
screen.routing.roleId = screenAccessRole
|
||||
|
||||
// Create the screen
|
||||
await store.actions.screens.save(screen)
|
||||
const response = await store.actions.screens.save(screen)
|
||||
screenId = response._id
|
||||
|
||||
// Add link in layout for list screens
|
||||
if (screen.props._instanceName.endsWith("List")) {
|
||||
|
@ -74,7 +66,10 @@
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
$goto(`./${screenId}`)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
notifications.error("Error creating screens")
|
||||
}
|
||||
}
|
||||
|
@ -104,18 +99,24 @@
|
|||
}
|
||||
|
||||
// Handler for NewScreenModal
|
||||
const confirmScreenSelection = async mode => {
|
||||
export const show = mode => {
|
||||
selectedTemplates = null
|
||||
blankScreenUrl = null
|
||||
screenMode = mode
|
||||
pendingScreen = null
|
||||
screenAccessRole = Roles.BASIC
|
||||
|
||||
if (mode === "autoCreate") {
|
||||
if (mode === "table") {
|
||||
datasourceModal.show()
|
||||
} else {
|
||||
} else if (mode === "blank") {
|
||||
let templates = getTemplates($store, $tables.list)
|
||||
const blankScreenTemplate = templates.find(
|
||||
t => t.id === "createFromScratch"
|
||||
)
|
||||
pendingScreen = blankScreenTemplate.create()
|
||||
screenDetailsModal.show()
|
||||
} else {
|
||||
throw new Error("Invalid mode provided")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -155,7 +156,7 @@
|
|||
|
||||
// Submit screen config for creation.
|
||||
const confirmScreenCreation = async () => {
|
||||
if (screenMode === "blankScreen") {
|
||||
if (screenMode === "blank") {
|
||||
confirmBlankScreenCreation({
|
||||
screenUrl: blankScreenUrl,
|
||||
screenAccessRole,
|
||||
|
@ -166,7 +167,7 @@
|
|||
}
|
||||
|
||||
const roleSelectBack = () => {
|
||||
if (screenMode === "blankScreen") {
|
||||
if (screenMode === "blank") {
|
||||
screenDetailsModal.show()
|
||||
} else {
|
||||
datasourceModal.show()
|
||||
|
@ -174,14 +175,9 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<Modal bind:this={newScreenModal}>
|
||||
<NewScreenModal onConfirm={confirmScreenSelection} />
|
||||
</Modal>
|
||||
|
||||
<Modal bind:this={datasourceModal}>
|
||||
<DatasourceModal
|
||||
onConfirm={confirmScreenDatasources}
|
||||
onCancel={() => newScreenModal.show()}
|
||||
initalScreens={!selectedTemplates ? [] : [...selectedTemplates]}
|
||||
/>
|
||||
</Modal>
|
||||
|
@ -198,7 +194,6 @@
|
|||
<Modal bind:this={screenDetailsModal}>
|
||||
<ScreenDetailsModal
|
||||
onConfirm={confirmScreenBlank}
|
||||
onCancel={() => newScreenModal.show()}
|
||||
initialUrl={blankScreenUrl}
|
||||
/>
|
||||
</Modal>
|
|
@ -40,14 +40,14 @@
|
|||
</script>
|
||||
|
||||
<ModalContent
|
||||
title="Autogenerated screens"
|
||||
title="Access"
|
||||
confirmText="Done"
|
||||
cancelText="Back"
|
||||
{onConfirm}
|
||||
{onCancel}
|
||||
disabled={!!error}
|
||||
>
|
||||
Select which level of access you want your screens to have
|
||||
Select the level of access required to see these screens
|
||||
<Select
|
||||
bind:value={screenAccessRole}
|
||||
on:change={onChangeRole}
|
Binary file not shown.
After Width: | Height: | Size: 32 KiB |
|
@ -1,67 +1,12 @@
|
|||
<script>
|
||||
import { store, selectedScreen } from "builderStore"
|
||||
import { onMount } from "svelte"
|
||||
import { redirect } from "@roxi/routify"
|
||||
import { Layout, Button, Detail, notifications } from "@budibase/bbui"
|
||||
import Logo from "assets/bb-space-man.svg"
|
||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||
import { Roles } from "constants/backend"
|
||||
import { store as frontendStore } from "builderStore"
|
||||
|
||||
let loaded = false
|
||||
|
||||
const createFirstScreen = async () => {
|
||||
let screen = createFromScratchScreen.create()
|
||||
screen.routing.route = "/home"
|
||||
screen.routing.roldId = Roles.BASIC
|
||||
screen.routing.homeScreen = true
|
||||
try {
|
||||
const savedScreen = await store.actions.screens.save(screen)
|
||||
notifications.success("Screen created successfully")
|
||||
$redirect(`./${savedScreen._id}`)
|
||||
} catch (err) {
|
||||
console.error("Could not create screen", err)
|
||||
notifications.error("Error creating screen")
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if ($selectedScreen) {
|
||||
$redirect(`./${$selectedScreen._id}`)
|
||||
} else if ($store.screens?.length) {
|
||||
$redirect(`./${$store.screens[0]._id}`)
|
||||
$: {
|
||||
if ($frontendStore.screens.length > 0) {
|
||||
$redirect(`./${$frontendStore.screens[0]._id}`)
|
||||
} else {
|
||||
loaded = true
|
||||
$redirect("./new")
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if loaded}
|
||||
<div class="centered">
|
||||
<Layout gap="S" justifyItems="center">
|
||||
<img class="img-size" alt="logo" src={Logo} />
|
||||
<div class="new-screen-text">
|
||||
<Detail size="L">LET’S BRING THIS APP TO LIFE</Detail>
|
||||
</div>
|
||||
<Button on:click={createFirstScreen} size="M" cta icon="Add">
|
||||
Create first screen
|
||||
</Button>
|
||||
</Layout>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.centered {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.new-screen-text {
|
||||
width: 150px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
.img-size {
|
||||
width: 170px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
<script>
|
||||
import { Body } from "@budibase/bbui"
|
||||
import CreationPage from "components/common/CreationPage.svelte"
|
||||
import blankImage from "./blank.png"
|
||||
import tableImage from "./table.png"
|
||||
import CreateScreenModal from "./_components/CreateScreenModal.svelte"
|
||||
import { store } from "builderStore"
|
||||
import { goto } from "@roxi/routify"
|
||||
|
||||
let createScreenModal
|
||||
|
||||
$: hasScreens = $store.screens?.length
|
||||
</script>
|
||||
|
||||
<div class="page">
|
||||
<CreationPage
|
||||
showClose={$store.screens.length > 0}
|
||||
onClose={() => $goto(`./${$store.screens[0]._id}`)}
|
||||
heading={hasScreens ? "Create new screen" : "Create your first screen"}
|
||||
>
|
||||
<div class="subHeading">
|
||||
<Body size="L">Start from scratch or create screens from your data</Body>
|
||||
</div>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card" on:click={() => createScreenModal.show("blank")}>
|
||||
<div class="image">
|
||||
<img alt="" src={blankImage} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Blank screen</Body>
|
||||
<Body size="XS">Add an empty blank screen</Body>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card" on:click={() => createScreenModal.show("table")}>
|
||||
<div class="image">
|
||||
<img alt="" src={tableImage} />
|
||||
</div>
|
||||
<div class="text">
|
||||
<Body size="S">Table</Body>
|
||||
<Body size="XS">View, edit and delete rows on a table</Body>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CreationPage>
|
||||
</div>
|
||||
|
||||
<CreateScreenModal bind:this={createScreenModal} />
|
||||
|
||||
<style>
|
||||
.page {
|
||||
padding: 28px 40px 40px 40px;
|
||||
}
|
||||
|
||||
.subHeading :global(p) {
|
||||
text-align: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 24px;
|
||||
color: var(--grey-6);
|
||||
}
|
||||
|
||||
.cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin: 12px;
|
||||
max-width: 235px;
|
||||
transition: filter 150ms;
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
filter: brightness(1.1);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.image {
|
||||
border-radius: 4px 4px 0 0;
|
||||
width: 100%;
|
||||
max-height: 127px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.image img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.text {
|
||||
border: 1px solid var(--grey-4);
|
||||
border-radius: 0 0 4px 4px;
|
||||
padding: 8px 16px 13px 16px;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(1)) {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.text :global(p:nth-child(2)) {
|
||||
color: var(--grey-6);
|
||||
}
|
||||
</style>
|
Binary file not shown.
After Width: | Height: | Size: 26 KiB |
|
@ -0,0 +1,40 @@
|
|||
<script>
|
||||
import { Layout, Body, Button } from "@budibase/bbui"
|
||||
import { downloadStream } from "@budibase/frontend-core"
|
||||
import Spinner from "components/common/Spinner.svelte"
|
||||
|
||||
import { API } from "api"
|
||||
|
||||
let loading = false
|
||||
|
||||
async function download() {
|
||||
loading = true
|
||||
try {
|
||||
await downloadStream(await API.getSystemLogs())
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Layout noPadding>
|
||||
<Body>Download your latest logs to share with the Budibase team</Body>
|
||||
<div class="download-button">
|
||||
<Button cta on:click={download} disabled={loading}>
|
||||
<div class="button-content">
|
||||
{#if loading}
|
||||
<Spinner size="10" />
|
||||
{/if}
|
||||
Download system logs
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
<style>
|
||||
.button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--spacing-m);
|
||||
}
|
||||
</style>
|
|
@ -7,8 +7,6 @@
|
|||
import { API } from "api"
|
||||
import { store, automationStore } from "builderStore"
|
||||
import { auth, admin } from "stores/portal"
|
||||
import createFromScratchScreen from "builderStore/store/screenTemplates/createFromScratchScreen"
|
||||
import { Roles } from "constants/backend"
|
||||
|
||||
let name = "My first app"
|
||||
let url = "my-first-app"
|
||||
|
@ -38,11 +36,6 @@
|
|||
// Create user
|
||||
await auth.setInitInfo({})
|
||||
|
||||
let defaultScreenTemplate = createFromScratchScreen.create()
|
||||
defaultScreenTemplate.routing.route = "/home"
|
||||
defaultScreenTemplate.routing.roldId = Roles.BASIC
|
||||
await store.actions.screens.save(defaultScreenTemplate)
|
||||
|
||||
appId = createdApp.instance._id
|
||||
return createdApp
|
||||
}
|
||||
|
|
|
@ -85,6 +85,13 @@ export const menu = derived([admin, auth], ([$admin, $auth]) => {
|
|||
title: "Audit Logs",
|
||||
href: "/builder/portal/account/auditLogs",
|
||||
})
|
||||
|
||||
if (!$admin.cloud) {
|
||||
accountSubPages.push({
|
||||
title: "System Logs",
|
||||
href: "/builder/portal/account/systemLogs",
|
||||
})
|
||||
}
|
||||
}
|
||||
if ($admin.cloud && $auth?.user?.accountPortalAccess) {
|
||||
accountSubPages.push({
|
||||
|
|
|
@ -30,6 +30,7 @@ import { buildBackupsEndpoints } from "./backups"
|
|||
import { buildEnvironmentVariableEndpoints } from "./environmentVariables"
|
||||
import { buildEventEndpoints } from "./events"
|
||||
import { buildAuditLogsEndpoints } from "./auditLogs"
|
||||
import { buildLogsEndpoints } from "./logs"
|
||||
|
||||
/**
|
||||
* Random identifier to uniquely identify a session in a tab. This is
|
||||
|
@ -277,5 +278,6 @@ export const createAPIClient = config => {
|
|||
...buildEnvironmentVariableEndpoints(API),
|
||||
...buildEventEndpoints(API),
|
||||
...buildAuditLogsEndpoints(API),
|
||||
...buildLogsEndpoints(API),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
export const buildLogsEndpoints = API => ({
|
||||
/**
|
||||
* Gets a stream for the system logs.
|
||||
*/
|
||||
getSystemLogs: async () => {
|
||||
return await API.get({
|
||||
url: "/api/system/logs",
|
||||
json: false,
|
||||
parseResponse: async response => {
|
||||
return response
|
||||
},
|
||||
})
|
||||
},
|
||||
})
|
|
@ -11,3 +11,26 @@ export function downloadText(filename, text) {
|
|||
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
export async function downloadStream(streamResponse) {
|
||||
const blob = await streamResponse.blob()
|
||||
|
||||
const contentDisposition = streamResponse.headers.get("Content-Disposition")
|
||||
|
||||
const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(
|
||||
contentDisposition
|
||||
)
|
||||
|
||||
const filename = matches[1].replace(/['"]/g, "")
|
||||
|
||||
const resBlob = new Blob([blob])
|
||||
|
||||
const blobUrl = URL.createObjectURL(resBlob)
|
||||
|
||||
const link = document.createElement("a")
|
||||
link.href = blobUrl
|
||||
link.download = filename
|
||||
link.click()
|
||||
|
||||
URL.revokeObjectURL(blobUrl)
|
||||
}
|
||||
|
|
|
@ -5,4 +5,4 @@ export * as RoleUtils from "./roles"
|
|||
export * as Utils from "./utils"
|
||||
export { memo, derivedMemo } from "./memo"
|
||||
export { createWebsocket } from "./websocket"
|
||||
export { downloadText } from "./download"
|
||||
export * from "./download"
|
||||
|
|
|
@ -27,11 +27,14 @@ const DEFAULT_SCHEMA = "dbo"
|
|||
|
||||
import { ConfidentialClientApplication } from "@azure/msal-node"
|
||||
|
||||
import { utils } from "@budibase/shared-core"
|
||||
|
||||
enum MSSQLConfigAuthType {
|
||||
ACTIVE_DIRECTORY = "Active Directory",
|
||||
AZURE_ACTIVE_DIRECTORY = "Azure Active Directory",
|
||||
NTLM = "NTLM",
|
||||
}
|
||||
|
||||
interface MSSQLConfig {
|
||||
interface BasicMSSQLConfig {
|
||||
user: string
|
||||
password: string
|
||||
server: string
|
||||
|
@ -40,13 +43,30 @@ interface MSSQLConfig {
|
|||
schema: string
|
||||
encrypt?: boolean
|
||||
authType?: MSSQLConfigAuthType
|
||||
adConfig?: {
|
||||
}
|
||||
|
||||
interface AzureADMSSQLConfig extends BasicMSSQLConfig {
|
||||
authType: MSSQLConfigAuthType.AZURE_ACTIVE_DIRECTORY
|
||||
adConfig: {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
tenantId: string
|
||||
}
|
||||
}
|
||||
|
||||
interface NTLMMSSQLConfig extends BasicMSSQLConfig {
|
||||
authType: MSSQLConfigAuthType.NTLM
|
||||
ntlmConfig: {
|
||||
domain?: string
|
||||
trustServerCertificate?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
type MSSQLConfig =
|
||||
| (BasicMSSQLConfig & { authType: undefined })
|
||||
| AzureADMSSQLConfig
|
||||
| NTLMMSSQLConfig
|
||||
|
||||
const SCHEMA: Integration = {
|
||||
docs: "https://github.com/tediousjs/node-mssql",
|
||||
plus: true,
|
||||
|
@ -93,13 +113,18 @@ const SCHEMA: Integration = {
|
|||
authType: {
|
||||
type: DatasourceFieldType.SELECT,
|
||||
display: "Advanced auth",
|
||||
config: { options: [MSSQLConfigAuthType.ACTIVE_DIRECTORY] },
|
||||
config: {
|
||||
options: [
|
||||
MSSQLConfigAuthType.AZURE_ACTIVE_DIRECTORY,
|
||||
MSSQLConfigAuthType.NTLM,
|
||||
],
|
||||
},
|
||||
},
|
||||
adConfig: {
|
||||
type: DatasourceFieldType.FIELD_GROUP,
|
||||
default: true,
|
||||
display: "Configure Active Directory",
|
||||
hidden: "'{{authType}}' !== 'Active Directory'",
|
||||
hidden: `'{{authType}}' !== '${MSSQLConfigAuthType.AZURE_ACTIVE_DIRECTORY}'`,
|
||||
config: {
|
||||
openByDefault: true,
|
||||
nestedFields: true,
|
||||
|
@ -122,6 +147,28 @@ const SCHEMA: Integration = {
|
|||
},
|
||||
},
|
||||
},
|
||||
ntlmConfig: {
|
||||
type: DatasourceFieldType.FIELD_GROUP,
|
||||
default: true,
|
||||
display: "Configure NTLM",
|
||||
hidden: `'{{authType}}' !== '${MSSQLConfigAuthType.NTLM}'`,
|
||||
config: {
|
||||
openByDefault: true,
|
||||
nestedFields: true,
|
||||
},
|
||||
fields: {
|
||||
domain: {
|
||||
type: DatasourceFieldType.STRING,
|
||||
required: false,
|
||||
display: "Domain",
|
||||
},
|
||||
trustServerCertificate: {
|
||||
type: DatasourceFieldType.BOOLEAN,
|
||||
required: false,
|
||||
display: "Trust server certificate",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
query: {
|
||||
create: {
|
||||
|
@ -199,26 +246,43 @@ class SqlServerIntegration extends Sql implements DatasourcePlus {
|
|||
}
|
||||
delete clientCfg.encrypt
|
||||
|
||||
if (this.config.authType === MSSQLConfigAuthType.ACTIVE_DIRECTORY) {
|
||||
const { clientId, tenantId, clientSecret } = this.config.adConfig!
|
||||
const clientApp = new ConfidentialClientApplication({
|
||||
auth: {
|
||||
clientId,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
clientSecret,
|
||||
},
|
||||
})
|
||||
switch (this.config.authType) {
|
||||
case MSSQLConfigAuthType.AZURE_ACTIVE_DIRECTORY:
|
||||
const { clientId, tenantId, clientSecret } = this.config.adConfig
|
||||
const clientApp = new ConfidentialClientApplication({
|
||||
auth: {
|
||||
clientId,
|
||||
authority: `https://login.microsoftonline.com/${tenantId}`,
|
||||
clientSecret,
|
||||
},
|
||||
})
|
||||
|
||||
const response = await clientApp.acquireTokenByClientCredential({
|
||||
scopes: ["https://database.windows.net/.default"],
|
||||
})
|
||||
const response = await clientApp.acquireTokenByClientCredential({
|
||||
scopes: ["https://database.windows.net/.default"],
|
||||
})
|
||||
|
||||
clientCfg.authentication = {
|
||||
type: "azure-active-directory-access-token",
|
||||
options: {
|
||||
token: response!.accessToken,
|
||||
},
|
||||
}
|
||||
clientCfg.authentication = {
|
||||
type: "azure-active-directory-access-token",
|
||||
options: {
|
||||
token: response!.accessToken,
|
||||
},
|
||||
}
|
||||
break
|
||||
case MSSQLConfigAuthType.NTLM:
|
||||
const { domain, trustServerCertificate } = this.config.ntlmConfig
|
||||
clientCfg.authentication = {
|
||||
type: "ntlm",
|
||||
options: {
|
||||
domain,
|
||||
},
|
||||
}
|
||||
clientCfg.options ??= {}
|
||||
clientCfg.options.trustServerCertificate = trustServerCertificate
|
||||
break
|
||||
case undefined:
|
||||
break
|
||||
default:
|
||||
utils.unreachable(this.config)
|
||||
}
|
||||
|
||||
const pool = new sqlServer.ConnectionPool(clientCfg)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { UserCtx } from "@budibase/types"
|
||||
import { installation, logging } from "@budibase/backend-core"
|
||||
|
||||
export async function getLogs(ctx: UserCtx) {
|
||||
const logReadStream = logging.system.getLogReadStream()
|
||||
|
||||
const { installId } = await installation.getInstall()
|
||||
|
||||
const fileName = `${installId}-${Date.now()}.log`
|
||||
|
||||
ctx.set("content-disposition", `attachment; filename=${fileName}`)
|
||||
ctx.body = logReadStream
|
||||
}
|
|
@ -16,6 +16,9 @@ import licenseRoutes from "./global/license"
|
|||
import migrationRoutes from "./system/migrations"
|
||||
import accountRoutes from "./system/accounts"
|
||||
import restoreRoutes from "./system/restore"
|
||||
import systemLogRoutes from "./system/logs"
|
||||
|
||||
import env from "../../environment"
|
||||
|
||||
export const routes: Router[] = [
|
||||
configRoutes,
|
||||
|
@ -38,3 +41,7 @@ export const routes: Router[] = [
|
|||
eventRoutes,
|
||||
pro.scim,
|
||||
]
|
||||
|
||||
if (env.SELF_HOSTED) {
|
||||
routes.push(systemLogRoutes)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import Router from "@koa/router"
|
||||
import { middleware } from "@budibase/backend-core"
|
||||
import * as controller from "../../controllers/system/logs"
|
||||
|
||||
const router: Router = new Router()
|
||||
|
||||
router.get("/api/system/logs", middleware.adminOnly, controller.getLogs)
|
||||
|
||||
export default router
|
|
@ -23197,6 +23197,11 @@ rollup@^3.18.0:
|
|||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
rotating-file-stream@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/rotating-file-stream/-/rotating-file-stream-3.1.0.tgz#6cf50e1671de82a396de6d31d39a6f2445f45fba"
|
||||
integrity sha512-TkMF6cP1/QDcon9D71mjxHoflNuznNOrY5JJQfuxkKklZRmoow/lWBLNxXVjb6KcjAU8BDCV145buLgOx9Px1Q==
|
||||
|
||||
rrweb-cssom@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/rrweb-cssom/-/rrweb-cssom-0.6.0.tgz#ed298055b97cbddcdeb278f904857629dec5e0e1"
|
||||
|
|
Loading…
Reference in New Issue