Merge branch 'feature/table-fetching-frontend' of github.com:Budibase/budibase into feature/table-fetching-frontend

This commit is contained in:
mike12345567 2023-06-06 16:40:09 +01:00
commit 1760fcb34d
24 changed files with 517 additions and 277 deletions

View File

@ -16,7 +16,8 @@
"dist",
"public",
"*.spec.js",
"bundle.js"
"bundle.js",
"packages/pro"
],
"plugins": ["svelte3"],
"extends": ["eslint:recommended"],

View File

@ -1,43 +1,53 @@
name: Budibase Deploy Production
on:
workflow_dispatch:
inputs:
version:
description: Budibase release version. For example - 1.0.0
required: false
workflow_dispatch:
inputs:
version:
description: Budibase release version. For example - 1.0.0
required: false
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Fail if branch is not master
if: github.ref != 'refs/heads/master'
run: |
echo "Ref is not master, you must run this job from master."
exit 1
# - name: Fail if not a tag
# run: |
# if [[ $GITHUB_REF != refs/tags/* ]]; then
# echo "Workflow Dispatch can only be run on tags"
# exit 1
# fi
- uses: actions/checkout@v2
# with:
# fetch-depth: 0
# - name: Fail if tag is not in master
# run: |
# if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
# echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
# exit 1
# fi
- name: Pull values.yaml from budibase-infra
run: |
run: |
curl -H "Authorization: token ${{ secrets.GH_ACCESS_TOKEN }}" \
-H 'Accept: application/vnd.github.v3.raw' \
-o values.production.yaml \
-L https://api.github.com/repos/budibase/budibase-infra/contents/kubernetes/values.yaml
wc -l values.production.yaml
- name: Get the latest budibase release version
id: version
run: |
run: |
if [ -z "${{ github.event.inputs.version }}" ]; then
release_version=$(cat lerna.json | jq -r '.version')
else
release_version=${{ github.event.inputs.version }}
fi
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
@ -64,4 +74,3 @@ jobs:
webhook-url: ${{ secrets.PROD_DEPLOY_WEBHOOK_URL }}
content: "Production Deployment Complete: ${{ env.RELEASE_VERSION }} deployed to Budibase Cloud."
embed-title: ${{ env.RELEASE_VERSION }}

View File

