Merge branch 'master' into feat/ai-license-key-fix
This commit is contained in:
commit
eda4e2022f
|
@ -9,6 +9,7 @@ export enum Cookie {
|
|||
ACCOUNT_RETURN_URL = "budibase:account:returnurl",
|
||||
DatasourceAuth = "budibase:datasourceauth",
|
||||
OIDC_CONFIG = "budibase:oidc:config",
|
||||
FeatureFlags = "budibase:featureflags",
|
||||
}
|
||||
|
||||
export { Header } from "@budibase/shared-core"
|
||||
|
|
|
@ -460,6 +460,17 @@ export function setFeatureFlags(key: string, value: Record<string, boolean>) {
|
|||
context.featureFlagCache[key] = value
|
||||
}
|
||||
|
||||
export function getFeatureFlagOverrides(): Record<string, boolean> {
|
||||
return getCurrentContext()?.featureFlagOverrides || {}
|
||||
}
|
||||
|
||||
export async function doInFeatureFlagOverrideContext<T>(
|
||||
value: Record<string, boolean>,
|
||||
callback: () => Promise<T>
|
||||
) {
|
||||
return await newContext({ featureFlagOverrides: value }, callback)
|
||||
}
|
||||
|
||||
export function getTableForView(viewId: string): Table | undefined {
|
||||
const context = getCurrentContext()
|
||||
if (!context) {
|
||||
|
|
|
@ -24,5 +24,6 @@ export type ContextMap = {
|
|||
featureFlagCache?: {
|
||||
[key: string]: Record<string, boolean>
|
||||
}
|
||||
featureFlagOverrides?: Record<string, boolean>
|
||||
viewToTableCache?: Record<string, Table>
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// BASE
|
||||
|
||||
import { ErrorCode } from "@budibase/types"
|
||||
|
||||
export abstract class BudibaseError extends Error {
|
||||
code: string
|
||||
|
||||
|
@ -13,13 +15,6 @@ export abstract class BudibaseError extends Error {
|
|||
|
||||
// ERROR HANDLING
|
||||
|
||||
export enum ErrorCode {
|
||||
USAGE_LIMIT_EXCEEDED = "usage_limit_exceeded",
|
||||
FEATURE_DISABLED = "feature_disabled",
|
||||
INVALID_API_KEY = "invalid_api_key",
|
||||
HTTP = "http",
|
||||
}
|
||||
|
||||
/**
|
||||
* For the given error, build the public representation that is safe
|
||||
* to be exposed over an api.
|
||||
|
|
|
@ -175,6 +175,21 @@ export class FlagSet<T extends { [name: string]: boolean }> {
|
|||
}
|
||||
}
|
||||
|
||||
const overrides = context.getFeatureFlagOverrides()
|
||||
for (const [key, value] of Object.entries(overrides)) {
|
||||
if (!this.isFlagName(key)) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (typeof value !== "boolean") {
|
||||
continue
|
||||
}
|
||||
|
||||
// @ts-expect-error - TS does not like you writing into a generic type.
|
||||
flagValues[key] = value
|
||||
tags[`flags.${key}.source`] = "override"
|
||||
}
|
||||
|
||||
context.setFeatureFlags(this.setId, flagValues)
|
||||
for (const [key, value] of Object.entries(flagValues)) {
|
||||
tags[`flags.${key}.value`] = value
|
||||
|
|
|
@ -16,11 +16,12 @@ import env from "../environment"
|
|||
import {
|
||||
Ctx,
|
||||
EndpointMatcher,
|
||||
ErrorCode,
|
||||
LoginMethod,
|
||||
SessionCookie,
|
||||
User,
|
||||
} from "@budibase/types"
|
||||
import { ErrorCode, InvalidAPIKeyError } from "../errors"
|
||||
import { InvalidAPIKeyError } from "../errors"
|
||||
import tracer from "dd-trace"
|
||||
import type { Middleware, Next } from "koa"
|
||||
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
import { Ctx, FeatureFlagCookie } from "@budibase/types"
|
||||
import { Middleware, Next } from "koa"
|
||||
import { getCookie } from "../utils"
|
||||
import { Cookie } from "../constants"
|
||||
import { doInFeatureFlagOverrideContext } from "../context"
|
||||
|
||||
export default (async (ctx: Ctx, next: Next) => {
|
||||
const cookie = getCookie<FeatureFlagCookie>(ctx, Cookie.FeatureFlags)
|
||||
const flags = cookie?.flags || {}
|
||||
await doInFeatureFlagOverrideContext(flags, async () => {
|
||||
await next()
|
||||
})
|
||||
}) as Middleware
|
|
@ -20,5 +20,6 @@ export { default as correlation } from "../logging/correlation/middleware"
|
|||
export { default as errorHandling } from "./errorHandling"
|
||||
export { default as querystringToBody } from "./querystringToBody"
|
||||
export { default as csp } from "./contentSecurityPolicy"
|
||||
export { default as featureFlagCookie } from "./featureFlagCookie"
|
||||
export * as joiValidator from "./joi-validator"
|
||||
export { default as ip } from "./ip"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
notifications,
|
||||
} from "@budibase/bbui"
|
||||
import { API } from "@/api"
|
||||
import { goto } from "@roxi/routify"
|
||||
import { goto, params, isActive } from "@roxi/routify"
|
||||
import {
|
||||
automationStore,
|
||||
previewStore,
|
||||
|
@ -19,54 +19,36 @@
|
|||
queries,
|
||||
tables,
|
||||
views,
|
||||
viewsV2,
|
||||
} from "@/stores/builder"
|
||||
import { themeStore } from "@/stores/portal"
|
||||
import { themeStore, featureFlags } from "@/stores/portal"
|
||||
import { getContext } from "svelte"
|
||||
import { ThemeOptions } from "@budibase/shared-core"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
|
||||
const modalContext = getContext(Context.Modal)
|
||||
const commands = [
|
||||
|
||||
let search
|
||||
let selected = null
|
||||
|
||||
$: inApp = $isActive("/builder/app/:application")
|
||||
$: commands = [
|
||||
{
|
||||
type: "Access",
|
||||
name: "Invite users and manage app access",
|
||||
description: "",
|
||||
icon: "User",
|
||||
action: () => builderStore.showBuilderSidePanel(),
|
||||
requiresApp: true,
|
||||
},
|
||||
{
|
||||
type: "Navigate",
|
||||
name: "Portal",
|
||||
description: "",
|
||||
icon: "Compass",
|
||||
action: () => $goto("../../portal"),
|
||||
},
|
||||
{
|
||||
type: "Navigate",
|
||||
name: "Data",
|
||||
description: "",
|
||||
icon: "Compass",
|
||||
action: () => $goto("./data"),
|
||||
},
|
||||
{
|
||||
type: "Navigate",
|
||||
name: "Design",
|
||||
description: "",
|
||||
icon: "Compass",
|
||||
action: () => $goto("./design"),
|
||||
},
|
||||
{
|
||||
type: "Navigate",
|
||||
name: "Automations",
|
||||
description: "",
|
||||
icon: "Compass",
|
||||
action: () => $goto("./automation"),
|
||||
},
|
||||
...navigationCommands(),
|
||||
{
|
||||
type: "Publish",
|
||||
name: "App",
|
||||
description: "Deploy your application",
|
||||
icon: "Box",
|
||||
action: deployApp,
|
||||
requiresApp: true,
|
||||
},
|
||||
{
|
||||
type: "Preview",
|
||||
|
@ -74,12 +56,14 @@
|
|||
description: "",
|
||||
icon: "Play",
|
||||
action: () => previewStore.showPreview(true),
|
||||
requiresApp: true,
|
||||
},
|
||||
{
|
||||
type: "Preview",
|
||||
name: "Published App",
|
||||
icon: "Play",
|
||||
action: () => window.open(`/app${$appStore.url}`),
|
||||
requiresApp: true,
|
||||
},
|
||||
{
|
||||
type: "Support",
|
||||
|
@ -87,6 +71,7 @@
|
|||
icon: "Help",
|
||||
action: () =>
|
||||
window.open(`https://github.com/Budibase/budibase/discussions/new`),
|
||||
requiresApp: true,
|
||||
},
|
||||
{
|
||||
type: "Support",
|
||||
|
@ -96,52 +81,166 @@
|
|||
window.open(
|
||||
`https://github.com/Budibase/budibase/issues/new?assignees=&labels=bug&template=bug_report.md&title=`
|
||||
),
|
||||
requiresApp: true,
|
||||
},
|
||||
...($datasources?.list?.map(datasource => ({
|
||||
...datasourceCommands($datasources?.list || []),
|
||||
...tableCommands($tables?.list || []),
|
||||
...viewCommands($views?.list || []),
|
||||
...viewV2Commands($viewsV2?.list || []),
|
||||
...queryCommands($queries?.list || []),
|
||||
...screenCommands($sortedScreens),
|
||||
...automationCommands($automationStore?.automations || []),
|
||||
...themeCommands(),
|
||||
...featureFlagCommands($featureFlags),
|
||||
]
|
||||
$: enrichedCommands = commands.map(cmd => ({
|
||||
...cmd,
|
||||
searchValue: `${cmd.type} ${cmd.name}`.toLowerCase().replace(/_/g, " "),
|
||||
}))
|
||||
$: results = filterResults(enrichedCommands, search, inApp)
|
||||
$: categories = groupResults(results)
|
||||
|
||||
const navigationCommands = () => {
|
||||
const routes = [
|
||||
{
|
||||
name: "Portal",
|
||||
url: "/builder/portal",
|
||||
},
|
||||
{
|
||||
name: "Data",
|
||||
url: "/builder/app/:application/data",
|
||||
},
|
||||
{
|
||||
name: "Design",
|
||||
url: "/builder/app/:application/design",
|
||||
},
|
||||
{
|
||||
name: "Automations",
|
||||
url: "/builder/app/:application/automation",
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
url: "/builder/app/:application/settings",
|
||||
},
|
||||
]
|
||||
return routes.map(route => ({
|
||||
type: "Navigate",
|
||||
name: route.name,
|
||||
icon: "Compass",
|
||||
action: () => {
|
||||
const gotoParams = route.url.includes(":application")
|
||||
? { application: $params.application }
|
||||
: {}
|
||||
$goto(route.url, gotoParams)
|
||||
},
|
||||
requiresApp: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const datasourceCommands = datasources => {
|
||||
return datasources.map(datasource => ({
|
||||
type: "Datasource",
|
||||
name: `${datasource.name}`,
|
||||
name: datasource.name,
|
||||
icon: "Data",
|
||||
action: () => $goto(`./data/datasource/${datasource._id}`),
|
||||
})) ?? []),
|
||||
...($tables?.list?.map(table => ({
|
||||
action: () =>
|
||||
$goto(`/builder/app/:application/data/datasource/:id`, {
|
||||
application: $params.application,
|
||||
id: datasource._id,
|
||||
}),
|
||||
requiresApp: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const tableCommands = tables => {
|
||||
return tables.map(table => ({
|
||||
type: "Table",
|
||||
name: table.name,
|
||||
icon: "Table",
|
||||
action: () => $goto(`./data/table/${table._id}`),
|
||||
})) ?? []),
|
||||
...($views?.list?.map(view => ({
|
||||
action: () =>
|
||||
$goto(`/builder/app/:application/data/table/:id`, {
|
||||
application: $params.application,
|
||||
id: table._id,
|
||||
}),
|
||||
requiresApp: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const viewCommands = views => {
|
||||
return views.map(view => ({
|
||||
type: "View",
|
||||
name: view.name,
|
||||
icon: "Remove",
|
||||
action: () => {
|
||||
if (view.version === 2) {
|
||||
$goto(`./data/view/v2/${view.id}`)
|
||||
} else {
|
||||
$goto(`./data/view/${view.name}`)
|
||||
}
|
||||
$goto(`/builder/app/:application/data/view/:name`, {
|
||||
application: $params.application,
|
||||
name: view.name,
|
||||
})
|
||||
},
|
||||
})) ?? []),
|
||||
...($queries?.list?.map(query => ({
|
||||
requiresApp: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const viewV2Commands = views => {
|
||||
return views.map(view => ({
|
||||
type: "View",
|
||||
name: view.name,
|
||||
icon: "Remove",
|
||||
action: () => {
|
||||
$goto(`/builder/app/:application/data/table/:tableId/:viewId`, {
|
||||
application: $params.application,
|
||||
x: view.tableId,
|
||||
viewId: view.id,
|
||||
})
|
||||
},
|
||||
requiresApp: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const queryCommands = queries => {
|
||||
return queries.map(query => ({
|
||||
type: "Query",
|
||||
name: query.name,
|
||||
icon: "SQLQuery",
|
||||
action: () => $goto(`./data/query/${query._id}`),
|
||||
})) ?? []),
|
||||
...$sortedScreens.map(screen => ({
|
||||
action: () =>
|
||||
$goto(`/builder/app/:application/data/query/:id`, {
|
||||
application: $params.application,
|
||||
id: query._id,
|
||||
}),
|
||||
requiresApp: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const screenCommands = screens => {
|
||||
return screens.map(screen => ({
|
||||
type: "Screen",
|
||||
name: screen.routing.route,
|
||||
icon: "WebPage",
|
||||
action: () => {
|
||||
$goto(`./design/${screen._id}/${screen._id}-screen`)
|
||||
},
|
||||
})),
|
||||
...($automationStore?.automations?.map(automation => ({
|
||||
action: () =>
|
||||
$goto(`/builder/app/:application/design/:screenId/:componentId`, {
|
||||
application: $params.application,
|
||||
screenId: screen._id,
|
||||
componentId: `${screen._id}-screen`,
|
||||
}),
|
||||
requiresApp: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const automationCommands = automations => {
|
||||
return automations.map(automation => ({
|
||||
type: "Automation",
|
||||
name: automation.name,
|
||||
icon: "ShareAndroid",
|
||||
action: () => $goto(`./automation/${automation._id}`),
|
||||
})) ?? []),
|
||||
...ThemeOptions.map(themeMeta => ({
|
||||
action: () =>
|
||||
$goto(`/builder/app/:application/automation/:id`, {
|
||||
application: $params.application,
|
||||
id: automation._id,
|
||||
}),
|
||||
requiresApp: true,
|
||||
}))
|
||||
}
|
||||
|
||||
const themeCommands = () => {
|
||||
return ThemeOptions.map(themeMeta => ({
|
||||
type: "Change Builder Theme",
|
||||
name: themeMeta.name,
|
||||
icon: "ColorPalette",
|
||||
|
@ -150,28 +249,41 @@
|
|||
state.theme = themeMeta.id
|
||||
return state
|
||||
}),
|
||||
})),
|
||||
]
|
||||
|
||||
let search
|
||||
let selected = null
|
||||
|
||||
$: enrichedCommands = commands.map(cmd => ({
|
||||
...cmd,
|
||||
searchValue: `${cmd.type} ${cmd.name}`.toLowerCase(),
|
||||
}))
|
||||
$: results = filterResults(enrichedCommands, search)
|
||||
$: categories = groupResults(results)
|
||||
|
||||
const filterResults = (commands, search) => {
|
||||
if (!search) {
|
||||
selected = null
|
||||
return commands
|
||||
}
|
||||
|
||||
const featureFlagCommands = flags => {
|
||||
if (!flags.DEBUG_UI) {
|
||||
return []
|
||||
}
|
||||
return Object.entries(flags)
|
||||
.filter(([flag]) => flag !== FeatureFlag.DEBUG_UI)
|
||||
.map(([flag, value]) => ({
|
||||
type: "Feature Flag",
|
||||
name: `${value ? "Disable" : "Enable"} <code>${flag}</code>`,
|
||||
icon: "Flag",
|
||||
action: () => {
|
||||
featureFlags.setFlag(flag, !value)
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
const filterResults = (commands, search, inApp) => {
|
||||
if (search) {
|
||||
selected = 0
|
||||
search = search.toLowerCase()
|
||||
search = search.toLowerCase().replace(/_/g, " ")
|
||||
} else {
|
||||
selected = null
|
||||
}
|
||||
return commands
|
||||
.filter(cmd => cmd.searchValue.includes(search))
|
||||
.filter(cmd => {
|
||||
// Handle searching
|
||||
if (search && !cmd.searchValue.includes(search)) {
|
||||
return false
|
||||
}
|
||||
// Handle commands that require an app
|
||||
return inApp || !cmd.requiresApp
|
||||
})
|
||||
.map((cmd, idx) => ({
|
||||
...cmd,
|
||||
idx,
|
||||
|
@ -264,7 +376,8 @@
|
|||
<Icon size="M" name={command.icon} />
|
||||
<strong>{command.type}: </strong>
|
||||
<div class="name">
|
||||
{command.name}
|
||||
<!--eslint-disable-next-line svelte/no-at-html-tags-->
|
||||
{@html command.name}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
@ -339,4 +452,10 @@
|
|||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.name :global(code) {
|
||||
font-size: 12px;
|
||||
background: var(--background-alt);
|
||||
padding: 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import { createEventDispatcher } from "svelte"
|
||||
import { API } from "@/api"
|
||||
import type { EnrichedBinding } from "@budibase/types"
|
||||
import { ErrorCode, type EnrichedBinding } from "@budibase/types"
|
||||
import analytics, { Events } from "@/analytics"
|
||||
import AiInput from "../ai/AIInput.svelte"
|
||||
|
||||
|
@ -43,17 +43,26 @@
|
|||
const resp = await API.generateJs({ prompt, bindings })
|
||||
const code = resp.code
|
||||
if (code === "") {
|
||||
throw new Error("We didn't understand your prompt. Please rephrase it.")
|
||||
throw new Error(
|
||||
"We didn't understand your prompt. This can happen if the prompt isn't specific, or if it's a request for something other than code. Try expressing your request in a different way."
|
||||
)
|
||||
}
|
||||
suggestedCode = code
|
||||
dispatch("update", { code })
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
if (!(e instanceof Error)) {
|
||||
notifications.error("Unable to generate code. Please try again later.")
|
||||
return
|
||||
}
|
||||
|
||||
if ("code" in e && e.code === ErrorCode.USAGE_LIMIT_EXCEEDED) {
|
||||
notifications.error(
|
||||
e instanceof Error
|
||||
? `Unable to generate code: ${e.message}`
|
||||
: "Unable to generate code. Please try again later."
|
||||
"Monthly usage limit reached. We're exploring options to expand this soon. Questions? Contact support@budibase.com"
|
||||
)
|
||||
} else {
|
||||
notifications.error(`Unable to generate code: ${e.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -78,6 +87,8 @@
|
|||
function reset() {
|
||||
suggestedCode = null
|
||||
previousContents = null
|
||||
promptText = ""
|
||||
expanded = false
|
||||
}
|
||||
|
||||
function calculateExpandedWidth() {
|
||||
|
@ -103,7 +114,6 @@
|
|||
placeholder="Generate with AI"
|
||||
onSubmit={generateJs}
|
||||
bind:expanded
|
||||
on:collapse={rejectSuggestion}
|
||||
readonly={!!suggestedCode}
|
||||
{expandedOnly}
|
||||
/>
|
||||
|
@ -121,21 +131,11 @@
|
|||
overflow: visible;
|
||||
}
|
||||
|
||||
@keyframes border-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.floating-actions {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
bottom: calc(100% + 5px);
|
||||
left: 5px;
|
||||
z-index: 2;
|
||||
animation: fade-in 0.2s ease-out forwards;
|
||||
}
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import { Label } from "@budibase/bbui"
|
||||
import { onMount, createEventDispatcher, onDestroy } from "svelte"
|
||||
import { FIND_ANY_HBS_REGEX } from "@budibase/string-templates"
|
||||
|
||||
import {
|
||||
autocompletion,
|
||||
closeBrackets,
|
||||
|
@ -52,17 +51,12 @@
|
|||
import type { Extension } from "@codemirror/state"
|
||||
import { javascript } from "@codemirror/lang-javascript"
|
||||
import { EditorModes } from "./"
|
||||
import { themeStore } from "@/stores/portal"
|
||||
import {
|
||||
type EnrichedBinding,
|
||||
FeatureFlag,
|
||||
type EditorMode,
|
||||
} from "@budibase/types"
|
||||
import { featureFlags, themeStore } from "@/stores/portal"
|
||||
import { type EnrichedBinding, type EditorMode } from "@budibase/types"
|
||||
import { tooltips } from "@codemirror/view"
|
||||
import type { BindingCompletion, CodeValidator } from "@/types"
|
||||
import { validateHbsTemplate } from "./validator/hbs"
|
||||
import { validateJsTemplate } from "./validator/js"
|
||||
import { featureFlag } from "@/helpers"
|
||||
import AIGen from "./AIGen.svelte"
|
||||
|
||||
export let label: string | undefined = undefined
|
||||
|
@ -88,6 +82,7 @@
|
|||
let isEditorInitialised = false
|
||||
let queuedRefresh = false
|
||||
let editorWidth: number | null = null
|
||||
let isAIGeneratedContent = false
|
||||
|
||||
// Theming!
|
||||
let currentTheme = $themeStore?.theme
|
||||
|
@ -101,9 +96,7 @@
|
|||
}
|
||||
|
||||
$: aiGenEnabled =
|
||||
featureFlag.isEnabled(FeatureFlag.AI_JS_GENERATION) &&
|
||||
mode.name === "javascript" &&
|
||||
!readonly
|
||||
$featureFlags.AI_JS_GENERATION && mode.name === "javascript" && !readonly
|
||||
|
||||
$: {
|
||||
if (autofocus && isEditorInitialised) {
|
||||
|
@ -429,6 +422,7 @@
|
|||
editor.dispatch({
|
||||
changes: { from: 0, to: editor.state.doc.length, insert: code },
|
||||
})
|
||||
isAIGeneratedContent = true
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
|
@ -462,7 +456,12 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<div class={`code-editor ${mode?.name || ""}`} bind:this={editorEle}>
|
||||
<div
|
||||
class={`code-editor ${mode?.name || ""} ${
|
||||
isAIGeneratedContent ? "ai-generated" : ""
|
||||
}`}
|
||||
bind:this={editorEle}
|
||||
>
|
||||
<div tabindex="-1" bind:this={textarea} />
|
||||
</div>
|
||||
|
||||
|
@ -475,6 +474,7 @@
|
|||
on:accept={() => {
|
||||
dispatch("change", editor.state.doc.toString())
|
||||
dispatch("blur", editor.state.doc.toString())
|
||||
isAIGeneratedContent = false
|
||||
}}
|
||||
on:reject={event => {
|
||||
const { code } = event.detail
|
||||
|
@ -482,6 +482,7 @@
|
|||
editor.dispatch({
|
||||
changes: { from: 0, to: editor.state.doc.length, insert: code || "" },
|
||||
})
|
||||
isAIGeneratedContent = false
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
|
@ -691,4 +692,19 @@
|
|||
text-overflow: ellipsis !important;
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
|
||||
.code-editor.ai-generated :global(.cm-editor) {
|
||||
background: var(--spectrum-global-color-blue-50) !important;
|
||||
}
|
||||
|
||||
.code-editor.ai-generated :global(.cm-content) {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.code-editor.ai-generated :global(.cm-line) {
|
||||
background: #765ffe1a !important;
|
||||
display: inline-block;
|
||||
min-width: fit-content;
|
||||
padding-right: 2px !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
export const submit = onPromptSubmit
|
||||
|
||||
$: expanded = expandedOnly || expanded
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let promptInput: HTMLInputElement
|
||||
|
@ -28,7 +27,7 @@
|
|||
$: aiEnabled = $auth?.user?.llm
|
||||
|
||||
$: creditsExceeded = $licensing.aiCreditsExceeded
|
||||
$: disabled = !aiEnabled || creditsExceeded || readonly || promptLoading
|
||||
$: disabled = !aiEnabled || creditsExceeded || readonly
|
||||
$: animateBorder = !disabled && expanded
|
||||
|
||||
$: canSubmit = !readonly && !!value
|
||||
|
@ -91,6 +90,7 @@
|
|||
src={BBAI}
|
||||
alt="AI"
|
||||
class="ai-icon"
|
||||
class:loading={promptLoading}
|
||||
class:disabled={expanded && disabled}
|
||||
on:click={e => {
|
||||
e.stopPropagation()
|
||||
|
@ -105,7 +105,7 @@
|
|||
class="prompt-input"
|
||||
{placeholder}
|
||||
on:keydown={handleKeyPress}
|
||||
{disabled}
|
||||
disabled={disabled || promptLoading}
|
||||
/>
|
||||
{:else}
|
||||
<span class="spectrum-ActionButton-label ai-gen-text">
|
||||
|
@ -153,11 +153,11 @@
|
|||
</Modal>
|
||||
{:else}
|
||||
<Icon
|
||||
color={promptLoading
|
||||
color={promptInput
|
||||
? "#6E56FF"
|
||||
: "var(--spectrum-global-color-gray-600)"}
|
||||
size="S"
|
||||
disabled={!canSubmit}
|
||||
disabled={!canSubmit || promptLoading}
|
||||
hoverable={!readonly}
|
||||
hoverColor="#6E56FF"
|
||||
name={promptLoading ? "StopCircle" : "PlayCircle"}
|
||||
|
@ -170,14 +170,12 @@
|
|||
|
||||
<style>
|
||||
.spectrum-ActionButton {
|
||||
--offset: 1px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
padding: var(--spacing-s);
|
||||
border: 1px solid var(--spectrum-alias-border-color);
|
||||
border-radius: 30px;
|
||||
transition: width 0.8s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
width: 100%;
|
||||
|
@ -185,16 +183,30 @@
|
|||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
background-color: var(--spectrum-global-color-gray-75);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.spectrum-ActionButton::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
width: calc(100% + 2px);
|
||||
height: calc(100% + 2px);
|
||||
border-radius: inherit;
|
||||
inset: 0;
|
||||
border-radius: 30px;
|
||||
padding: 1px;
|
||||
background: var(--spectrum-alias-border-color);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
-webkit-mask-composite: xor;
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.animate-border::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: 30px;
|
||||
padding: 1px;
|
||||
background: linear-gradient(
|
||||
125deg,
|
||||
transparent -10%,
|
||||
|
@ -204,40 +216,42 @@
|
|||
transparent 35%,
|
||||
transparent 110%
|
||||
);
|
||||
-webkit-mask: linear-gradient(#fff 0 0) content-box,
|
||||
linear-gradient(#fff 0 0);
|
||||
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
|
||||
mask-composite: exclude;
|
||||
pointer-events: none;
|
||||
z-index: 0;
|
||||
animation: border-flow 1.5s cubic-bezier(0.17, 0.67, 0.83, 0.67) forwards;
|
||||
}
|
||||
|
||||
.spectrum-ActionButton:not(.animate-border)::before {
|
||||
@keyframes border-flow {
|
||||
0% {
|
||||
clip-path: polygon(0% 0%, 10% 0%, 8% 100%, 0% 100%);
|
||||
}
|
||||
30% {
|
||||
clip-path: polygon(0% 0%, 35% 0%, 26% 100%, 0% 100%);
|
||||
}
|
||||
50% {
|
||||
clip-path: polygon(0% 0%, 55% 0%, 41% 100%, 0% 100%);
|
||||
}
|
||||
70% {
|
||||
clip-path: polygon(0% 0%, 70% 0%, 53% 100%, 0% 100%);
|
||||
}
|
||||
85% {
|
||||
clip-path: polygon(0% 0%, 80% 0%, 60% 100%, 0% 100%);
|
||||
}
|
||||
95% {
|
||||
clip-path: polygon(0% 0%, 86% 0%, 65% 100%, 0% 100%);
|
||||
}
|
||||
100% {
|
||||
clip-path: polygon(0% 0%, 90% 0%, 68% 100%, 0% 100%);
|
||||
}
|
||||
}
|
||||
|
||||
.spectrum-ActionButton:not(.animate-border)::after {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.animate-border::before {
|
||||
animation: border-fade-in 1s cubic-bezier(0.17, 0.67, 0.83, 0.67);
|
||||
animation-fill-mode: forwards;
|
||||
}
|
||||
|
||||
@keyframes border-fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.spectrum-ActionButton::after {
|
||||
content: "";
|
||||
background: inherit;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
inset: var(--offset);
|
||||
height: calc(100% - 2 * var(--offset));
|
||||
width: calc(100% - 2 * var(--offset));
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
@ -268,10 +282,12 @@
|
|||
.ai-icon {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-left: 4px;
|
||||
margin-right: 8px;
|
||||
margin-left: var(--spacing-xs);
|
||||
margin-right: var(--spacing-s);
|
||||
flex-shrink: 0;
|
||||
cursor: var(--ai-icon-cursor, pointer);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.ai-gen-text {
|
||||
|
@ -280,10 +296,12 @@
|
|||
text-overflow: ellipsis;
|
||||
transition: opacity 0.2s ease-out;
|
||||
margin-right: var(--spacing-xs);
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.prompt-input {
|
||||
font-size: 14px;
|
||||
font-size: inherit;
|
||||
flex: 1;
|
||||
border: none;
|
||||
background: transparent;
|
||||
|
@ -293,6 +311,8 @@
|
|||
min-width: 0;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.prompt-input::placeholder {
|
||||
|
@ -303,14 +323,15 @@
|
|||
.action-buttons {
|
||||
display: flex;
|
||||
gap: var(--spacing-s);
|
||||
z-index: 4;
|
||||
z-index: 5;
|
||||
flex-shrink: 0;
|
||||
margin-right: var(--spacing-s);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.button-content-wrapper {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
|
@ -328,4 +349,17 @@
|
|||
filter: grayscale(1) brightness(1.5);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.ai-icon.loading {
|
||||
animation: spin 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,8 +6,11 @@
|
|||
import { API } from "@/api"
|
||||
import Branding from "./Branding.svelte"
|
||||
import ContextMenu from "@/components/ContextMenu.svelte"
|
||||
import CommandPalette from "@/components/commandPalette/CommandPalette.svelte"
|
||||
import { Modal } from "@budibase/bbui"
|
||||
|
||||
let loaded = false
|
||||
let commandPaletteModal
|
||||
|
||||
$: multiTenancyEnabled = $admin.multiTenancy
|
||||
$: hasAdminUser = $admin?.checklist?.adminUser?.checked
|
||||
|
@ -157,12 +160,25 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Event handler for the command palette
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
commandPaletteModal.toggle()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<!--Portal branding overrides -->
|
||||
<Branding />
|
||||
<ContextMenu />
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
<Modal bind:this={commandPaletteModal} zIndex={999999}>
|
||||
<CommandPalette />
|
||||
</Modal>
|
||||
|
||||
{#if loaded}
|
||||
<slot />
|
||||
{/if}
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
Tabs,
|
||||
Tab,
|
||||
Heading,
|
||||
Modal,
|
||||
notifications,
|
||||
TooltipPosition,
|
||||
} from "@budibase/bbui"
|
||||
|
@ -24,7 +23,6 @@
|
|||
import { capitalise } from "@/helpers"
|
||||
import { onMount, onDestroy } from "svelte"
|
||||
import VerificationPromptBanner from "@/components/common/VerificationPromptBanner.svelte"
|
||||
import CommandPalette from "@/components/commandPalette/CommandPalette.svelte"
|
||||
import TourWrap from "@/components/portal/onboarding/TourWrap.svelte"
|
||||
import TourPopover from "@/components/portal/onboarding/TourPopover.svelte"
|
||||
import BuilderSidePanel from "./_components/BuilderSidePanel.svelte"
|
||||
|
@ -38,7 +36,6 @@
|
|||
|
||||
let promise = getPackage()
|
||||
let hasSynced = false
|
||||
let commandPaletteModal
|
||||
let loaded = false
|
||||
|
||||
$: loaded && initTour()
|
||||
|
@ -79,14 +76,6 @@
|
|||
$goto($builderStore.previousTopNavPath[path] || path)
|
||||
}
|
||||
|
||||
// Event handler for the command palette
|
||||
const handleKeyDown = e => {
|
||||
if (e.key === "k" && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
commandPaletteModal.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
const initTour = async () => {
|
||||
// Check if onboarding is enabled.
|
||||
if (!$auth.user?.onboardedAt) {
|
||||
|
@ -184,11 +173,6 @@
|
|||
<PreviewOverlay />
|
||||
{/if}
|
||||
|
||||
<svelte:window on:keydown={handleKeyDown} />
|
||||
<Modal bind:this={commandPaletteModal} zIndex={999999}>
|
||||
<CommandPalette />
|
||||
</Modal>
|
||||
|
||||
<EnterpriseBasicTrialModal />
|
||||
|
||||
<style>
|
||||
|
|
|
@ -16,8 +16,7 @@
|
|||
import CreationPage from "@/components/common/CreationPage.svelte"
|
||||
import ICONS from "@/components/backend/DatasourceNavigator/icons/index.js"
|
||||
import AiTableGeneration from "./_components/AITableGeneration.svelte"
|
||||
import { featureFlag } from "@/helpers"
|
||||
import { FeatureFlag } from "@budibase/types"
|
||||
import { featureFlags } from "@/stores/portal"
|
||||
|
||||
let internalTableModal: CreateInternalTableModal
|
||||
let externalDatasourceModal: CreateExternalDatasourceModal
|
||||
|
@ -26,10 +25,7 @@
|
|||
let externalDatasourceLoading = false
|
||||
|
||||
$: disabled = sampleDataLoading || externalDatasourceLoading
|
||||
|
||||
$: aiTableGenerationEnabled = featureFlag.isEnabled(
|
||||
FeatureFlag.AI_TABLE_GENERATION
|
||||
)
|
||||
$: aiTableGenerationEnabled = $featureFlags.AI_TABLE_GENERATION
|
||||
|
||||
const createSampleData = async () => {
|
||||
sampleDataLoading = true
|
||||
|
|
|
@ -17,16 +17,9 @@ import { bindings } from "@/helpers"
|
|||
import { getBindableProperties } from "@/dataBinding"
|
||||
import { componentStore } from "./components"
|
||||
import { getSettingsDefinition } from "@budibase/frontend-core"
|
||||
import { utils } from "@budibase/shared-core"
|
||||
|
||||
function reduceBy<TItem extends {}, TKey extends keyof TItem>(
|
||||
key: TKey,
|
||||
list: TItem[]
|
||||
): Record<string, TItem> {
|
||||
return list.reduce<Record<string, TItem>>((result, item) => {
|
||||
result[item[key] as string] = item
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
const reduceBy = utils.toMap
|
||||
|
||||
const friendlyNameByType: Partial<Record<UIDatasourceType, string>> = {
|
||||
viewV2: "view",
|
||||
|
|
|
@ -1,8 +1,38 @@
|
|||
import { derived, Readable } from "svelte/store"
|
||||
import { auth } from "@/stores/portal"
|
||||
import { FeatureFlags, FeatureFlagDefaults } from "@budibase/types"
|
||||
import { FeatureFlag, FeatureFlags, FeatureFlagDefaults } from "@budibase/types"
|
||||
import { BudiStore } from "@/stores/BudiStore"
|
||||
import { API } from "@/api"
|
||||
import { notifications } from "@budibase/bbui"
|
||||
|
||||
export const featureFlags: Readable<FeatureFlags> = derived(auth, $auth => ({
|
||||
class FeatureFlagStore extends BudiStore<FeatureFlags> {
|
||||
constructor() {
|
||||
super(FeatureFlagDefaults)
|
||||
auth.subscribe($auth => {
|
||||
this.set({
|
||||
...FeatureFlagDefaults,
|
||||
...($auth?.user?.flags || {}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async setFlag(flag: FeatureFlag, value: boolean) {
|
||||
try {
|
||||
await API.overrideFeatureFlags({
|
||||
[flag]: value,
|
||||
})
|
||||
// The feature flag store is derived from the auth store, so we need to
|
||||
// refresh the auth store to update the feature flags.
|
||||
await auth.getSelf()
|
||||
notifications.success(
|
||||
`Feature flag ${flag} ${value ? "enabled" : "disabled"}`
|
||||
)
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
notifications.error(
|
||||
`Failed to set feature flag ${flag} to ${value}, see console for details`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const featureFlags = new FeatureFlagStore()
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import { OverrideFeatureFlagRequest } from "@budibase/types"
|
||||
import { BaseAPIClient } from "./types"
|
||||
|
||||
export interface FeatureFlagEndpoints {
|
||||
overrideFeatureFlags: (flags: Record<string, boolean>) => Promise<void>
|
||||
}
|
||||
|
||||
export const buildFeatureFlagEndpoints = (
|
||||
API: BaseAPIClient
|
||||
): FeatureFlagEndpoints => ({
|
||||
overrideFeatureFlags: async flags => {
|
||||
const body: OverrideFeatureFlagRequest = { flags }
|
||||
return await API.patch({
|
||||
url: "/api/features",
|
||||
body,
|
||||
parseResponse: () => {},
|
||||
})
|
||||
},
|
||||
})
|
|
@ -46,6 +46,7 @@ import { buildLogsEndpoints } from "./logs"
|
|||
import { buildMigrationEndpoints } from "./migrations"
|
||||
import { buildRowActionEndpoints } from "./rowActions"
|
||||
import { buildOAuth2Endpoints } from "./oauth2"
|
||||
import { buildFeatureFlagEndpoints } from "./features"
|
||||
|
||||
export type { APIClient } from "./types"
|
||||
|
||||
|
@ -289,6 +290,7 @@ export const createAPIClient = (config: APIClientConfig = {}): APIClient => {
|
|||
...buildAuditLogEndpoints(API),
|
||||
...buildLogsEndpoints(API),
|
||||
...buildMigrationEndpoints(API),
|
||||
...buildFeatureFlagEndpoints(API),
|
||||
viewV2: buildViewV2Endpoints(API),
|
||||
rowActions: buildRowActionEndpoints(API),
|
||||
oauth2: buildOAuth2Endpoints(API),
|
||||
|
|
|
@ -10,6 +10,7 @@ import { ConfigEndpoints } from "./configs"
|
|||
import { DatasourceEndpoints } from "./datasources"
|
||||
import { EnvironmentVariableEndpoints } from "./environmentVariables"
|
||||
import { EventEndpoints } from "./events"
|
||||
import { FeatureFlagEndpoints } from "./features"
|
||||
import { FlagEndpoints } from "./flags"
|
||||
import { GroupEndpoints } from "./groups"
|
||||
import { LayoutEndpoints } from "./layouts"
|
||||
|
@ -133,6 +134,7 @@ export type APIClient = BaseAPIClient &
|
|||
TableEndpoints &
|
||||
TemplateEndpoints &
|
||||
UserEndpoints &
|
||||
FeatureFlagEndpoints &
|
||||
ViewEndpoints & {
|
||||
rowActions: RowActionEndpoints
|
||||
viewV2: ViewV2Endpoints
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 350e18c4aaf482d050143abd01076778a771b69a
|
||||
Subproject commit cf45e608e393a7fc33d69e7a37b918aecac51100
|
|
@ -13,11 +13,13 @@ export async function generateTables(
|
|||
|
||||
const tableGenerator = await ai.TableGeneration.init({
|
||||
generateTablesDelegate: sdk.ai.helpers.generateTables,
|
||||
getTablesDelegate: sdk.tables.getTables,
|
||||
generateDataDelegate: sdk.ai.helpers.generateRows,
|
||||
})
|
||||
tableGenerator.withData(ctx.user._id || "")
|
||||
const createdTables = await tableGenerator.generate(prompt)
|
||||
|
||||
const createdTables = await tableGenerator.generate(
|
||||
prompt,
|
||||
ctx.user._id || ""
|
||||
)
|
||||
|
||||
ctx.body = {
|
||||
createdTables,
|
||||
|
|
|
@ -26,7 +26,6 @@ import {
|
|||
db as dbCore,
|
||||
docIds,
|
||||
env as envCore,
|
||||
ErrorCode,
|
||||
events,
|
||||
objectStore,
|
||||
roles,
|
||||
|
@ -70,6 +69,7 @@ import {
|
|||
AddAppSampleDataResponse,
|
||||
UnpublishAppResponse,
|
||||
SetRevertableAppVersionResponse,
|
||||
ErrorCode,
|
||||
} from "@budibase/types"
|
||||
import { BASE_LAYOUT_PROP_IDS } from "../../constants/layouts"
|
||||
import sdk from "../../sdk"
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
import {
|
||||
UserCtx,
|
||||
OverrideFeatureFlagRequest,
|
||||
FeatureFlagCookie,
|
||||
} from "@budibase/types"
|
||||
import { Cookie, utils } from "@budibase/backend-core"
|
||||
|
||||
export async function override(ctx: UserCtx<OverrideFeatureFlagRequest, void>) {
|
||||
const { flags } = ctx.request.body
|
||||
|
||||
let cookie = utils.getCookie<FeatureFlagCookie>(ctx, Cookie.FeatureFlags)
|
||||
if (!cookie) {
|
||||
cookie = {
|
||||
flags: {},
|
||||
}
|
||||
}
|
||||
|
||||
cookie.flags = {
|
||||
...cookie.flags,
|
||||
...flags,
|
||||
}
|
||||
|
||||
utils.setCookie(ctx, cookie, Cookie.FeatureFlags)
|
||||
|
||||
ctx.status = 200
|
||||
}
|
|
@ -91,6 +91,7 @@ export async function patch(ctx: UserCtx<PatchRowRequest, PatchRowResponse>) {
|
|||
|
||||
const result = await finaliseRow(source, row, {
|
||||
updateFormula: true,
|
||||
updateAIColumns: true,
|
||||
})
|
||||
|
||||
return { ...result, oldRow }
|
||||
|
|
|
@ -81,6 +81,7 @@ export async function updateRelatedFormula(
|
|||
relatedRows[tableId].map(related =>
|
||||
finaliseRow(relatedTable, related, {
|
||||
updateFormula: false,
|
||||
updateAIColumns: false,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
@ -136,10 +137,10 @@ export async function updateAllFormulasInTable(table: Table) {
|
|||
export async function finaliseRow(
|
||||
source: Table | ViewV2,
|
||||
row: Row,
|
||||
opts?: { updateFormula: boolean }
|
||||
opts?: { updateFormula: boolean; updateAIColumns: boolean }
|
||||
) {
|
||||
const db = context.getAppDB()
|
||||
const { updateFormula = true } = opts || {}
|
||||
const { updateFormula = true, updateAIColumns = true } = opts || {}
|
||||
const table = sdk.views.isView(source)
|
||||
? await sdk.views.getTable(source.id)
|
||||
: source
|
||||
|
@ -155,9 +156,11 @@ export async function finaliseRow(
|
|||
contextRows: [enrichedRow],
|
||||
})
|
||||
|
||||
if (updateAIColumns) {
|
||||
row = await processAIColumns(table, row, {
|
||||
contextRows: [enrichedRow],
|
||||
})
|
||||
}
|
||||
|
||||
await db.put(row)
|
||||
const retrieved = await db.tryGet<Row>(row._id)
|
||||
|
|
|
@ -31,7 +31,7 @@ router.get("/health", async ctx => {
|
|||
})
|
||||
router.get("/version", ctx => (ctx.body = envCore.VERSION))
|
||||
|
||||
router.use(middleware.errorHandling)
|
||||
router.use(middleware.errorHandling).use(middleware.featureFlagCookie)
|
||||
|
||||
// only add the routes if they are enabled
|
||||
if (apiEnabled()) {
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
import Router from "@koa/router"
|
||||
import * as controller from "../controllers/features"
|
||||
import { validateBody } from "../../middleware/zod-validator"
|
||||
import { z } from "zod"
|
||||
|
||||
const router: Router = new Router()
|
||||
|
||||
const validator = z.object({
|
||||
flags: z.record(z.boolean()),
|
||||
})
|
||||
|
||||
router.patch("/api/features", validateBody(validator), controller.override)
|
||||
|
||||
export default router
|
|
@ -30,6 +30,7 @@ import Router from "@koa/router"
|
|||
import { api as pro } from "@budibase/pro"
|
||||
import rowActionRoutes from "./rowAction"
|
||||
import oauth2Routes from "./oauth2"
|
||||
import featuresRoutes from "./features"
|
||||
import aiRoutes from "./ai"
|
||||
|
||||
export { default as staticRoutes } from "./static"
|
||||
|
@ -72,6 +73,7 @@ export const mainRoutes: Router[] = [
|
|||
rowActionRoutes,
|
||||
proAiRoutes,
|
||||
oauth2Routes,
|
||||
featuresRoutes,
|
||||
// these need to be handled last as they still use /api/:tableId
|
||||
// this could be breaking as koa may recognise other routes as this
|
||||
tableRoutes,
|
||||
|
|
|
@ -470,7 +470,9 @@ describe("BudibaseAI", () => {
|
|||
) =>
|
||||
mockChatGPTResponse(JSON.stringify(aiColumnGeneration), {
|
||||
format: zodResponseFormat(
|
||||
ai.aiColumnSchemas(generationStructure),
|
||||
ai.aiColumnSchemas(
|
||||
ai.aiTableResponseToTableSchema(generationStructure)
|
||||
),
|
||||
"key"
|
||||
),
|
||||
})
|
||||
|
@ -684,7 +686,7 @@ describe("BudibaseAI", () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
"Employees 2": [
|
||||
Employees: [
|
||||
{
|
||||
"First Name": "Joshua",
|
||||
"Last Name": "Lee",
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { FieldSchema, FieldType, Table } from "@budibase/types"
|
||||
import { FieldSchema, FieldType, TableSchema } from "@budibase/types"
|
||||
import sdk from "../../.."
|
||||
import { uploadFile, uploadUrl } from "../../../../utilities"
|
||||
|
||||
export async function generateRows(
|
||||
data: Record<string, Record<string, any>[]>,
|
||||
userId: string,
|
||||
tables: Record<string, Table>
|
||||
tables: Record<string, { _id: string; schema: TableSchema }>
|
||||
) {
|
||||
const createdData: Record<string, Record<string, string>> = {}
|
||||
const toUpdateLinks: {
|
||||
|
@ -13,6 +13,9 @@ export async function generateRows(
|
|||
rowId: string
|
||||
data: Record<string, { rowId: string[]; tableId: string }>
|
||||
}[] = []
|
||||
|
||||
const rowPromises = []
|
||||
|
||||
for (const tableName of Object.keys(data)) {
|
||||
const table = tables[tableName]
|
||||
const linksOverride: Record<string, null> = {}
|
||||
|
@ -26,7 +29,8 @@ export async function generateRows(
|
|||
[FieldType.ATTACHMENTS, FieldType.ATTACHMENT_SINGLE].includes(f.type)
|
||||
)
|
||||
|
||||
for (const entry of data[tableName]) {
|
||||
rowPromises.push(
|
||||
...data[tableName].map(async entry => {
|
||||
await processAttachments(entry, attachmentColumns)
|
||||
|
||||
const tableId = table._id!
|
||||
|
@ -62,10 +66,14 @@ export async function generateRows(
|
|||
data: overridenLinks,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
for (const data of toUpdateLinks) {
|
||||
await Promise.all(rowPromises)
|
||||
|
||||
await Promise.all(
|
||||
toUpdateLinks.map(async data => {
|
||||
const persistedRow = await sdk.rows.find(data.tableId, data.rowId)
|
||||
|
||||
const updatedLinks = Object.keys(data.data).reduce<Record<string, any>>(
|
||||
|
@ -87,9 +95,11 @@ export async function generateRows(
|
|||
...persistedRow,
|
||||
...updatedLinks,
|
||||
},
|
||||
userId
|
||||
userId,
|
||||
{ updateAIColumns: false }
|
||||
)
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
async function processAttachments(
|
||||
entry: Record<string, any>,
|
||||
|
|
|
@ -15,7 +15,8 @@ import { helpers } from "@budibase/shared-core"
|
|||
export async function save(
|
||||
tableOrViewId: string,
|
||||
inputs: Row,
|
||||
userId: string | undefined
|
||||
userId: string | undefined,
|
||||
opts?: { updateAIColumns: boolean }
|
||||
) {
|
||||
const { tableId, viewId } = tryExtractingTableAndViewId(tableOrViewId)
|
||||
inputs.tableId = tableId
|
||||
|
@ -57,7 +58,10 @@ export async function save(
|
|||
table,
|
||||
})) as Row
|
||||
|
||||
return finaliseRow(source, row, { updateFormula: true })
|
||||
return finaliseRow(source, row, {
|
||||
updateFormula: true,
|
||||
updateAIColumns: opts?.updateAIColumns || true,
|
||||
})
|
||||
}
|
||||
|
||||
export async function find(sourceId: string, rowId: string): Promise<Row> {
|
||||
|
|
|
@ -35,9 +35,10 @@ function pickApi(tableOrViewId: string) {
|
|||
export async function save(
|
||||
sourceId: string,
|
||||
row: Row,
|
||||
userId: string | undefined
|
||||
userId: string | undefined,
|
||||
opts?: { updateAIColumns: boolean }
|
||||
) {
|
||||
return pickApi(sourceId).save(sourceId, row, userId)
|
||||
return pickApi(sourceId).save(sourceId, row, userId, opts)
|
||||
}
|
||||
|
||||
export async function find(sourceId: string, rowId: string) {
|
||||
|
|
|
@ -174,3 +174,13 @@ export function processSearchFilters(
|
|||
],
|
||||
}
|
||||
}
|
||||
|
||||
export function toMap<TKey extends keyof TItem, TItem extends {}>(
|
||||
key: TKey,
|
||||
list: TItem[]
|
||||
): Record<string, TItem> {
|
||||
return list.reduce<Record<string, TItem>>((result, item) => {
|
||||
result[item[key] as string] = item
|
||||
return result
|
||||
}, {})
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export interface OverrideFeatureFlagRequest {
|
||||
flags: Record<string, boolean>
|
||||
}
|
|
@ -5,6 +5,7 @@ export * from "./backup"
|
|||
export * from "./component"
|
||||
export * from "./datasource"
|
||||
export * from "./deployment"
|
||||
export * from "./features"
|
||||
export * from "./integration"
|
||||
export * from "./layout"
|
||||
export * from "./metadata"
|
||||
|
|
|
@ -7,3 +7,7 @@ export interface SessionCookie {
|
|||
sessionId: string
|
||||
userId: string
|
||||
}
|
||||
|
||||
export interface FeatureFlagCookie {
|
||||
flags: Record<string, boolean>
|
||||
}
|
||||
|
|
|
@ -4,3 +4,10 @@ export interface APIError {
|
|||
error?: any
|
||||
validationErrors?: any
|
||||
}
|
||||
|
||||
export enum ErrorCode {
|
||||
USAGE_LIMIT_EXCEEDED = "usage_limit_exceeded",
|
||||
FEATURE_DISABLED = "feature_disabled",
|
||||
INVALID_API_KEY = "invalid_api_key",
|
||||
HTTP = "http",
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export enum FeatureFlag {
|
||||
DEBUG_UI = "DEBUG_UI",
|
||||
USE_ZOD_VALIDATOR = "USE_ZOD_VALIDATOR",
|
||||
AI_JS_GENERATION = "AI_JS_GENERATION",
|
||||
AI_TABLE_GENERATION = "AI_TABLE_GENERATION",
|
||||
|
@ -14,6 +15,8 @@ export const FeatureFlagDefaults: Record<FeatureFlag, boolean> = {
|
|||
|
||||
// Account-portal
|
||||
[FeatureFlag.DIRECT_LOGIN_TO_ACCOUNT_PORTAL]: false,
|
||||
|
||||
[FeatureFlag.DEBUG_UI]: false,
|
||||
}
|
||||
|
||||
export type FeatureFlags = typeof FeatureFlagDefaults
|
||||
|
|
|
@ -32,14 +32,12 @@ import {
|
|||
OIDCConfigs,
|
||||
OIDCLogosConfig,
|
||||
PASSWORD_REPLACEMENT,
|
||||
QuotaUsageType,
|
||||
SaveConfigRequest,
|
||||
SaveConfigResponse,
|
||||
SettingsBrandingConfig,
|
||||
SettingsInnerConfig,
|
||||
SSOConfig,
|
||||
SSOConfigType,
|
||||
StaticQuotaName,
|
||||
UploadConfigFileResponse,
|
||||
UserCtx,
|
||||
} from "@budibase/types"
|
||||
|
@ -53,7 +51,6 @@ const getEventFns = async (config: Config, existing?: Config) => {
|
|||
fns.push(events.email.SMTPCreated)
|
||||
} else if (isAIConfig(config)) {
|
||||
fns.push(() => events.ai.AIConfigCreated)
|
||||
fns.push(() => pro.quotas.addCustomAIConfig())
|
||||
} else if (isGoogleConfig(config)) {
|
||||
fns.push(() => events.auth.SSOCreated(ConfigType.GOOGLE))
|
||||
if (config.config.activated) {
|
||||
|
@ -92,12 +89,6 @@ const getEventFns = async (config: Config, existing?: Config) => {
|
|||
fns.push(events.email.SMTPUpdated)
|
||||
} else if (isAIConfig(config)) {
|
||||
fns.push(() => events.ai.AIConfigUpdated)
|
||||
if (
|
||||
Object.keys(existing.config).length > Object.keys(config.config).length
|
||||
) {
|
||||
fns.push(() => pro.quotas.removeCustomAIConfig())
|
||||
}
|
||||
fns.push(() => pro.quotas.addCustomAIConfig())
|
||||
} else if (isGoogleConfig(config)) {
|
||||
fns.push(() => events.auth.SSOUpdated(ConfigType.GOOGLE))
|
||||
if (!existing.config.activated && config.config.activated) {
|
||||
|
@ -576,13 +567,6 @@ export async function destroy(ctx: UserCtx<void, DeleteConfigResponse>) {
|
|||
try {
|
||||
await db.remove(id, rev)
|
||||
await cache.destroy(cache.CacheKey.CHECKLIST)
|
||||
if (id === configs.generateConfigID(ConfigType.AI)) {
|
||||
await pro.quotas.set(
|
||||
StaticQuotaName.AI_CUSTOM_CONFIGS,
|
||||
QuotaUsageType.STATIC,
|
||||
0
|
||||
)
|
||||
}
|
||||
ctx.body = { message: "Config deleted successfully" }
|
||||
} catch (err: any) {
|
||||
ctx.throw(err.status, err)
|
||||
|
|
|
@ -16,6 +16,7 @@ import {
|
|||
DeleteInviteUsersRequest,
|
||||
DeleteInviteUsersResponse,
|
||||
DeleteUserResponse,
|
||||
ErrorCode,
|
||||
FetchUsersResponse,
|
||||
FindUserResponse,
|
||||
GetUserInvitesResponse,
|
||||
|
@ -42,7 +43,6 @@ import {
|
|||
import {
|
||||
users,
|
||||
cache,
|
||||
ErrorCode,
|
||||
events,
|
||||
platform,
|
||||
tenancy,
|
||||
|
|
|
@ -130,6 +130,7 @@ const router: Router = new Router()
|
|||
|
||||
router
|
||||
.use(middleware.errorHandling)
|
||||
.use(middleware.featureFlagCookie)
|
||||
.use(
|
||||
compress({
|
||||
threshold: 2048,
|
||||
|
|
Loading…
Reference in New Issue