@ -1,27 +1,35 @@
name: "deploy-preprod"
on:
workflow_dispatch:
inputs:
version:
description: Budibase release version. For example - 1.0.0
required: false
workflow_call:
jobs:
deploy-to-legacy-preprod-env:
runs-on: ubuntu-latest
steps:
- name: Fail if not a tag
run: |
if [[ $GITHUB_REF != refs/tags/* ]]; then
echo "Workflow Dispatch can only be run on tags"
exit 1
fi
- uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Get the latest budibase release version
id: version
run: |
if [ -z "${{ github.event.inputs.version }}" ]; then
git pull
release_version=$(cat lerna.json | jq -r '.version')
else
release_version=${{ github.event.inputs.version }}
fi
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1

View File

@ -22,6 +22,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Fail if not a tag
run: |
if [[ $GITHUB_REF != refs/tags/* ]]; then
echo "Workflow Dispatch can only be run on tags"
exit 1
fi
- uses: actions/checkout@v2
with:
submodules: true

View File

@ -9,12 +9,6 @@ on:
- "v[0-9]+.[0-9]+.[0-9]+"
# Exclude all pre-releases
- "!v*[0-9]+.[0-9]+.[0-9]+-*"
workflow_dispatch:
inputs:
tags:
description: "Release tag"
required: true
type: boolean
env:
# Posthog token used by ui at build time
@ -33,12 +27,13 @@ jobs:
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
fetch-depth: 0
- name: Fail if branch is not master
if: github.ref != 'refs/heads/master'
- name: Fail if tag is not in master
run: |
echo "Ref is not master, you must run this job from master."
// Change to "exit 1" when merged. Left to 0 to not fail all the pipelines and not to cause noise
exit 0
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- uses: actions/setup-node@v1
with:
@ -65,7 +60,6 @@ jobs:
- name: Publish budibase packages to NPM
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
RELEASE_VERSION_TYPE: ${{ github.event.inputs.versioning }}
run: |
# setup the username and email. I tend to use 'GitHub Actions Bot' with no email by default
git config --global user.name "Budibase Release Bot"
@ -140,7 +134,6 @@ jobs:
- name: Get the latest budibase release version
id: version
run: |
git pull
release_version=$(cat lerna.json | jq -r '.version')
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV

View File

@ -1,22 +1,30 @@
name: Budibase Release Selfhost
on:
workflow_dispatch:
workflow_dispatch:
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Fail if branch is not master
if: github.ref != 'refs/heads/master'
- name: Fail if not a tag
run: |
echo "Ref is not master, you must run this job from master."
exit 1
if [[ $GITHUB_REF != refs/tags/* ]]; then
echo "Workflow Dispatch can only be run on tags"
exit 1
fi
- uses: actions/checkout@v2
with:
fetch_depth: 0
with:
fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Use Node.js 14.x
uses: actions/setup-node@v1
@ -30,7 +38,7 @@ jobs:
echo "RELEASE_VERSION=$release_version" >> $GITHUB_ENV
- name: Tag and release Docker images (Self Host)
run: |
run: |
docker login -u $DOCKER_USER -p $DOCKER_PASSWORD
release_tag=v${{ env.RELEASE_VERSION }}
@ -44,7 +52,7 @@ jobs:
docker tag budibase/apps:$release_tag budibase/apps:$SELFHOST_TAG
docker tag budibase/worker:$release_tag budibase/worker:$SELFHOST_TAG
docker tag budibase/proxy:$release_tag budibase/proxy:$SELFHOST_TAG
# Push images
docker push budibase/apps:$SELFHOST_TAG
docker push budibase/worker:$SELFHOST_TAG
@ -66,19 +74,19 @@ jobs:
yarn
yarn specs
popd
- name: Setup Helm
- name: Setup Helm
uses: azure/setup-helm@v1
id: helm-install
# due to helm repo index issue: https://github.com/helm/helm/issues/7363
# we need to create new package in a different dir, merge the index and move the package back
- name: Build and release helm chart
run: |
run: |
git config user.name "Budibase Helm Bot"
git config user.email "<>"
git reset --hard
git pull
git fetch
mkdir sync
echo "Packaging chart to sync dir"
helm package charts/budibase --version "$RELEASE_VERSION" --app-version "$RELEASE_VERSION" --destination sync

View File

@ -15,13 +15,24 @@ jobs:
matrix:
node-version: [14.x]
steps:
- name: Fail if branch is not master
if: github.ref != 'refs/heads/master'
run: |
echo "Ref is not master, you must run this job from master."
exit 1
- name: Fail if not a tag
run: |
if [[ $GITHUB_REF != refs/tags/* ]]; then
echo "Workflow Dispatch can only be run on tags"
exit 1
fi
- name: "Checkout"
uses: actions/checkout@v2
with:
fetch-depth: 0
- name: Fail if tag is not in master
run: |
if ! git merge-base --is-ancestor ${{ github.sha }} origin/master; then
echo "Tag is not in master. This pipeline can only execute tags that are present on the master branch"
exit 1
fi
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v1
with:

View File

@ -28,7 +28,7 @@ on:
required: true
jobs:
tag-prerelease:
tag-release:
runs-on: ubuntu-latest
steps:
@ -43,9 +43,11 @@ jobs:
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
- run: yarn
- name: Tag prerelease
- name: Tag release
run: |
# setup the username and email.
git config --global user.name "Budibase Staging Release Bot"
git config --global user.email "<>"
./scripts/versionCommit.sh ${{ github.event.inputs.versioning }}
BUMP_TYPE_INPUT=${{ github.event.inputs.versioning }}
BUMP_TYPE=${BUMP_TYPE_INPUT:-"patch"}
./scripts/versionCommit.sh $BUMP_TYPE

View File

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

View File

@ -38,8 +38,8 @@
"backend:build": "./scripts/scopeBackend.sh 'lerna run --stream build'",
"build:sdk": "lerna run --stream build:sdk",
"deps:circular": "madge packages/server/dist/index.js packages/worker/src/index.ts packages/backend-core/dist/src/index.js packages/cli/src/index.js --circular",
"release": "lerna publish ${RELEASE_VERSION_TYPE:-patch} --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
"release:develop": "lerna publish from-package --yes --force-publish --dist-tag develop --exact --no-git-tag-version --no-push --no-git-reset",
"release": "lerna publish from-package --yes --force-publish --no-git-tag-version --no-push --no-git-reset",
"release:develop": "yarn release --dist-tag develop",
"restore": "yarn run clean && yarn run bootstrap && yarn run build",
"nuke": "yarn run nuke:packages && yarn run nuke:docker",
"nuke:packages": "yarn run restore",

View File

@ -3,6 +3,7 @@ import {
Event,
User,
UserCreatedEvent,
UserDataCollaborationEvent,
UserDeletedEvent,
UserInviteAcceptedEvent,
UserInvitedEvent,
@ -173,6 +174,15 @@ async function passwordReset(user: User) {
await publishEvent(Event.USER_PASSWORD_RESET, properties)
}
// COLLABORATION
async function dataCollaboration(users: number) {
const properties: UserDataCollaborationEvent = {
users,
}
await publishEvent(Event.USER_DATA_COLLABORATION, properties)
}
export default {
created,
updated,
@ -188,4 +198,5 @@ export default {
passwordUpdated,
passwordResetRequested,
passwordReset,
dataCollaboration,
}

View File

@ -1,10 +1,10 @@
import * as google from "../sso/google"
import { Cookie } from "../../../constants"
import { clearCookie, getCookie } from "../../../utils"
import { doWithDB } from "../../../db"
import * as configs from "../../../configs"
import { BBContext, Database, SSOProfile } from "@budibase/types"
import { BBContext, SSOProfile } from "@budibase/types"
import { ssoSaveUserNoOp } from "../sso/sso"
import { cache, utils } from "../../../"
const GoogleStrategy = require("passport-google-oauth").OAuth2Strategy
type Passport = {
@ -36,8 +36,8 @@ export async function preAuth(
ssoSaveUserNoOp
)
if (!ctx.query.appId || !ctx.query.datasourceId) {
ctx.throw(400, "appId and datasourceId query params not present.")
if (!ctx.query.appId) {
ctx.throw(400, "appId query param not present.")
}
return passport.authenticate(strategy, {
@ -69,7 +69,7 @@ export async function postAuth(
(
accessToken: string,
refreshToken: string,
profile: SSOProfile,
_profile: SSOProfile,
done: Function
) => {
clearCookie(ctx, Cookie.DatasourceAuth)
@ -79,23 +79,16 @@ export async function postAuth(
{ successRedirect: "/", failureRedirect: "/error" },
async (err: any, tokens: string[]) => {
const baseUrl = `/builder/app/${authStateCookie.appId}/data`
// update the DB for the datasource with all the user info
await doWithDB(authStateCookie.appId, async (db: Database) => {
let datasource
try {
datasource = await db.get(authStateCookie.datasourceId)
} catch (err: any) {
if (err.status === 404) {
ctx.redirect(baseUrl)
}
const id = utils.newid()
await cache.store(
`datasource:creation:${authStateCookie.appId}:google:${id}`,
{
tokens,
}
if (!datasource.config) {
datasource.config = {}
}
datasource.config.auth = { type: "google", ...tokens }
await db.put(datasource)
ctx.redirect(`${baseUrl}/datasource/${authStateCookie.datasourceId}`)
})
)
ctx.redirect(`${baseUrl}/new?continue_google_setup=${id}`)
}
)(ctx, next)
}

View File

@ -3,8 +3,6 @@
import { store } from "builderStore"
import { auth } from "stores/portal"
export let preAuthStep
export let datasource
export let disabled
export let samePage
@ -15,18 +13,8 @@
class:disabled
{disabled}
on:click={async () => {
let ds = datasource
let appId = $store.appId
if (!ds) {
const resp = await preAuthStep()
if (resp.datasource && resp.appId) {
ds = resp.datasource
appId = resp.appId
} else {
ds = resp
}
}
const url = `/api/global/auth/${tenantId}/datasource/google?datasourceId=${ds._id}&appId=${appId}`
const url = `/api/global/auth/${tenantId}/datasource/google?appId=${appId}`
if (samePage) {
window.location = url
} else {

View File

@ -1,43 +1,110 @@
<script>
import { ModalContent, Body, Layout, Link } from "@budibase/bbui"
import { IntegrationNames } from "constants/backend"
import cloneDeep from "lodash/cloneDeepWith"
import {
ModalContent,
Body,
Layout,
Link,
notifications,
} from "@budibase/bbui"
import { IntegrationNames, IntegrationTypes } from "constants/backend"
import GoogleButton from "../_components/GoogleButton.svelte"
import { saveDatasource as save } from "builderStore/datasource"
import { organisation } from "stores/portal"
import { onMount } from "svelte"
import { validateDatasourceConfig } from "builderStore/datasource"
import cloneDeep from "lodash/cloneDeepWith"
import IntegrationConfigForm from "../TableIntegrationMenu/IntegrationConfigForm.svelte"
import { goto } from "@roxi/routify"
import { saveDatasource } from "builderStore/datasource"
import { DatasourceFeature } from "@budibase/types"
export let integration
export let continueSetupId = false
// kill the reference so the input isn't saved
let datasource = cloneDeep(integration)
datasource.config.continueSetupId = continueSetupId
$: isGoogleConfigured = !!$organisation.googleDatasourceConfigured
onMount(async () => {
await organisation.init()
})
const integrationName = IntegrationNames[IntegrationTypes.GOOGLE_SHEETS]
export const GoogleDatasouceConfigStep = {
AUTH: "Auth",
SET_URL: "Set_url",
}
let step = continueSetupId
? GoogleDatasouceConfigStep.SET_URL
: GoogleDatasouceConfigStep.AUTH
let isValid = false
const modalConfig = {
[GoogleDatasouceConfigStep.AUTH]: {},
[GoogleDatasouceConfigStep.SET_URL]: {
confirmButtonText: "Connect",
onConfirm: async () => {
if (integration.features[DatasourceFeature.CONNECTION_CHECKING]) {
const resp = await validateDatasourceConfig(datasource)
if (!resp.connected) {
notifications.error(`Unable to connect - ${resp.error}`)
return false
}
}
try {
const resp = await saveDatasource(datasource)
$goto(`./datasource/${resp._id}`)
notifications.success(`Datasource created successfully.`)
} catch (err) {
notifications.error(err?.message ?? "Error saving datasource")
// prevent the modal from closing
return false
}
},
},
}
</script>
<ModalContent
title={`Connect to ${IntegrationNames[datasource.type]}`}
cancelText="Back"
title={`Connect to ${integrationName}`}
cancelText="Cancel"
size="L"
confirmText={modalConfig[step].confirmButtonText}
showConfirmButton={!!modalConfig[step].onConfirm}
onConfirm={modalConfig[step].onConfirm}
disabled={!isValid}
>
<!-- check true and false directly, don't render until flag is set -->
{#if isGoogleConfigured === true}
<Layout noPadding>
{#if step === GoogleDatasouceConfigStep.AUTH}
<!-- check true and false directly, don't render until flag is set -->
{#if isGoogleConfigured === true}
<Layout noPadding>
<Body size="S"
>Authenticate with your google account to use the {integrationName} integration.</Body
>
</Layout>
<GoogleButton samePage />
{:else if isGoogleConfigured === false}
<Body size="S"
>Authenticate with your google account to use the {IntegrationNames[
datasource.type
]} integration.</Body
>Google authentication is not enabled, please complete Google SSO
configuration.</Body
>
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
{/if}
{/if}
{#if step === GoogleDatasouceConfigStep.SET_URL}
<Layout noPadding no>
<Body size="S">Add the URL of the sheet you want to connect.</Body>
<IntegrationConfigForm
schema={datasource.schema}
bind:datasource
creating={true}
on:valid={e => (isValid = e.detail)}
/>
</Layout>
<GoogleButton preAuthStep={() => save(datasource, true)} />
{:else if isGoogleConfigured === false}
<Body size="S"
>Google authentication is not enabled, please complete Google SSO
configuration.</Body
>
<Link href="/builder/portal/settings/auth">Configure Google SSO</Link>
{/if}
</ModalContent>

View File

@ -17,6 +17,7 @@
import IntegrationIcon from "components/backend/DatasourceNavigator/IntegrationIcon.svelte"
import ICONS from "components/backend/DatasourceNavigator/icons/index.js"
import FontAwesomeIcon from "components/common/FontAwesomeIcon.svelte"
import { onMount } from "svelte"
let internalTableModal
let externalDatasourceModal
@ -129,9 +130,19 @@
return integrationsArray
}
let continueGoogleSetup
onMount(() => {
const urlParams = new URLSearchParams(window.location.search)
continueGoogleSetup = urlParams.get("continue_google_setup")
})
const fetchIntegrations = async () => {
const unsortedIntegrations = await API.getIntegrations()
integrations = sortIntegrations(unsortedIntegrations)
if (continueGoogleSetup) {
handleIntegrationSelect(IntegrationTypes.GOOGLE_SHEETS)
}
}
$: fetchIntegrations()
@ -141,9 +152,17 @@
<CreateTableModal {promptUpload} afterSave={handleInternalTableSave} />
</Modal>
<Modal bind:this={externalDatasourceModal}>
<Modal
bind:this={externalDatasourceModal}
on:hide={() => {
continueGoogleSetup = null
}}
>
{#if integration?.auth?.type === "google"}
<GoogleDatasourceConfigModal {integration} />
<GoogleDatasourceConfigModal
continueSetupId={continueGoogleSetup}
{integration}
/>
{:else}
<DatasourceConfigModal {integration} />
{/if}

View File

@ -0,0 +1,235 @@
<script>
import GoogleLogo from "./_logos/Google.svelte"
import { isEqual, cloneDeep } from "lodash/fp"
import {
Button,
Heading,
Divider,
Label,
notifications,
Layout,
Input,
Body,
Toggle,
Icon,
Helpers,
Link,
} from "@budibase/bbui"
import { onMount } from "svelte"
import { API } from "api"
import { organisation, admin } from "stores/portal"
const ConfigTypes = {
Google: "google",
}
// Some older google configs contain a manually specified value - retain the functionality to edit the field
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
$: googleCallbackUrl = undefined
$: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl
// Indicate to user that callback is based on platform url
// If there is an existing value, indicate that it may be removed to return to default behaviour
$: googleCallbackTooltip = $admin.cloud
? null
: googleCallbackReadonly
? "Visit the organisation page to update the platform URL"
: "Leave blank to use the default callback URL"
$: googleSheetsCallbackUrl = `${$organisation.platformUrl}/api/global/auth/datasource/google/callback`
$: GoogleConfigFields = {
Google: [
{ name: "clientID", label: "Client ID" },
{ name: "clientSecret", label: "Client secret" },
{
name: "callbackURL",
label: "Callback URL",
readonly: googleCallbackReadonly,
tooltip: googleCallbackTooltip,
placeholder: $organisation.googleCallbackUrl,
copyButton: true,
},
{
name: "sheetsURL",
label: "Sheets URL",
readonly: googleCallbackReadonly,
tooltip: googleCallbackTooltip,
placeholder: googleSheetsCallbackUrl,
copyButton: true,
},
],
}
let google
const providers = { google }
// control the state of the save button depending on whether form has changed
let originalGoogleDoc
let googleSaveButtonDisabled
$: {
isEqual(providers.google?.config, originalGoogleDoc?.config)
? (googleSaveButtonDisabled = true)
: (googleSaveButtonDisabled = false)
}
$: googleComplete = !!(
providers.google?.config?.clientID && providers.google?.config?.clientSecret
)
async function saveConfig(config) {
// Delete unsupported fields
delete config.createdAt
delete config.updatedAt
return API.saveConfig(config)
}
async function saveGoogle() {
if (!googleComplete) {
notifications.error(
`Please fill in all required ${ConfigTypes.Google} fields`
)
return
}
const google = providers.google
try {
const res = await saveConfig(google)
providers[res.type]._rev = res._rev
providers[res.type]._id = res._id
notifications.success(`Settings saved`)
} catch (e) {
notifications.error(e.message)
return
}
googleSaveButtonDisabled = true
originalGoogleDoc = cloneDeep(providers.google)
}
const copyToClipboard = async value => {
await Helpers.copyToClipboard(value)
notifications.success("Copied")
}
onMount(async () => {
try {
await organisation.init()
} catch (error) {
notifications.error("Error getting org config")
}
// Fetch Google config
let googleDoc
try {
googleDoc = await API.getConfig(ConfigTypes.Google)
} catch (error) {
notifications.error("Error fetching Google OAuth config")
}
if (!googleDoc?._id) {
providers.google = {
type: ConfigTypes.Google,
config: { activated: false },
}
originalGoogleDoc = cloneDeep(googleDoc)
} else {
// Default activated to true for older configs
if (googleDoc.config.activated === undefined) {
googleDoc.config.activated = true
}
originalGoogleDoc = cloneDeep(googleDoc)
providers.google = googleDoc
}
googleCallbackUrl = providers?.google?.config?.callbackURL
})
</script>
{#if providers.google}
<Divider />
<Layout gap="XS" noPadding>
<Heading size="S">
<div class="provider-title">
<GoogleLogo />
<span>Google</span>
</div>
</Heading>
<Body size="S">
To allow users to authenticate using their Google accounts, fill out the
fields below. Read the <Link
size="M"
href={"https://docs.budibase.com/docs/sso-with-google"}
>documentation</Link
> for more information.
</Body>
</Layout>
<Layout gap="XS" noPadding>
{#each GoogleConfigFields.Google as field}
<div class="form-row">
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<div class="inputContainer">
<div class="input">
<Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
</div>
{#if field.copyButton}
<div
class="copy"
on:click={() => copyToClipboard(field.placeholder)}
>
<Icon size="S" name="Copy" />
</div>
{/if}
</div>
</div>
{/each}
<div class="form-row">
<Label size="L">Activated</Label>
<Toggle text="" bind:value={providers.google.config.activated} />
</div>
</Layout>
<div>
<Button
disabled={googleSaveButtonDisabled}
cta
on:click={() => saveGoogle()}
>
Save
</Button>
</div>
{/if}
<style>
.form-row {
display: grid;
grid-template-columns: 120px 1fr;
grid-gap: var(--spacing-l);
align-items: center;
}
.provider-title {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
gap: var(--spacing-m);
}
.provider-title span {
flex: 1 1 auto;
}
.inputContainer {
display: flex;
flex-direction: row;
}
.input {
flex: 1;
}
.copy {
display: flex;
align-items: center;
margin-left: 10px;
}
</style>

View File

@ -1,5 +1,4 @@
<script>
import GoogleLogo from "./_logos/Google.svelte"
import OidcLogo from "./_logos/OIDC.svelte"
import MicrosoftLogo from "assets/microsoft-logo.png"
import Auth0Logo from "assets/auth0-logo.png"
@ -28,9 +27,9 @@
import { API } from "api"
import { organisation, admin, licensing } from "stores/portal"
import Scim from "./scim.svelte"
import Google from "./google.svelte"
const ConfigTypes = {
Google: "google",
OIDC: "oidc",
}
@ -38,43 +37,6 @@
$: enforcedSSO = $organisation.isSSOEnforced
// Some older google configs contain a manually specified value - retain the functionality to edit the field
// When there is no value or we are in the cloud - prohibit editing the field, must use platform url to change
$: googleCallbackUrl = undefined
$: googleCallbackReadonly = $admin.cloud || !googleCallbackUrl
// Indicate to user that callback is based on platform url
// If there is an existing value, indicate that it may be removed to return to default behaviour
$: googleCallbackTooltip = $admin.cloud
? null
: googleCallbackReadonly
? "Visit the organisation page to update the platform URL"
: "Leave blank to use the default callback URL"
$: googleSheetsCallbackUrl = `${$organisation.platformUrl}/api/global/auth/datasource/google/callback`
$: GoogleConfigFields = {
Google: [
{ name: "clientID", label: "Client ID" },
{ name: "clientSecret", label: "Client secret" },
{
name: "callbackURL",
label: "Callback URL",
readonly: googleCallbackReadonly,
tooltip: googleCallbackTooltip,
placeholder: $organisation.googleCallbackUrl,
copyButton: true,
},
{
name: "sheetsURL",
label: "Sheets URL",
readonly: googleCallbackReadonly,
tooltip: googleCallbackTooltip,
placeholder: googleSheetsCallbackUrl,
copyButton: true,
},
],
}
$: OIDCConfigFields = {
Oidc: [
{ name: "configUrl", label: "Config URL" },
@ -133,15 +95,9 @@
const providers = { google, oidc }
// control the state of the save button depending on whether form has changed
let originalGoogleDoc
let originalOidcDoc
let googleSaveButtonDisabled
let oidcSaveButtonDisabled
$: {
isEqual(providers.google?.config, originalGoogleDoc?.config)
? (googleSaveButtonDisabled = true)
: (googleSaveButtonDisabled = false)
// delete the callback url which is never saved to the oidc
// config doc, to ensure an accurate comparison
delete providers.oidc?.config.configs[0].callbackURL
@ -151,10 +107,6 @@
: (oidcSaveButtonDisabled = false)
}
$: googleComplete = !!(
providers.google?.config?.clientID && providers.google?.config?.clientSecret
)
$: oidcComplete = !!(
providers.oidc?.config?.configs[0].configUrl &&
providers.oidc?.config?.configs[0].clientID &&
@ -230,30 +182,6 @@
originalOidcDoc = cloneDeep(providers.oidc)
}
async function saveGoogle() {
if (!googleComplete) {
notifications.error(
`Please fill in all required ${ConfigTypes.Google} fields`
)
return
}
const google = providers.google
try {
const res = await saveConfig(google)
providers[res.type]._rev = res._rev
providers[res.type]._id = res._id
notifications.success(`Settings saved`)
} catch (e) {
notifications.error(e.message)
return
}
googleSaveButtonDisabled = true
originalGoogleDoc = cloneDeep(providers.google)
}
let defaultScopes = ["profile", "email", "offline_access"]
const refreshScopes = idx => {
@ -281,29 +209,6 @@
notifications.error("Error getting org config")
}
// Fetch Google config
let googleDoc
try {
googleDoc = await API.getConfig(ConfigTypes.Google)
} catch (error) {
notifications.error("Error fetching Google OAuth config")
}
if (!googleDoc?._id) {
providers.google = {
type: ConfigTypes.Google,
config: { activated: false },
}
originalGoogleDoc = cloneDeep(googleDoc)
} else {
// Default activated to true for older configs
if (googleDoc.config.activated === undefined) {
googleDoc.config.activated = true
}
originalGoogleDoc = cloneDeep(googleDoc)
providers.google = googleDoc
}
googleCallbackUrl = providers?.google?.config?.callbackURL
// Get the list of user uploaded logos and push it to the dropdown options.
// This needs to be done before the config call so they're available when
// the dropdown renders.
@ -395,62 +300,7 @@
> before enabling this feature.
</Body>
</Layout>
{#if providers.google}
<Divider />
<Layout gap="XS" noPadding>
<Heading size="S">
<div class="provider-title">
<GoogleLogo />
<span>Google</span>
</div>
</Heading>
<Body size="S">
To allow users to authenticate using their Google accounts, fill out the
fields below. Read the <Link
size="M"
href={"https://docs.budibase.com/docs/sso-with-google"}
>documentation</Link
> for more information.
</Body>
</Layout>
<Layout gap="XS" noPadding>
{#each GoogleConfigFields.Google as field}
<div class="form-row">
<Label size="L" tooltip={field.tooltip}>{field.label}</Label>
<div class="inputContainer">
<div class="input">
<Input
bind:value={providers.google.config[field.name]}
readonly={field.readonly}
placeholder={field.placeholder}
/>
</div>
{#if field.copyButton}
<div
class="copy"
on:click={() => copyToClipboard(field.placeholder)}
>
<Icon size="S" name="Copy" />
</div>
{/if}
</div>
</div>
{/each}
<div class="form-row">
<Label size="L">Activated</Label>
<Toggle text="" bind:value={providers.google.config.activated} />
</div>
</Layout>
<div>
<Button
disabled={googleSaveButtonDisabled}
cta
on:click={() => saveGoogle()}
>
Save
</Button>
</div>
{/if}
<Google />
{#if providers.oidc}
<Divider />
<Layout gap="XS" noPadding>

View File

@ -11,7 +11,7 @@ import { BuildSchemaErrors, InvalidColumns } from "../../constants"
import { getIntegration } from "../../integrations"
import { getDatasourceAndQuery } from "./row/utils"
import { invalidateDynamicVariables } from "../../threads/utils"
import { db as dbCore, context, events } from "@budibase/backend-core"
import { db as dbCore, context, events, cache } from "@budibase/backend-core"
import {
UserCtx,
Datasource,
@ -25,9 +25,11 @@ import {
FetchDatasourceInfoResponse,
IntegrationBase,
DatasourcePlus,
SourceName,
} from "@budibase/types"
import sdk from "../../sdk"
import { builderSocket } from "../../websockets"
import { setupCreationAuth as googleSetupCreationAuth } from "../../integrations/googlesheets"
function getErrorTables(errors: any, errorType: string) {
return Object.entries(errors)
@ -306,6 +308,12 @@ export async function update(ctx: UserCtx<any, UpdateDatasourceResponse>) {
builderSocket?.emitDatasourceUpdate(ctx, datasource)
}
const preSaveAction: Partial<Record<SourceName, any>> = {
[SourceName.GOOGLE_SHEETS]: async (datasource: Datasource) => {
await googleSetupCreationAuth(datasource.config as any)
},
}
export async function save(
ctx: UserCtx<CreateDatasourceRequest, CreateDatasourceResponse>
) {
@ -327,6 +335,10 @@ export async function save(
setDefaultDisplayColumns(datasource)
}
if (preSaveAction[datasource.source]) {
await preSaveAction[datasource.source](datasource)
}
const dbResp = await db.put(datasource)
await events.datasource.created(datasource)
datasource._rev = dbResp.rev

View File

@ -1,5 +1,6 @@
import {
ConnectionInfo,
Datasource,
DatasourceFeature,
DatasourceFieldType,
DatasourcePlus,
@ -19,13 +20,15 @@ import { OAuth2Client } from "google-auth-library"
import { buildExternalTableId, finaliseExternalTables } from "./utils"
import { GoogleSpreadsheet, GoogleSpreadsheetRow } from "google-spreadsheet"
import fetch from "node-fetch"
import { configs, HTTPError } from "@budibase/backend-core"
import { cache, configs, context, HTTPError } from "@budibase/backend-core"
import { dataFilters } from "@budibase/shared-core"
import { GOOGLE_SHEETS_PRIMARY_KEY } from "../constants"
import sdk from "../sdk"
interface GoogleSheetsConfig {
spreadsheetId: string
auth: OAuthClientConfig
continueSetupId?: string
}
interface OAuthClientConfig {
@ -72,7 +75,7 @@ const SCHEMA: Integration = {
},
datasource: {
spreadsheetId: {
display: "Google Sheet URL",
display: "Spreadsheet URL",
type: DatasourceFieldType.STRING,
required: true,
},
@ -147,6 +150,7 @@ class GoogleSheetsIntegration implements DatasourcePlus {
async testConnection(): Promise<ConnectionInfo> {
try {
await setupCreationAuth(this.config)
await this.connect()
return { connected: true }
} catch (e: any) {
@ -566,6 +570,18 @@ class GoogleSheetsIntegration implements DatasourcePlus {
}
}
export async function setupCreationAuth(datasouce: GoogleSheetsConfig) {
if (datasouce.continueSetupId) {
const appId = context.getAppId()
const tokens = await cache.get(
`datasource:creation:${appId}:google:${datasouce.continueSetupId}`
)
datasouce.auth = tokens.tokens
delete datasouce.continueSetupId
}
}
export default {
schema: SCHEMA,
integration: GoogleSheetsIntegration,

View File

@ -1,6 +1,6 @@
import authorized from "../middleware/authorized"
import { BaseSocket } from "./websocket"
import { permissions } from "@budibase/backend-core"
import { permissions, events } from "@budibase/backend-core"
import http from "http"
import Koa from "koa"
import { Datasource, Table, SocketSession, ContextUser } from "@budibase/types"
@ -22,6 +22,9 @@ export default class BuilderSocket extends BaseSocket {
// Reply with all users in current room
const sessions = await this.getRoomSessions(appId)
callback({ users: sessions })
// Track usage
await events.user.dataCollaboration(sessions.length)
})
}

View File

@ -26,6 +26,9 @@ export enum Event {
USER_PASSWORD_RESET_REQUESTED = "user:password:reset:requested",
USER_PASSWORD_RESET = "user:password:reset",
// USER / COLLABORATION
USER_DATA_COLLABORATION = "user:data:collaboration",
// EMAIL
EMAIL_SMTP_CREATED = "email:smtp:created",
EMAIL_SMTP_UPDATED = "email:smtp:updated",
@ -233,6 +236,7 @@ export const AuditedEventFriendlyName: Record<Event, string | undefined> = {
[Event.USER_PASSWORD_FORCE_RESET]: undefined,
[Event.USER_GROUP_ONBOARDING]: undefined,
[Event.USER_ONBOARDING_COMPLETE]: undefined,
[Event.USER_DATA_COLLABORATION]: undefined,
// EMAIL
[Event.EMAIL_SMTP_CREATED]: `Email configuration created`,

View File

@ -86,3 +86,7 @@ export interface UserPasswordResetEvent extends BaseEvent {
email: string
}
}
export interface UserDataCollaborationEvent extends BaseEvent {
users: number
}

View File

@ -140,7 +140,6 @@ export const datasourcePreAuth = async (ctx: any, next: any) => {
{
provider,
appId: ctx.query.appId,
datasourceId: ctx.query.datasourceId,
},
Cookie.DatasourceAuth
)

View File

@ -2,7 +2,7 @@
if [ -z "$1" ]
then
echo "Error: version number is required. Usage: $0 [major|minor|patch|prerelease]"
echo "Error: bump type is required. Usage: $0 [major|minor|patch|prerelease]"
exit 1
fi