mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-10-26 08:02:31 +00:00
Merge branch 'glm/replace_menuV2' into glm/use_melt_menu
# Conflicts: # frontend/src/lib/components/apps/components/display/AppMenu.svelte # frontend/src/lib/components/flows/map/FlowJobsMenu.svelte # frontend/src/lib/components/meltComponents/Menu.svelte # frontend/src/lib/components/meltComponents/MenuItem.svelte # frontend/src/lib/components/meltComponents/index.ts
This commit is contained in:
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.456.0",
|
||||
"version": "1.458.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "windmill-components",
|
||||
"version": "1.456.0",
|
||||
"version": "1.458.4",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "windmill-components",
|
||||
"version": "1.456.0",
|
||||
"version": "1.458.4",
|
||||
"scripts": {
|
||||
"dev": "vite dev",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
import { Pen } from 'lucide-svelte'
|
||||
import GfmMarkdown from './GfmMarkdown.svelte'
|
||||
import { apiTokenApps, forceSecretValue, linkedSecretValue } from './app_connect'
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
|
||||
export let step = 1
|
||||
export let resourceType = ''
|
||||
@@ -55,18 +56,20 @@
|
||||
let args: any = {}
|
||||
let renderDescription = true
|
||||
|
||||
function computeCandidates(resourceType: string, argsKeys: string[]) {
|
||||
function computeCandidates(resourceType: string, argsKeys: string[], passwords: string[]) {
|
||||
return apiTokenApps[resourceType]?.linkedSecret
|
||||
? ([apiTokenApps[resourceType]?.linkedSecret] as string[])
|
||||
: argsKeys.filter((x) =>
|
||||
['token', 'secret', 'key', 'pass', 'private'].some((y) => x.toLowerCase().includes(y))
|
||||
: argsKeys.filter(
|
||||
(x) =>
|
||||
passwords.includes(x) ||
|
||||
['token', 'secret', 'key', 'pass', 'private'].some((y) => x.toLowerCase().includes(y))
|
||||
)
|
||||
}
|
||||
|
||||
let linkedSecret: string | undefined = undefined
|
||||
let linkedSecretCandidates: string[] | undefined = undefined
|
||||
function computeLinkedSecret(resourceType: string, argsKeys: string[]) {
|
||||
linkedSecretCandidates = computeCandidates(resourceType, argsKeys)
|
||||
function computeLinkedSecret(resourceType: string, argsKeys: string[], passwords: string[]) {
|
||||
linkedSecretCandidates = computeCandidates(resourceType, argsKeys, passwords)
|
||||
return (
|
||||
forceSecretValue(resourceType) ??
|
||||
linkedSecretCandidates?.sort((ua, ub) => linkedSecretValue(ub) - linkedSecretValue(ua))?.[0]
|
||||
@@ -240,9 +243,13 @@
|
||||
workspace: $workspaceStore!,
|
||||
path: resourceType
|
||||
})
|
||||
const newArgsKeys = Object.keys(resourceTypeInfo?.schema?.['properties'] ?? {}) ?? []
|
||||
const props: Record<string, SchemaProperty> = resourceTypeInfo?.schema?.['properties'] ?? {}
|
||||
const newArgsKeys = Object.keys(props) ?? []
|
||||
const passwords = newArgsKeys.filter((x) => {
|
||||
return props?.[x]?.password
|
||||
})
|
||||
if (!linkedSecret) {
|
||||
linkedSecret = computeLinkedSecret(resourceType, newArgsKeys)
|
||||
linkedSecret = computeLinkedSecret(resourceType, newArgsKeys, passwords)
|
||||
}
|
||||
}
|
||||
export async function next() {
|
||||
|
||||
@@ -230,7 +230,11 @@
|
||||
|
||||
let oldDefaultValue = structuredClone(defaultValue)
|
||||
function handleDefaultValueChange() {
|
||||
if (deepEqual(value, oldDefaultValue)) {
|
||||
if (
|
||||
deepEqual(value, oldDefaultValue) &&
|
||||
!deepEqual(value, defaultValue) &&
|
||||
!deepEqual(defaultValue, oldDefaultValue)
|
||||
) {
|
||||
value = defaultValue
|
||||
}
|
||||
oldDefaultValue = structuredClone(defaultValue)
|
||||
@@ -489,6 +493,7 @@
|
||||
<div class="items-start">
|
||||
<Multiselect
|
||||
ulOptionsClass={'p-2 !bg-surface-secondary'}
|
||||
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
|
||||
{disabled}
|
||||
bind:selected={value}
|
||||
options={itemsType?.multiselect ?? []}
|
||||
@@ -502,6 +507,7 @@
|
||||
<div class="items-start">
|
||||
<Multiselect
|
||||
ulOptionsClass={'p-2 !bg-surface-secondary'}
|
||||
outerDivClass={'dark:!border-gray-500 !border-gray-300'}
|
||||
{disabled}
|
||||
bind:selected={value}
|
||||
options={itemsType?.enum ?? []}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import type { SchemaProperty } from '$lib/common'
|
||||
|
||||
export let canEditResourceType: boolean = false
|
||||
export let originalType: string | undefined = undefined
|
||||
export let itemsType:
|
||||
| {
|
||||
type?: 'string' | 'number' | 'bytes' | 'object' | 'resource'
|
||||
@@ -42,7 +43,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if canEditResourceType}
|
||||
{#if canEditResourceType || originalType == 'string[]' || originalType == 'object[]'}
|
||||
<Label label="Items type">
|
||||
<select
|
||||
bind:value={selected}
|
||||
@@ -67,10 +68,12 @@
|
||||
>
|
||||
<option value="string"> Items are strings</option>
|
||||
<option value="enum">Items are strings from an enum</option>
|
||||
<option value="object"> Items are objects (JSON)</option>
|
||||
<option value="resource"> Items are resources</option>
|
||||
<option value="number">Items are numbers</option>
|
||||
<option value="bytes">Items are bytes</option>
|
||||
{#if originalType != 'string[]'}
|
||||
<option value="object"> Items are objects (JSON)</option>
|
||||
<option value="resource"> Items are resources</option>
|
||||
<option value="number">Items are numbers</option>
|
||||
<option value="bytes">Items are bytes</option>
|
||||
{/if}
|
||||
</select>
|
||||
</Label>
|
||||
{:else if itemsType?.resourceType}
|
||||
@@ -121,31 +124,33 @@
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="flex flex-row mb-1 mt-2">
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
if (itemsType?.enum) {
|
||||
let enum_ = itemsType.enum
|
||||
let choice = `choice ${enum_?.length ? enum_?.length + 1 : 1}`
|
||||
itemsType.enum = itemsType.enum ? itemsType.enum.concat(choice) : [choice]
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="sm"
|
||||
btnClasses="ml-2"
|
||||
on:click={() => itemsType?.enum && (itemsType.enum = undefined)}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
{#if canEditResourceType || originalType == 'string[]' || originalType == 'object[]'}
|
||||
<div class="flex flex-row mb-1 mt-2">
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
if (itemsType?.enum) {
|
||||
let enum_ = itemsType.enum
|
||||
let choice = `choice ${enum_?.length ? enum_?.length + 1 : 1}`
|
||||
itemsType.enum = itemsType.enum ? itemsType.enum.concat(choice) : [choice]
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
<Button
|
||||
color="light"
|
||||
variant="border"
|
||||
size="sm"
|
||||
btnClasses="ml-2"
|
||||
on:click={() => itemsType?.enum && (itemsType.enum = undefined)}
|
||||
>
|
||||
Clear
|
||||
</Button>
|
||||
</div>
|
||||
{/if}
|
||||
</label>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
<script lang="ts">
|
||||
import CollapseLink from './CollapseLink.svelte'
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
|
||||
export let value: any
|
||||
|
||||
$: enabled = value != undefined
|
||||
|
||||
$: changeDomain(value?.['domain'], value?.['custom'])
|
||||
|
||||
function changeDomain(domain, custom) {
|
||||
if (value) {
|
||||
let baseUrl = custom ? `https://${domain}` : `https://${domain}.auth0.com`
|
||||
value = {
|
||||
...value,
|
||||
login_config: {
|
||||
auth_url: `${baseUrl}/authorize`,
|
||||
token_url: `${baseUrl}/oauth/token`,
|
||||
userinfo_url: `${baseUrl}/userinfo`,
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
},
|
||||
connect_config: {
|
||||
auth_url: `${baseUrl}/authorize`,
|
||||
token_url: `${baseUrl}/oauth/token`,
|
||||
scopes: ['openid', 'profile', 'email']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col gap-1">
|
||||
<!-- svelte-ignore a11y-label-has-associated-control -->
|
||||
<label class="text-sm font-medium text-primary flex gap-4 items-center"
|
||||
><div class="w-[120px]"><IconedResourceType name="auth0" after={true} /></div><Toggle
|
||||
checked={enabled}
|
||||
on:change={(e) => {
|
||||
if (e.detail) {
|
||||
value = { id: '', secret: '', domain: '', custom: false }
|
||||
} else {
|
||||
value = undefined
|
||||
}
|
||||
}}
|
||||
/></label
|
||||
>
|
||||
{#if enabled}
|
||||
<div class="p-2 rounded border">
|
||||
<label class="block pb-2">
|
||||
<div class="flex gap-2 items-end">
|
||||
<div>
|
||||
<ToggleButtonGroup bind:selected={value['custom']}>
|
||||
<ToggleButton value={false} label={'Org'} />
|
||||
<ToggleButton value={true} label="Custom" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<div class="grow">
|
||||
<span class="text-primary font-semibold text-sm"
|
||||
>{#if value['custom']}Custom ({'https://<domain>'}){:else}
|
||||
Org ({'https://<your org>.auth0.com'}){/if}</span
|
||||
>
|
||||
<input type="text" placeholder="yourorg" bind:value={value['domain']} />
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm">Custom Name</span>
|
||||
<input type="text" placeholder="Custom Name" bind:value={value['display_name']} />
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm"
|
||||
>Client ID <Tooltip>Client ID credential of the auth0 service configuration</Tooltip
|
||||
></span
|
||||
>
|
||||
<input type="text" placeholder="Client Id" bind:value={value['id']} />
|
||||
</label>
|
||||
<label class="block pb-2">
|
||||
<span class="text-primary font-semibold text-sm"
|
||||
>Client Secret <Tooltip>Client Secret of the auth0 service configuration</Tooltip></span
|
||||
>
|
||||
<input type="text" placeholder="Client Secret" bind:value={value['secret']} />
|
||||
</label>
|
||||
<CollapseLink text="Instructions">
|
||||
<div class="text-sm text-secondary border p-2">
|
||||
From your Admin page, setup a Windmill application<br />
|
||||
Create a new application<br />
|
||||
For "application type" select "Regular Web Application"<br />
|
||||
Copy down the "Client ID" and "Client Secret" and paste them into the fields above <br />
|
||||
Under "Application URIs", set the following:<br />
|
||||
a. Application Login URI: `BASE_URL/user/login`<br />
|
||||
b. Allowed Callback URLs: `BASE_URL/user/login_callback/auth0`<br />
|
||||
c. Allowed Logout URLs: `BASE_URL/auth/logout`<br />
|
||||
d. Allowed Web Origins: `BASE_URL`<br />
|
||||
e. Allowed Origins (CORS): `BASE_URL`<br />
|
||||
</div>
|
||||
</CollapseLink>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
import OAuthSetting from '$lib/components/OAuthSetting.svelte'
|
||||
import OktaSetting from './OktaSetting.svelte'
|
||||
import Auth0Setting from './Auth0Setting.svelte'
|
||||
import CloseButton from './common/CloseButton.svelte'
|
||||
import KeycloakSetting from './KeycloakSetting.svelte'
|
||||
import CustomSso from './CustomSso.svelte'
|
||||
@@ -81,6 +82,7 @@
|
||||
<OAuthSetting name="google" bind:value={oauths['google']} />
|
||||
<OAuthSetting name="microsoft" bind:value={oauths['microsoft']} />
|
||||
<OktaSetting bind:value={oauths['okta']} />
|
||||
<Auth0Setting bind:value={oauths['auth0']} />
|
||||
<OAuthSetting name="github" bind:value={oauths['github']} />
|
||||
<OAuthSetting name="gitlab" bind:value={oauths['gitlab']} />
|
||||
<OAuthSetting name="jumpcloud" bind:value={oauths['jumpcloud']} />
|
||||
@@ -90,7 +92,7 @@
|
||||
<KanidmSetting bind:value={oauths['kanidm']} />
|
||||
<ZitadelSetting bind:value={oauths['zitadel']} />
|
||||
{#each Object.keys(oauths) as k}
|
||||
{#if !['authelia', 'authentik', 'google', 'microsoft', 'github', 'gitlab', 'jumpcloud', 'okta', 'keycloak', 'slack', 'kanidm', 'zitadel'].includes(k) && 'login_config' in oauths[k]}
|
||||
{#if !['authelia', 'authentik', 'google', 'microsoft', 'github', 'gitlab', 'jumpcloud', 'okta', 'auth0', 'keycloak', 'slack', 'kanidm', 'zitadel'].includes(k) && 'login_config' in oauths[k]}
|
||||
{#if oauths[k]}
|
||||
<div class="flex flex-col gap-2 pb-4">
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
} from '$lib/relative_imports'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import type { ScheduleTrigger, TriggerContext } from './triggers'
|
||||
import { initAllAiWorkspace } from './copilot/lib'
|
||||
import { workspaceAIClients } from './copilot/lib'
|
||||
import type { FlowPropPickerConfig, PropPickerContext } from './prop_picker'
|
||||
import type { PickableProperties } from './flows/previousResults'
|
||||
$: token = $page.url.searchParams.get('wm_token') ?? undefined
|
||||
@@ -110,14 +110,19 @@
|
||||
|
||||
async function setCopilotInfo() {
|
||||
if (workspace) {
|
||||
initAllAiWorkspace(workspace)
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
copilotInfo.set(await WorkspaceService.getCopilotInfo({ workspace }))
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
copilotInfo.set({
|
||||
...info,
|
||||
ai_provider: info.ai_provider ?? 'openai'
|
||||
})
|
||||
} catch (err) {
|
||||
copilotInfo.set({
|
||||
ai_provider: '',
|
||||
ai_provider: 'openai',
|
||||
exists_ai_resource: false,
|
||||
code_completion_enabled: false
|
||||
code_completion_model: undefined,
|
||||
ai_models: []
|
||||
})
|
||||
|
||||
console.error('Could not get copilot info')
|
||||
|
||||
@@ -677,16 +677,22 @@
|
||||
<img
|
||||
alt="preview rendered"
|
||||
class="w-auto h-full"
|
||||
src={`/api/w/${workspaceId}/${appPath ? 'apps_u/load_image_preview/' + appPath : 'job_helpers/load_image_preview'}?file_key=${result.s3}` +
|
||||
(result.storage ? `&storage=${result.storage}` : '')}
|
||||
src={`/api/w/${workspaceId}/${
|
||||
appPath
|
||||
? 'apps_u/load_image_preview/' + appPath
|
||||
: 'job_helpers/load_image_preview'
|
||||
}?file_key=${result.s3}` + (result.storage ? `&storage=${result.storage}` : '')}
|
||||
/>
|
||||
</div>
|
||||
{:else if result?.s3?.endsWith('.pdf')}
|
||||
<div class="h-96 mt-2 border">
|
||||
<PdfViewer
|
||||
allowFullscreen
|
||||
source={`/api/w/${workspaceId}/${appPath ? 'apps_u/load_image_preview/' + appPath : 'job_helpers/load_image_preview'}?file_key=${result.s3}` +
|
||||
(result.storage ? `&storage=${result.storage}` : '')}
|
||||
source={`/api/w/${workspaceId}/${
|
||||
appPath
|
||||
? 'apps_u/load_image_preview/' + appPath
|
||||
: 'job_helpers/load_image_preview'
|
||||
}?file_key=${result.s3}` + (result.storage ? `&storage=${result.storage}` : '')}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
export let jsonEnabled: boolean = true
|
||||
export let isAppInput: boolean = false
|
||||
export let displayWebhookWarning: boolean = false
|
||||
export let onlyMaskPassword: boolean = false
|
||||
export let dndType: string | undefined = undefined
|
||||
export let editTab:
|
||||
| 'inputEditor'
|
||||
@@ -282,6 +283,7 @@
|
||||
schema={previewSchema ? previewSchema : schema}
|
||||
{dndType}
|
||||
{disableDnd}
|
||||
{onlyMaskPassword}
|
||||
bind:args
|
||||
on:click={(e) => {
|
||||
opened = e.detail
|
||||
|
||||
@@ -172,7 +172,6 @@
|
||||
import { initVim } from './monaco_keybindings'
|
||||
import { buildWorkerDefinition } from '$lib/monaco_workers/build_workers'
|
||||
import { parseTypescriptDeps } from '$lib/relative_imports'
|
||||
import type { AiProviderTypes } from './copilot/lib'
|
||||
|
||||
// import EditorTheme from './EditorTheme.svelte'
|
||||
|
||||
@@ -640,7 +639,7 @@
|
||||
textAfterPosition,
|
||||
lang,
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
aiProvider
|
||||
)
|
||||
if (insertText) {
|
||||
items = [
|
||||
@@ -668,7 +667,7 @@
|
||||
}
|
||||
|
||||
$: $copilotInfo.exists_ai_resource &&
|
||||
$copilotInfo.code_completion_enabled &&
|
||||
$copilotInfo.code_completion_model &&
|
||||
$codeCompletionSessionEnabled &&
|
||||
initialized &&
|
||||
addCopilotSuggestions()
|
||||
|
||||
@@ -86,7 +86,6 @@
|
||||
import type { FlowBuilderWhitelabelCustomUi } from './custom_ui'
|
||||
import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte'
|
||||
import { type TriggerContext, type ScheduleTrigger } from './triggers'
|
||||
import type { AiProviderTypes } from './copilot/lib'
|
||||
|
||||
export let initialPath: string = ''
|
||||
export let pathStoreInit: string | undefined = undefined
|
||||
@@ -821,7 +820,7 @@
|
||||
try {
|
||||
push(history, $flowStore)
|
||||
let module = stepOnly ? $copilotModulesStore[0] : $copilotModulesStore[idx]
|
||||
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
|
||||
copilotLoading = true
|
||||
copilotStatus = "Generating code for step '" + module.id + "'..."
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
})
|
||||
if (jobs.length > 0) {
|
||||
dispatch('select', { jobId: jobs[0].id, initial: true })
|
||||
} else {
|
||||
dispatch('nohistory')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -169,6 +169,34 @@
|
||||
}
|
||||
|
||||
$: selectedJobStep !== undefined && onSelectedJobStepChange()
|
||||
|
||||
async function loadIndividualStepsStates() {
|
||||
dfs($flowStore.value.modules, async (module) => {
|
||||
if ($flowStateStore[module.id]?.previewResult) {
|
||||
return
|
||||
}
|
||||
const previousJobId = await JobService.listJobs({
|
||||
workspace: $workspaceStore!,
|
||||
scriptPathExact: (initialPath == '' ? $pathStore : initialPath) + '/' + module.id,
|
||||
jobKinds: ['preview', 'script', 'flowpreview', 'flow'].join(','),
|
||||
page: 1,
|
||||
perPage: 1
|
||||
})
|
||||
|
||||
if (previousJobId.length > 0) {
|
||||
const getJobResult = await JobService.getCompletedJobResultMaybe({
|
||||
workspace: $workspaceStore!,
|
||||
id: previousJobId[0].id
|
||||
})
|
||||
if (getJobResult.result) {
|
||||
$flowStateStore[module.id] = {
|
||||
...($flowStateStore[module.id] ?? {}),
|
||||
previewResult: getJobResult.result
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:keydown={onKeyDown} />
|
||||
@@ -401,6 +429,9 @@
|
||||
class="absolute top-[22px] right-2 border p-1.5 hover:bg-surface-hover rounded-md center-center"
|
||||
>
|
||||
<FlowHistoryJobPicker
|
||||
on:nohistory={() => {
|
||||
loadIndividualStepsStates()
|
||||
}}
|
||||
on:select={(e) => {
|
||||
if (!currentJobId) {
|
||||
currentJobId = jobId
|
||||
@@ -439,11 +470,14 @@
|
||||
{flowStateStore}
|
||||
{jobId}
|
||||
on:done={() => {
|
||||
console.log('done')
|
||||
$executionCount = $executionCount + 1
|
||||
}}
|
||||
on:jobsLoaded={({ detail }) => {
|
||||
job = detail
|
||||
if (initial) {
|
||||
console.log('loading initial steps after initial job loaded')
|
||||
loadIndividualStepsStates()
|
||||
}
|
||||
}}
|
||||
bind:selectedJobStep
|
||||
/>
|
||||
|
||||
@@ -121,9 +121,9 @@
|
||||
/>
|
||||
{/each}
|
||||
{#if jobs?.length == 5}
|
||||
<div class="text-left text-tertiary text-xs"
|
||||
>... there may be more runs not displayed here as the limit is 5</div
|
||||
>
|
||||
<tr class="text-left text-tertiary text-xs w-full">
|
||||
<td class="w-full px-2" colspan="3">limited to 5 runs</td>
|
||||
</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
{:else}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import Google from '$lib/components/icons/brands/Google.svelte'
|
||||
import Microsoft from '$lib/components/icons/brands/Microsoft.svelte'
|
||||
import Okta from '$lib/components/icons/brands/Okta.svelte'
|
||||
import Auth0 from '$lib/components/icons/brands/Auth0.svelte'
|
||||
|
||||
import { OauthService, UserService, WorkspaceService } from '$lib/gen'
|
||||
import { usersWorkspaceStore, workspaceStore, userStore } from '$lib/stores'
|
||||
@@ -49,6 +50,11 @@
|
||||
type: 'okta',
|
||||
name: 'Okta',
|
||||
icon: Okta
|
||||
},
|
||||
{
|
||||
type: 'auth0',
|
||||
name: 'Auth0',
|
||||
icon: Auth0
|
||||
}
|
||||
] as const
|
||||
|
||||
|
||||
@@ -204,7 +204,6 @@
|
||||
</div>
|
||||
{/if}</h4
|
||||
>
|
||||
|
||||
{#if can_write && editDescription}
|
||||
<div>
|
||||
<div class="flex flex-row-reverse text-2xs text-tertiary -mt-1">GH Markdown</div>
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
export let initialFileKey: { s3: string } | undefined = undefined
|
||||
let initialFileKeyInternalCopy: { s3: string }
|
||||
export let selectedFileKey: { s3: string } | undefined = undefined
|
||||
export let folderOnly = false
|
||||
|
||||
let csvSeparatorChar: string = ','
|
||||
let csvHasHeader: boolean = true
|
||||
@@ -369,6 +370,11 @@
|
||||
let item_key = displayedFileKeys[index]
|
||||
let item = allFilesByKey[item_key]
|
||||
if (item.type === 'folder') {
|
||||
if (folderOnly) {
|
||||
selectedFileKey = {
|
||||
s3: item_key
|
||||
}
|
||||
}
|
||||
if (toggleCollapsed) {
|
||||
item.collapsed = !item.collapsed
|
||||
}
|
||||
@@ -709,7 +715,9 @@
|
||||
>
|
||||
{#if !fromWorkspaceSettings}
|
||||
<Button
|
||||
disable={selectedFileKey === undefined || emptyString(selectedFileKey.s3)}
|
||||
disabled={selectedFileKey === undefined ||
|
||||
emptyString(selectedFileKey.s3) ||
|
||||
(folderOnly && allFilesByKey[selectedFileKey.s3]?.type !== 'folder')}
|
||||
on:click={selectAndClose}>Select</Button
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -265,7 +265,10 @@
|
||||
dispatch('click', argName)
|
||||
}}
|
||||
>
|
||||
{#if typeof args == 'object' && schema?.properties[argName]}
|
||||
{#if args && typeof args == 'object' && schema?.properties[argName]}
|
||||
<!-- {argName}
|
||||
{args == undefined}
|
||||
{JSON.stringify(args?.[argName])} -->
|
||||
{#if !hidden[argName]}
|
||||
<ArgInput
|
||||
on:change={() => {
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
{runnableId}
|
||||
{runnableType}
|
||||
on:select={(e) => {
|
||||
dispatch('select', { payload: e.detail.args, type: 'history' })
|
||||
dispatch('select', { payload: e.detail?.args, type: 'history' })
|
||||
}}
|
||||
/>
|
||||
</FlowInputEditor>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { AlertTriangle, ChevronDown, ChevronRight } from 'lucide-svelte'
|
||||
import { AlertTriangle, ChevronRight } from 'lucide-svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { slide } from 'svelte/transition'
|
||||
|
||||
export let label: string | undefined = undefined
|
||||
export let tooltip: string | undefined = undefined
|
||||
@@ -14,6 +15,7 @@
|
||||
export let collapsable: boolean = false
|
||||
export let collapsed: boolean = true
|
||||
export let headless: boolean = false
|
||||
export let animate: boolean = false
|
||||
</script>
|
||||
|
||||
<div class={twMerge('w-full flex flex-col', wrapperClass)}>
|
||||
@@ -27,11 +29,14 @@
|
||||
>
|
||||
{#if collapsable}
|
||||
<button class="flex items-center gap-1" on:click={() => (collapsed = !collapsed)}>
|
||||
{#if collapsed}
|
||||
<ChevronRight size={16} />
|
||||
{:else}
|
||||
<ChevronDown size={16} />
|
||||
{/if}
|
||||
<ChevronRight
|
||||
size={16}
|
||||
class={twMerge(
|
||||
'transition',
|
||||
collapsed ? '' : 'rotate-90',
|
||||
animate ? 'duration-200' : 'duration-0'
|
||||
)}
|
||||
/>
|
||||
{label}
|
||||
</button>
|
||||
{:else}
|
||||
@@ -58,7 +63,10 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if !collapsable || !collapsed}
|
||||
<div class={twMerge('grow min-h-0', $$props.class)}>
|
||||
<div
|
||||
class={twMerge('grow min-h-0', $$props.class)}
|
||||
transition:slide={animate ? { duration: 200 } : { duration: 0 }}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -120,6 +120,8 @@
|
||||
}
|
||||
if (e.detail == 'date-time') {
|
||||
format = 'date-time'
|
||||
} else if (format) {
|
||||
format = undefined
|
||||
}
|
||||
if (e.detail == 'none') {
|
||||
pattern = undefined
|
||||
|
||||
@@ -76,12 +76,29 @@
|
||||
$: resolvedConfig.beforeIcon && beforeIconComponent && handleBeforeIcon()
|
||||
$: resolvedConfig.afterIcon && afterIconComponent && handleAfterIcon()
|
||||
|
||||
function getIconSize() {
|
||||
switch (resolvedConfig.size as 'xs' | 'sm' | 'md' | 'lg' | 'xl') {
|
||||
case 'xs':
|
||||
return 14
|
||||
case 'sm':
|
||||
return 16
|
||||
case 'md':
|
||||
return 20
|
||||
case 'lg':
|
||||
return 24
|
||||
case 'xl':
|
||||
return 26
|
||||
default:
|
||||
return 24
|
||||
}
|
||||
}
|
||||
|
||||
async function handleBeforeIcon() {
|
||||
if (resolvedConfig.beforeIcon) {
|
||||
beforeIconComponent = await loadIcon(
|
||||
resolvedConfig.beforeIcon,
|
||||
beforeIconComponent,
|
||||
14,
|
||||
getIconSize(),
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
@@ -93,7 +110,7 @@
|
||||
afterIconComponent = await loadIcon(
|
||||
resolvedConfig.afterIcon,
|
||||
afterIconComponent,
|
||||
14,
|
||||
getIconSize(),
|
||||
undefined,
|
||||
undefined
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
)}
|
||||
>
|
||||
<span class="mr-2 w-8 font-mono">{activeIndex === index ? '-' : '+'}</span>
|
||||
{accordionInput?.value[index]?.header || `Header ${index}`}
|
||||
{result[index]?.header || `Header ${index}`}
|
||||
</button>
|
||||
{#if activeIndex === index}
|
||||
<div class="p-2 overflow-auto w-full">
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import { components } from '../../editor/component'
|
||||
import ResolveConfig from '../helpers/ResolveConfig.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
|
||||
export let id: string
|
||||
export let componentInput: AppInput | undefined
|
||||
@@ -96,7 +97,7 @@
|
||||
{result}
|
||||
{requireHtmlApproval}
|
||||
disableExpand={resolvedConfig?.hideDetails}
|
||||
appPath={$appPath}
|
||||
appPath={$userStore ? undefined : $appPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+3
-1
@@ -16,6 +16,7 @@
|
||||
import ResolveStyle from '../helpers/ResolveStyle.svelte'
|
||||
import InitializeComponent from '../helpers/InitializeComponent.svelte'
|
||||
import DisplayResult from '$lib/components/DisplayResult.svelte'
|
||||
import { userStore } from '$lib/stores'
|
||||
|
||||
export let id: string
|
||||
export let initializing: boolean | undefined = false
|
||||
@@ -23,7 +24,7 @@
|
||||
export let configuration: RichConfigurations
|
||||
export let render: boolean
|
||||
|
||||
const { app, worldStore, workspace } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const { app, worldStore, workspace, appPath } = getContext<AppViewerContext>('AppViewerContext')
|
||||
const requireHtmlApproval = getContext<boolean | undefined>(IS_APP_PUBLIC_CONTEXT_KEY)
|
||||
|
||||
let resolvedConfig = initConfig(
|
||||
@@ -115,6 +116,7 @@
|
||||
{result}
|
||||
{requireHtmlApproval}
|
||||
disableExpand={resolvedConfig?.hideDetails}
|
||||
appPath={$userStore ? undefined : $appPath}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -105,6 +105,7 @@
|
||||
on:close
|
||||
on:open
|
||||
class={resolvedConfig.fillContainer ? 'w-full h-full' : ''}
|
||||
usePointerDownOutside={true}
|
||||
>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<div use:melt={trigger} class="w-full h-full">
|
||||
@@ -145,7 +146,7 @@
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
|
||||
<div class="flex flex-col w-full p-1 gap-2">
|
||||
<div class="flex flex-col w-full p-1 gap-2 max-h-[50vh] overflow-y-auto">
|
||||
{#if menuItems.length > 0}
|
||||
{#each menuItems as actionButton, actionIndex (actionButton?.id)}
|
||||
{#if actionButton.type == 'buttoncomponent'}
|
||||
|
||||
@@ -248,7 +248,7 @@ declare function setValue(id: string, value: any): void;
|
||||
*/
|
||||
declare function setSelectedIndex(id: string, index: number): void;
|
||||
|
||||
/** close a drawer or modal
|
||||
/** open a drawer or modal
|
||||
* @param id component's id
|
||||
*/
|
||||
declare function open(id: string): void;
|
||||
|
||||
@@ -20,30 +20,24 @@
|
||||
export let submittedText: string | undefined = undefined
|
||||
export let defaultFile: string | undefined = undefined
|
||||
export let disabled: boolean | undefined = undefined
|
||||
export let folderOnly = false
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
let input: HTMLInputElement
|
||||
export let files: File[] | undefined = undefined
|
||||
type FileWithPath = File & { path?: string }
|
||||
export let files: FileWithPath[] | undefined = undefined
|
||||
|
||||
async function onChange(fileList: FileList | null) {
|
||||
async function onChange(fileList: FileWithPath[] | null) {
|
||||
if (!fileList || !fileList.length) {
|
||||
files = undefined
|
||||
dispatch('change', files)
|
||||
return
|
||||
}
|
||||
|
||||
if (!multiple || !files) {
|
||||
files = []
|
||||
}
|
||||
for (let i = 0; i < fileList.length; i++) {
|
||||
const file = fileList.item(i)
|
||||
if (file) {
|
||||
files.push(file)
|
||||
}
|
||||
}
|
||||
|
||||
if (!files.length) {
|
||||
files = undefined
|
||||
if (multiple && files) {
|
||||
files = [...files, ...fileList]
|
||||
} else {
|
||||
files = fileList
|
||||
}
|
||||
|
||||
// Needs to be reset so the same file can be selected
|
||||
@@ -86,15 +80,70 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleDrop(event: DragEvent) {
|
||||
async function handleFile(fileEntry: FileSystemFileEntry): Promise<FileWithPath> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fileEntry.file(resolve, reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function handleDirectory(
|
||||
dirEntry: FileSystemDirectoryEntry,
|
||||
path: string
|
||||
): Promise<FileWithPath[]> {
|
||||
const files: FileWithPath[] = []
|
||||
const dirReader = dirEntry.createReader()
|
||||
|
||||
async function readEntries() {
|
||||
return new Promise<FileWithPath[]>((resolve) => {
|
||||
dirReader.readEntries(async (entries) => {
|
||||
if (entries.length === 0) {
|
||||
resolve(files)
|
||||
return
|
||||
}
|
||||
|
||||
const filePromises = entries.map(async (entry) => {
|
||||
return traverseFileTree(entry, path + dirEntry.name + '/')
|
||||
})
|
||||
const nestedFiles = await Promise.all(filePromises)
|
||||
files.push(...nestedFiles.flat())
|
||||
// readEntries only return up to 100 files
|
||||
// continue reading if more files exist
|
||||
resolve(await readEntries())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return readEntries()
|
||||
}
|
||||
|
||||
async function traverseFileTree(entry: FileSystemEntry, path = ''): Promise<FileWithPath[]> {
|
||||
if (entry.isFile) {
|
||||
const file = await handleFile(entry as FileSystemFileEntry)
|
||||
file.path = path + file.name
|
||||
return [file]
|
||||
} else if (entry.isDirectory) {
|
||||
return handleDirectory(entry as FileSystemDirectoryEntry, path)
|
||||
}
|
||||
return []
|
||||
}
|
||||
|
||||
async function handleDrop(event: DragEvent) {
|
||||
event.preventDefault()
|
||||
if (event.dataTransfer) {
|
||||
if (event.dataTransfer.files && event.dataTransfer.files.length) {
|
||||
if (!multiple && event.dataTransfer.files.length > 1) {
|
||||
sendUserToast('Only one file can be uploaded at a time')
|
||||
return
|
||||
} else {
|
||||
onChange(event.dataTransfer.files)
|
||||
if (folderOnly) {
|
||||
const item = event.dataTransfer.items[0]?.webkitGetAsEntry()
|
||||
if (item) {
|
||||
const files = await traverseFileTree(item, '')
|
||||
onChange(files)
|
||||
}
|
||||
} else {
|
||||
if (event.dataTransfer.files && event.dataTransfer.files.length) {
|
||||
if (!multiple && event.dataTransfer.files.length > 1) {
|
||||
sendUserToast('Only one file can be uploaded at a time')
|
||||
return
|
||||
} else {
|
||||
onChange(Array.from(event.dataTransfer.files))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,22 +223,22 @@
|
||||
</div>
|
||||
{:else}
|
||||
<slot>
|
||||
<span>Drag and drop {multiple ? 'files' : 'a file'}</span>
|
||||
<span>Drag and drop {folderOnly ? 'a folder' : multiple ? 'files' : 'a file'}</span>
|
||||
</slot>
|
||||
{/if}
|
||||
<input
|
||||
class="!absolute !inset-0 !z-10 !opacity-0 !cursor-pointer"
|
||||
type="file"
|
||||
{...{ webkitdirectory: folderOnly }}
|
||||
title={files ? `${files.length} file${files.length > 1 ? 's' : ''} chosen` : 'No file chosen'}
|
||||
bind:this={input}
|
||||
on:change={({ currentTarget }) => {
|
||||
onChange(currentTarget.files)
|
||||
onChange(currentTarget.files ? Array.from(currentTarget.files) : null)
|
||||
}}
|
||||
{accept}
|
||||
{multiple}
|
||||
{...$$restProps}
|
||||
/>
|
||||
|
||||
{#if defaultFile}
|
||||
<div class="w-full border-dashed border-t-2 text-2xs pt-1 text-tertiary mt-2">
|
||||
Default file: <span class="text-blue-500">{defaultFile}</span>
|
||||
|
||||
@@ -14,7 +14,10 @@
|
||||
|
||||
export let acceptedFileTypes: string[] | undefined = ['*']
|
||||
export let allowMultiple: boolean = true
|
||||
export let containerText: string = allowMultiple
|
||||
export let folderOnly = false
|
||||
export let containerText: string = folderOnly
|
||||
? 'Drag and drop a folder here or click to browse'
|
||||
: allowMultiple
|
||||
? 'Drag and drop files here or click to browse'
|
||||
: 'Drag and drop a file here or click to browse'
|
||||
export let customResourcePath: string | undefined = undefined
|
||||
@@ -53,20 +56,61 @@
|
||||
}
|
||||
|
||||
async function handleChange(files: File[] | undefined) {
|
||||
for (const file of files ?? []) {
|
||||
uploadFileToS3(file, file.name)
|
||||
if (folderOnly) {
|
||||
uniqueFolderPrefix = getRandomFolderPrefix()
|
||||
uploadedFolderRoot = undefined
|
||||
await Promise.all(
|
||||
files?.map(async (file) => {
|
||||
await uploadFileToS3(file, file.name)
|
||||
}) ?? []
|
||||
)
|
||||
if (uploadedFolderRoot) {
|
||||
dispatch('addition', {
|
||||
path: uniqueFolderPrefix + uploadedFolderRoot,
|
||||
filename: undefined
|
||||
})
|
||||
sendUserToast('Folder uploaded!')
|
||||
}
|
||||
} else {
|
||||
for (const file of files ?? []) {
|
||||
uploadFileToS3(file, file.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let activeUploads: { xhr: XMLHttpRequest; fileName: string }[] = []
|
||||
|
||||
async function uploadFileToS3(fileToUpload: File, fileToUploadKey: string) {
|
||||
function getRandomFolderPrefix(): string {
|
||||
const now = Date.now()
|
||||
const randomNum = Math.floor(Math.random() * 65536)
|
||||
return `windmill_uploads/upload_${now}_${randomNum}/`
|
||||
}
|
||||
|
||||
let uniqueFolderPrefix = getRandomFolderPrefix()
|
||||
let uploadedFolderRoot: string | undefined = undefined
|
||||
|
||||
async function uploadFileToS3(fileToUpload: File & { path?: string }, fileToUploadKey: string) {
|
||||
if (fileToUpload === undefined || fileToUploadKey === undefined) {
|
||||
return
|
||||
}
|
||||
let path: string | undefined = undefined
|
||||
let fileExtension: string | undefined = undefined
|
||||
if (randomFileKey) {
|
||||
if (folderOnly) {
|
||||
const relativePath = fileToUpload.webkitRelativePath || fileToUpload.path
|
||||
if (!relativePath) {
|
||||
throw new Error('Missing file relative path')
|
||||
}
|
||||
const rootFolder = relativePath.split('/')[0]
|
||||
if (!rootFolder) {
|
||||
throw new Error('Missing root folder')
|
||||
}
|
||||
if (uploadedFolderRoot && rootFolder !== uploadedFolderRoot) {
|
||||
throw new Error('Uploading a file in a different root folder than the previous files')
|
||||
} else {
|
||||
uploadedFolderRoot = rootFolder
|
||||
}
|
||||
path = uniqueFolderPrefix + relativePath
|
||||
} else if (randomFileKey) {
|
||||
fileExtension = fileToUpload.name.split('.').pop()
|
||||
if (emptyString(fileExtension)) {
|
||||
fileExtension = undefined
|
||||
@@ -219,8 +263,10 @@
|
||||
})
|
||||
return
|
||||
}
|
||||
dispatch('addition', { path: uploadData.path, filename: fileToUpload.name })
|
||||
sendUserToast('File upload finished!')
|
||||
if (!folderOnly) {
|
||||
dispatch('addition', { path: uploadData.path, filename: fileToUpload.name })
|
||||
sendUserToast('File uploaded!')
|
||||
}
|
||||
|
||||
uploadData.progress = 100
|
||||
$fileUploads = $fileUploads.map((fileUpload) => {
|
||||
@@ -442,6 +488,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<FileInput
|
||||
{folderOnly}
|
||||
{disabled}
|
||||
accept={acceptedFileTypes?.join(',')}
|
||||
multiple={allowMultiple}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $copilotInfo.exists_ai_resource && $copilotInfo.code_completion_enabled}
|
||||
{#if $copilotInfo.exists_ai_resource && $copilotInfo.code_completion_model}
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={$codeCompletionSessionEnabled}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { ExternalLink, Wand2 } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
|
||||
import { base } from '$lib/base'
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/index.mjs'
|
||||
import type { AIProvider } from '$lib/gen'
|
||||
|
||||
export let schedule: string
|
||||
export let cronVersion: string
|
||||
@@ -56,7 +57,7 @@
|
||||
const response = await getNonStreamingCompletion(
|
||||
messages,
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
aiProvider as AIProvider
|
||||
)
|
||||
|
||||
if (response.startsWith('ERROR:')) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Check, Loader2, Wand2 } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { Flow, InputTransform } from '$lib/gen'
|
||||
import ManualPopover from '../ManualPopover.svelte'
|
||||
@@ -71,7 +71,7 @@ Only output the expression, do not explain or discuss.`
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
aiProvider
|
||||
)
|
||||
} catch (err) {
|
||||
if (!abortController.signal.aborted) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { getCompletion, getResponseFromEvent, type AiProviderTypes } from './lib'
|
||||
import { getCompletion, getResponseFromEvent } from './lib'
|
||||
import { isInitialCode } from '$lib/script_helpers'
|
||||
import { Check, Loader2, Wand2 } from 'lucide-svelte'
|
||||
import { copilotInfo, metadataCompletionEnabled } from '$lib/stores'
|
||||
@@ -118,7 +118,7 @@ Generate a description for the flow below:
|
||||
content: config.user.replace(`{${config.placeholderName}}`, placeholderContent)
|
||||
}
|
||||
]
|
||||
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
const response = await getCompletion(messages, abortController, aiProvider)
|
||||
generatedContent = ''
|
||||
for await (const chunk of response) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Wand2 } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import { createEventDispatcher, getContext } from 'svelte'
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
@@ -68,7 +68,7 @@ Only return the expression without any wrapper. Do not explain or discuss.`
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
aiProvider
|
||||
)
|
||||
|
||||
dispatch('setExpr', result)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$lib/base'
|
||||
import { Button } from '../common'
|
||||
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
import { copilotInfo } from '$lib/stores'
|
||||
@@ -40,7 +40,7 @@
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
aiProvider
|
||||
)
|
||||
dispatch('gen', { res: res, prompt: funcDesc })
|
||||
funcDesc = ''
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { base } from '$lib/base'
|
||||
import { Button } from '../common'
|
||||
|
||||
import { SUPPORTED_LANGUAGES, copilot, type AiProviderTypes } from './lib'
|
||||
import { SUPPORTED_LANGUAGES, copilot } from './lib'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type Editor from '../Editor.svelte'
|
||||
@@ -40,7 +40,7 @@
|
||||
try {
|
||||
genLoading = true
|
||||
abortController = new AbortController()
|
||||
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
await copilot(
|
||||
{
|
||||
language: lang,
|
||||
@@ -53,7 +53,7 @@
|
||||
generatedCode,
|
||||
abortController,
|
||||
aiProvider,
|
||||
generatedExplanation,
|
||||
generatedExplanation
|
||||
)
|
||||
setupDiff()
|
||||
diffEditor?.setModified($generatedCode)
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
import { base } from '$lib/base'
|
||||
import { Button } from '../common'
|
||||
|
||||
import {
|
||||
MAX_SCHEMA_LENGTH,
|
||||
SUPPORTED_LANGUAGES,
|
||||
addThousandsSeparator,
|
||||
copilot,
|
||||
type AiProviderTypes
|
||||
} from './lib'
|
||||
import { MAX_SCHEMA_LENGTH, SUPPORTED_LANGUAGES, addThousandsSeparator, copilot } from './lib'
|
||||
import type { SupportedLanguage } from '$lib/common'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type Editor from '../Editor.svelte'
|
||||
import Popup from '../common/popup/Popup.svelte'
|
||||
import { dbSchemas, copilotInfo, type DBSchema, workspaceStore } from '$lib/stores'
|
||||
import {
|
||||
dbSchemas,
|
||||
copilotInfo,
|
||||
type DBSchema,
|
||||
workspaceStore,
|
||||
copilotSessionModel
|
||||
} from '$lib/stores'
|
||||
import type DiffEditor from '../DiffEditor.svelte'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
import type SimpleEditor from '../SimpleEditor.svelte'
|
||||
@@ -25,16 +25,7 @@
|
||||
import LoadingIcon from '../apps/svelte-select/lib/LoadingIcon.svelte'
|
||||
import { sleep } from '$lib/utils'
|
||||
import { autoPlacement } from '@floating-ui/core'
|
||||
import {
|
||||
AlertTriangle,
|
||||
Ban,
|
||||
Bot,
|
||||
Check,
|
||||
ExternalLink,
|
||||
HistoryIcon,
|
||||
Wand2,
|
||||
X
|
||||
} from 'lucide-svelte'
|
||||
import { AlertTriangle, Ban, Check, ExternalLink, HistoryIcon, Wand2, X } from 'lucide-svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import { isInitialCode } from '$lib/script_helpers'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
@@ -74,7 +65,7 @@
|
||||
return
|
||||
}
|
||||
savePrompt()
|
||||
const aiProvider = $copilotInfo.ai_provider as AiProviderTypes
|
||||
const aiProvider = $copilotInfo.ai_provider
|
||||
try {
|
||||
genLoading = true
|
||||
blockPopupOpen = true
|
||||
@@ -248,6 +239,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
function checkForInvalidModel() {
|
||||
if (
|
||||
!$copilotSessionModel ||
|
||||
($copilotSessionModel && !$copilotInfo.ai_models.includes($copilotSessionModel))
|
||||
) {
|
||||
$copilotSessionModel = $copilotInfo.ai_models[0]
|
||||
}
|
||||
}
|
||||
$: $copilotInfo && checkForInvalidModel()
|
||||
|
||||
onDestroy(() => {
|
||||
abortController?.abort()
|
||||
})
|
||||
@@ -399,21 +400,27 @@
|
||||
</div>
|
||||
{:else if $copilotInfo.exists_ai_resource}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row justify-between items-center">
|
||||
<div class="flex flex-row justify-between items-center w-96 gap-2">
|
||||
<ToggleButtonGroup class="w-auto shrink-0" bind:selected={mode}>
|
||||
<ToggleButton value={'gen'} label="Generate from scratch" small light />
|
||||
<ToggleButton value={'edit'} label="Edit existing code" small light />
|
||||
</ToggleButtonGroup>
|
||||
|
||||
<div class="text-[0.6rem] text-secondary opacity-60 flex flex-row items-center gap-0.5">
|
||||
{#if $copilotInfo.ai_provider === 'openai'}
|
||||
GPT-4o
|
||||
{:else if $copilotInfo.ai_provider === 'anthropic'}
|
||||
Claude-3.5
|
||||
{:else}
|
||||
Codestral
|
||||
<div class="min-w-0">
|
||||
{#if $copilotInfo.ai_models.length > 1}
|
||||
<select
|
||||
bind:value={$copilotSessionModel}
|
||||
class="!text-xs !pr-5 !bg-[right_center] overflow-ellipsis text-right !border-none !shadow-none"
|
||||
>
|
||||
{#each $copilotInfo.ai_models as model}
|
||||
<option value={model} class="pr-4">{model}</option>
|
||||
{/each}
|
||||
</select>
|
||||
{:else if $copilotInfo.ai_models.length === 1}
|
||||
<div class="text-xs whitespace-nowrap overflow-hidden overflow-ellipsis">
|
||||
{$copilotInfo.ai_models[0]}
|
||||
</div>
|
||||
{/if}
|
||||
<Bot size={14} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-96 items-start">
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { Check, Loader2, Wand2 } from 'lucide-svelte'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import type { Flow, InputTransform } from '$lib/gen'
|
||||
import ManualPopover from '../ManualPopover.svelte'
|
||||
@@ -116,7 +116,7 @@ Only return the expression without any wrapper.`
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
aiProvider
|
||||
)
|
||||
|
||||
if (
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { FlowEditorContext } from '../flows/types'
|
||||
import type { PickableProperties } from '../flows/previousResults'
|
||||
import { getContext } from 'svelte'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import type { FlowCopilotContext } from './flow'
|
||||
@@ -92,7 +92,7 @@ input_name2: expression2
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider as AiProviderTypes
|
||||
aiProvider
|
||||
)
|
||||
|
||||
parsedInputs = generatedContent.split('\n').map((x) => x.split(': '))
|
||||
|
||||
+16
-4
@@ -1,10 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { AIProvider } from '$lib/gen'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import Button from '../common/button/Button.svelte'
|
||||
import { testKey, type AiProviderTypes } from './lib'
|
||||
import { testKey } from './lib'
|
||||
export let disabled = false
|
||||
export let apiKey: string | undefined = undefined
|
||||
export let aiProvider: AiProviderTypes
|
||||
export let resourcePath: string | undefined = undefined
|
||||
export let aiProvider: AIProvider
|
||||
export let model: string
|
||||
let loading = false
|
||||
</script>
|
||||
|
||||
@@ -21,8 +24,10 @@
|
||||
setTimeout(() => {
|
||||
abortController.abort()
|
||||
}, 10000)
|
||||
|
||||
await testKey({
|
||||
apiKey,
|
||||
resourcePath,
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
@@ -30,7 +35,8 @@
|
||||
}
|
||||
],
|
||||
abortController,
|
||||
aiProvider
|
||||
aiProvider,
|
||||
model
|
||||
})
|
||||
sendUserToast('Valid key')
|
||||
} catch (err) {
|
||||
@@ -42,5 +48,11 @@
|
||||
} finally {
|
||||
loading = false
|
||||
}
|
||||
}}>Test key</Button
|
||||
}}
|
||||
>
|
||||
{#if apiKey}
|
||||
Test key
|
||||
{:else}
|
||||
Test
|
||||
{/if}
|
||||
</Button>
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/chat/index.mjs'
|
||||
import { getNonStreamingCompletion, type AiProviderTypes } from './lib'
|
||||
import type { AIProvider } from '$lib/gen'
|
||||
import { codeCompletionLoading } from '$lib/stores'
|
||||
import type { ChatCompletionMessageParam } from 'openai/resources/chat/index.mjs'
|
||||
import { getNonStreamingCompletion } from './lib'
|
||||
|
||||
const systemPrompt = `You are a code completion assistant, return the code that should go instead of the <completion_tokens>.
|
||||
|
||||
@@ -74,7 +75,7 @@ export async function editorCodeCompletion(
|
||||
after: string,
|
||||
lang: string,
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes
|
||||
aiProvider: AIProvider
|
||||
) {
|
||||
codeCompletionLoading.set(true)
|
||||
const messages: ChatCompletionMessageParam[] = [
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
import {
|
||||
type FlowModule,
|
||||
ScriptService,
|
||||
type RawScript,
|
||||
type PathScript,
|
||||
type AIProvider,
|
||||
type FlowModule,
|
||||
type InputTransform,
|
||||
type PathScript,
|
||||
type RawScript,
|
||||
type Script
|
||||
} from '$lib/gen'
|
||||
import {
|
||||
addResourceTypes,
|
||||
deltaCodeCompletion,
|
||||
getNonStreamingCompletion,
|
||||
type AiProviderTypes
|
||||
} from './lib'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type Editor from '../Editor.svelte'
|
||||
import type { Drawer } from '../common'
|
||||
import { scriptLangToEditorLang } from '$lib/scripts'
|
||||
import { addResourceTypes, deltaCodeCompletion, getNonStreamingCompletion } from './lib'
|
||||
|
||||
export type FlowCopilotModule = {
|
||||
id: string
|
||||
@@ -259,7 +255,7 @@ export async function stepCopilot(
|
||||
| undefined,
|
||||
isFirstInLoop: boolean,
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes
|
||||
aiProvider: AIProvider
|
||||
) {
|
||||
if (module.source !== 'custom') {
|
||||
throw new Error('Not a custom module')
|
||||
@@ -328,7 +324,7 @@ export async function glueCopilot(
|
||||
},
|
||||
isFirstInLoop: boolean,
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes
|
||||
aiProvider: AIProvider
|
||||
) {
|
||||
const { prevCode, prevLang } = await getPreviousStepContent(pastModule, workspace)
|
||||
|
||||
|
||||
@@ -1,56 +1,130 @@
|
||||
import { OpenAI } from 'openai'
|
||||
import { OpenAPI, ResourceService, type Script } from '../../gen'
|
||||
import type { Writable } from 'svelte/store'
|
||||
import type { AIProvider } from '$lib/gen'
|
||||
import {
|
||||
copilotInfo,
|
||||
copilotSessionModel,
|
||||
type DBSchema,
|
||||
type GraphqlSchema,
|
||||
type SQLSchema
|
||||
} from '$lib/stores'
|
||||
import { Anthropic } from '@anthropic-ai/sdk'
|
||||
import type { DBSchema, GraphqlSchema, SQLSchema } from '$lib/stores'
|
||||
import { formatResourceTypes } from './utils'
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
import { Mistral } from '@mistralai/mistralai'
|
||||
import { buildClientSchema, printSchema } from 'graphql'
|
||||
import { OpenAI } from 'openai'
|
||||
import type {
|
||||
ChatCompletionCreateParamsStreaming,
|
||||
ChatCompletionMessageParam
|
||||
} from 'openai/resources/index.mjs'
|
||||
import { get, type Writable } from 'svelte/store'
|
||||
import { OpenAPI, ResourceService, type Script } from '../../gen'
|
||||
import { EDIT_CONFIG, FIX_CONFIG, GEN_CONFIG } from './prompts'
|
||||
import { formatResourceTypes } from './utils'
|
||||
|
||||
import type { MessageCreateParams, MessageParam } from '@anthropic-ai/sdk/resources/messages.mjs'
|
||||
import type { ChatCompletionRequest } from '@mistralai/mistralai/models/components/chatcompletionrequest'
|
||||
import type {
|
||||
SystemMessage,
|
||||
UserMessage,
|
||||
AssistantMessage,
|
||||
ToolMessage,
|
||||
CompletionEvent,
|
||||
ContentChunk
|
||||
ContentChunk,
|
||||
SystemMessage,
|
||||
ToolMessage,
|
||||
UserMessage
|
||||
} from '@mistralai/mistralai/models/components'
|
||||
import type { ChatCompletionRequest } from '@mistralai/mistralai/models/components/chatcompletionrequest'
|
||||
|
||||
export const SUPPORTED_LANGUAGES = new Set(Object.keys(GEN_CONFIG.prompts))
|
||||
|
||||
export type AiProviderTypes = 'openai' | 'anthropic' | 'mistral'
|
||||
|
||||
interface AiProvider {
|
||||
init: (workspace: string, updateClient: boolean, token?: string) => void
|
||||
export const AI_DEFAULT_MODELS: Record<AIProvider, string[]> = {
|
||||
openai: ['gpt-4o', 'gpt-4o-mini'],
|
||||
anthropic: ['claude-3-5-sonnet-latest', 'claude-3-5-haiku-latest'],
|
||||
mistral: ['codestral-latest'],
|
||||
deepseek: ['deepseek-chat', 'deepseek-reasoner'],
|
||||
groq: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'],
|
||||
openrouter: ['meta-llama/llama-3.2-3b-instruct:free'],
|
||||
customai: []
|
||||
}
|
||||
|
||||
class WorkspacedMistral implements AiProvider {
|
||||
private client: Mistral | undefined
|
||||
export const OPENAI_COMPATIBLE_BASE_URLS = {
|
||||
groq: 'https://api.groq.com/openai/v1',
|
||||
openrouter: 'https://openrouter.ai/api/v1',
|
||||
deepseek: 'https://api.deepseek.com/v1'
|
||||
} as const
|
||||
|
||||
init(workspace: string, updateClient: boolean, token?: string) {
|
||||
if (!this.client || updateClient) {
|
||||
this.client = initWorkspaceAiProvider(workspace, 'mistral', token) as unknown as Mistral
|
||||
}
|
||||
class WorkspacedAIClients {
|
||||
private openaiClient: OpenAI | undefined
|
||||
private anthropicClient: Anthropic | undefined
|
||||
private mistralClient: Mistral | undefined
|
||||
|
||||
init(workspace: string) {
|
||||
this.initOpenai(workspace)
|
||||
this.initAnthropic(workspace)
|
||||
this.initMistral(workspace)
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (!this.client) {
|
||||
throw new Error('AnthropicAi not initialized')
|
||||
private getBaseURL(workspace: string) {
|
||||
return `${location.origin}${OpenAPI.BASE}/w/${workspace}/ai/proxy`
|
||||
}
|
||||
|
||||
private initOpenai(workspace: string) {
|
||||
const baseURL = this.getBaseURL(workspace)
|
||||
this.openaiClient = new OpenAI({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
defaultHeaders: {
|
||||
Authorization: '' // a non empty string will be unable to access Windmill backend proxy
|
||||
},
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
|
||||
private initAnthropic(workspace: string) {
|
||||
const baseURL = this.getBaseURL(workspace)
|
||||
this.anthropicClient = new Anthropic({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
}
|
||||
|
||||
private initMistral(workspace: string) {
|
||||
const baseURL = this.getBaseURL(workspace)
|
||||
this.mistralClient = new Mistral({
|
||||
serverURL: baseURL
|
||||
})
|
||||
}
|
||||
|
||||
getOpenaiClient() {
|
||||
if (!this.openaiClient) {
|
||||
throw new Error('OpenAI not initialized')
|
||||
}
|
||||
return this.client
|
||||
return this.openaiClient
|
||||
}
|
||||
|
||||
getAnthropicClient() {
|
||||
if (!this.anthropicClient) {
|
||||
throw new Error('Anthropic not initialized')
|
||||
}
|
||||
return this.anthropicClient
|
||||
}
|
||||
|
||||
getMistralClient() {
|
||||
if (!this.mistralClient) {
|
||||
throw new Error('Mistral not initialized')
|
||||
}
|
||||
return this.mistralClient
|
||||
}
|
||||
}
|
||||
|
||||
export namespace MistralAi {
|
||||
export let workspace = new WorkspacedMistral()
|
||||
export const workspaceAIClients = new WorkspacedAIClients()
|
||||
|
||||
const DEFAULT_COMPLETION_CONFIG: ChatCompletionCreateParamsStreaming = {
|
||||
model: '',
|
||||
max_tokens: 8000, //TODO: make this dynamic
|
||||
temperature: 0,
|
||||
seed: 42,
|
||||
stream: true,
|
||||
messages: []
|
||||
}
|
||||
|
||||
namespace MistralAI {
|
||||
export const mistralConfig: ChatCompletionRequest = {
|
||||
temperature: 0,
|
||||
model: null,
|
||||
@@ -78,30 +152,11 @@ export namespace MistralAi {
|
||||
}
|
||||
}
|
||||
|
||||
class WorkspacedAnthropic implements AiProvider {
|
||||
private client: Anthropic | undefined
|
||||
|
||||
init(workspace: string, updateClient: boolean, token: string | undefined = undefined) {
|
||||
if (!this.client || updateClient) {
|
||||
this.client = initWorkspaceAiProvider(workspace, 'anthropic', token) as unknown as Anthropic
|
||||
}
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (!this.client) {
|
||||
throw new Error('AnthropicAi not initialized')
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
}
|
||||
|
||||
export namespace AnthropicAi {
|
||||
export let workspace = new WorkspacedAnthropic()
|
||||
|
||||
export namespace AnthropicAI {
|
||||
export const config: MessageCreateParams = {
|
||||
temperature: 0,
|
||||
max_tokens: 8192,
|
||||
model: 'claude-3-5-sonnet-20241022',
|
||||
model: '',
|
||||
messages: []
|
||||
}
|
||||
|
||||
@@ -135,30 +190,11 @@ export namespace AnthropicAi {
|
||||
}
|
||||
}
|
||||
|
||||
class WorkspacedOpenai implements AiProvider {
|
||||
private client: OpenAI | undefined
|
||||
|
||||
init(workspace: string, updateClient: boolean, token: string | undefined = undefined) {
|
||||
if (!this.client || updateClient) {
|
||||
this.client = initWorkspaceAiProvider(workspace, 'openai', token) as unknown as OpenAI
|
||||
}
|
||||
}
|
||||
|
||||
getClient() {
|
||||
if (!this.client) {
|
||||
throw new Error('OpenAI not initialized')
|
||||
}
|
||||
return this.client
|
||||
}
|
||||
}
|
||||
|
||||
export namespace OpenAi {
|
||||
export let workspace = new WorkspacedOpenai()
|
||||
|
||||
namespace OpenAi {
|
||||
export const openaiConfig: ChatCompletionCreateParamsStreaming = {
|
||||
temperature: 0,
|
||||
max_tokens: 16384,
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
model: '',
|
||||
seed: 42,
|
||||
stream: true,
|
||||
messages: []
|
||||
@@ -169,125 +205,38 @@ export namespace OpenAi {
|
||||
}
|
||||
}
|
||||
|
||||
export function initAllAiWorkspace(workspace: string, updateClient: boolean = false) {
|
||||
OpenAi.workspace.init(workspace, updateClient)
|
||||
AnthropicAi.workspace.init(workspace, updateClient)
|
||||
MistralAi.workspace.init(workspace, updateClient)
|
||||
}
|
||||
|
||||
function initWorkspaceAiProvider(
|
||||
workspace: string,
|
||||
aiProvider: AiProviderTypes,
|
||||
token: string | undefined = undefined
|
||||
): Anthropic | OpenAI | Mistral {
|
||||
const baseURL = `${location.origin}${OpenAPI.BASE}/w/${workspace}/ai/proxy`
|
||||
let client
|
||||
switch (aiProvider) {
|
||||
case 'openai': {
|
||||
client = new OpenAI({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
defaultHeaders: {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
},
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'anthropic': {
|
||||
client = new Anthropic({
|
||||
baseURL,
|
||||
apiKey: 'fake-key',
|
||||
defaultHeaders: {
|
||||
Authorization: token ? `Bearer ${token}` : ''
|
||||
},
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'mistral': {
|
||||
client = new Mistral({
|
||||
serverURL: baseURL
|
||||
})
|
||||
}
|
||||
}
|
||||
return client
|
||||
}
|
||||
|
||||
export async function testKey({
|
||||
apiKey,
|
||||
resourcePath,
|
||||
model,
|
||||
abortController,
|
||||
messages,
|
||||
aiProvider
|
||||
}: {
|
||||
apiKey?: string
|
||||
resourcePath?: string
|
||||
model: string | undefined
|
||||
messages: ChatCompletionMessageParam[]
|
||||
abortController: AbortController
|
||||
aiProvider: AiProviderTypes
|
||||
aiProvider: AIProvider
|
||||
}) {
|
||||
if (apiKey) {
|
||||
switch (aiProvider) {
|
||||
case 'openai': {
|
||||
const openai = new OpenAI({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
await openai.chat.completions.create(
|
||||
{
|
||||
...OpenAi.openaiConfig,
|
||||
messages,
|
||||
stream: false
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'anthropic': {
|
||||
const anthropic = new Anthropic({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
const [, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
|
||||
await anthropic.messages.create(
|
||||
{
|
||||
...AnthropicAi.config,
|
||||
messages: anthropicMessages,
|
||||
stream: false
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
case 'mistral': {
|
||||
const mistral = new Mistral({
|
||||
apiKey
|
||||
})
|
||||
await mistral.chat.complete(
|
||||
{
|
||||
...MistralAi.mistralConfig,
|
||||
model: 'codestral-latest',
|
||||
stream: false,
|
||||
messages: messages as MistralAi.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
signal: abortController.signal,
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await getNonStreamingCompletion(messages, abortController, aiProvider, undefined, true)
|
||||
if (!apiKey && !resourcePath) {
|
||||
throw new Error('API key or resource path is required')
|
||||
}
|
||||
const modelToTest = model ?? AI_DEFAULT_MODELS[aiProvider][0]
|
||||
|
||||
if (!modelToTest) {
|
||||
throw new Error('Missing a model to test')
|
||||
}
|
||||
|
||||
await getNonStreamingCompletion(
|
||||
messages,
|
||||
abortController,
|
||||
aiProvider,
|
||||
apiKey,
|
||||
resourcePath,
|
||||
modelToTest
|
||||
)
|
||||
}
|
||||
|
||||
interface BaseOptions {
|
||||
@@ -410,7 +359,9 @@ function addDBSChema(scriptOptions: CopilotOptions, prompt: string) {
|
||||
const { dbSchema, language } = scriptOptions
|
||||
if (
|
||||
dbSchema &&
|
||||
['postgresql', 'mysql', 'snowflake', 'bigquery', 'mssql', 'graphql', 'oracledb'].includes(language) && // make sure we are using a SQL/query language
|
||||
['postgresql', 'mysql', 'snowflake', 'bigquery', 'mssql', 'graphql', 'oracledb'].includes(
|
||||
language
|
||||
) && // make sure we are using a SQL/query language
|
||||
language === dbSchema.lang // make sure we are using the same language as the schema
|
||||
) {
|
||||
let { stringified } = dbSchema
|
||||
@@ -467,66 +418,109 @@ const PROMPTS_CONFIGS = {
|
||||
export async function getNonStreamingCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes,
|
||||
model = OpenAi.openaiConfig.model,
|
||||
noCache?: boolean
|
||||
aiProvider: AIProvider,
|
||||
apiKey?: string, // testing API KEY directly from the frontend
|
||||
resourcePath?: string, // testing resource path passed as a header to the backend proxy
|
||||
forceModel?: string
|
||||
) {
|
||||
let response: string | undefined = ''
|
||||
const queryOptions = {
|
||||
query: {
|
||||
no_cache: noCache
|
||||
},
|
||||
let model = forceModel
|
||||
|
||||
if (!model) {
|
||||
model = get(copilotSessionModel)
|
||||
let info = get(copilotInfo)
|
||||
const { ai_models: aiModels } = info
|
||||
|
||||
if (!model || !aiModels.includes(model)) {
|
||||
console.warn('Invalid model, using default model:', aiModels[0])
|
||||
model = aiModels[0]
|
||||
}
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
throw new Error('No model found')
|
||||
}
|
||||
|
||||
const fetchOptions: {
|
||||
signal: AbortSignal
|
||||
headers?: Record<string, string>
|
||||
} = {
|
||||
signal: abortController.signal
|
||||
}
|
||||
switch (aiProvider) {
|
||||
case 'openai': {
|
||||
const openaiClient = OpenAi.workspace.getClient()
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...OpenAi.openaiConfig,
|
||||
messages,
|
||||
stream: false,
|
||||
model
|
||||
},
|
||||
queryOptions
|
||||
)
|
||||
response = completion.choices[0]?.message.content || ''
|
||||
break
|
||||
if (resourcePath) {
|
||||
fetchOptions.headers = {
|
||||
'X-Resource-Path': resourcePath
|
||||
}
|
||||
}
|
||||
switch (aiProvider) {
|
||||
case 'anthropic': {
|
||||
const anthropicClient = AnthropicAi.workspace.getClient()
|
||||
const [system, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
|
||||
const anthropicClient = apiKey
|
||||
? new Anthropic({
|
||||
apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
: workspaceAIClients.getAnthropicClient()
|
||||
const [system, anthropicMessages] = AnthropicAI.getSystemPromptAndArrayMessages(messages)
|
||||
const message = await anthropicClient.messages.create(
|
||||
{
|
||||
...AnthropicAi.config,
|
||||
...AnthropicAI.config,
|
||||
system,
|
||||
model,
|
||||
messages: anthropicMessages,
|
||||
stream: false
|
||||
},
|
||||
queryOptions
|
||||
fetchOptions
|
||||
)
|
||||
response = message.content[0].type === 'text' ? message.content[0].text : ''
|
||||
break
|
||||
}
|
||||
case 'mistral': {
|
||||
const mistralClient = MistralAi.workspace.getClient()
|
||||
const mistralClient = apiKey
|
||||
? new Mistral({
|
||||
apiKey
|
||||
})
|
||||
: workspaceAIClients.getMistralClient()
|
||||
const message = await mistralClient.chat.complete(
|
||||
{
|
||||
...MistralAi.mistralConfig,
|
||||
model: 'codestral-latest',
|
||||
...MistralAI.mistralConfig,
|
||||
model,
|
||||
stream: false,
|
||||
messages: messages as MistralAi.MistralParamsMessage[]
|
||||
messages: messages as MistralAI.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
signal: abortController.signal,
|
||||
cache: 'no-store'
|
||||
}
|
||||
fetchOptions: fetchOptions
|
||||
}
|
||||
)
|
||||
response = MistralAi.retrieveTextValue(message.choices && message.choices[0].message.content)
|
||||
response = MistralAI.retrieveTextValue(message.choices && message.choices[0].message.content)
|
||||
break
|
||||
}
|
||||
default: {
|
||||
if (aiProvider === 'customai' && apiKey) {
|
||||
throw new Error('Cannot test API key for Custom AI, only resource path is supported')
|
||||
}
|
||||
const baseURL = OPENAI_COMPATIBLE_BASE_URLS[aiProvider]
|
||||
|
||||
if (apiKey && aiProvider !== 'openai' && !baseURL) {
|
||||
throw new Error('No base URL for this provider: ' + aiProvider)
|
||||
}
|
||||
const openaiClient = apiKey
|
||||
? new OpenAI({
|
||||
apiKey,
|
||||
baseURL,
|
||||
dangerouslyAllowBrowser: true
|
||||
})
|
||||
: workspaceAIClients.getOpenaiClient()
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...(aiProvider === 'openai' ? OpenAi.openaiConfig : DEFAULT_COMPLETION_CONFIG),
|
||||
messages,
|
||||
model,
|
||||
stream: false
|
||||
},
|
||||
fetchOptions
|
||||
)
|
||||
response = completion.choices[0]?.message.content || ''
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
@@ -534,17 +528,30 @@ export async function getNonStreamingCompletion(
|
||||
export async function getCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes,
|
||||
model = OpenAi.openaiConfig.model
|
||||
aiProvider: AIProvider
|
||||
) {
|
||||
let model = get(copilotSessionModel)
|
||||
let info = get(copilotInfo)
|
||||
const { ai_models: aiModels } = info
|
||||
|
||||
if (!model || !aiModels.includes(model)) {
|
||||
console.warn('Invalid model, using default model:', aiModels[0])
|
||||
model = aiModels[0]
|
||||
}
|
||||
|
||||
if (!model) {
|
||||
throw new Error('No model found')
|
||||
}
|
||||
|
||||
switch (aiProvider) {
|
||||
case 'anthropic': {
|
||||
const anthropicClient = AnthropicAi.workspace.getClient()
|
||||
const [system, anthropicMessages] = AnthropicAi.getSystemPromptAndArrayMessages(messages)
|
||||
const anthropicClient = workspaceAIClients.getAnthropicClient()
|
||||
const [system, anthropicMessages] = AnthropicAI.getSystemPromptAndArrayMessages(messages)
|
||||
|
||||
const completion = await anthropicClient.messages.create(
|
||||
{
|
||||
...AnthropicAi.config,
|
||||
...AnthropicAI.config,
|
||||
model,
|
||||
system,
|
||||
messages: anthropicMessages,
|
||||
stream: true
|
||||
@@ -553,13 +560,29 @@ export async function getCompletion(
|
||||
)
|
||||
return completion
|
||||
}
|
||||
case 'openai': {
|
||||
const openaiClient = OpenAi.workspace.getClient()
|
||||
case 'mistral': {
|
||||
const mistralClient = workspaceAIClients.getMistralClient()
|
||||
const message = await mistralClient.chat.stream(
|
||||
{
|
||||
...MistralAI.mistralConfig,
|
||||
model,
|
||||
messages: messages as MistralAI.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
signal: abortController.signal
|
||||
}
|
||||
}
|
||||
)
|
||||
return message
|
||||
}
|
||||
default: {
|
||||
const openaiClient = workspaceAIClients.getOpenaiClient()
|
||||
const completion = await openaiClient.chat.completions.create(
|
||||
{
|
||||
...OpenAi.openaiConfig,
|
||||
messages,
|
||||
model
|
||||
...(aiProvider === 'openai' ? OpenAi.openaiConfig : DEFAULT_COMPLETION_CONFIG),
|
||||
model,
|
||||
messages
|
||||
},
|
||||
{
|
||||
signal: abortController.signal
|
||||
@@ -567,23 +590,6 @@ export async function getCompletion(
|
||||
)
|
||||
return completion
|
||||
}
|
||||
case 'mistral': {
|
||||
const mistralClient = MistralAi.workspace.getClient()
|
||||
const message = await mistralClient.chat.stream(
|
||||
{
|
||||
...MistralAi.mistralConfig,
|
||||
model: 'codestral-latest',
|
||||
messages: messages as MistralAi.MistralParamsMessage[]
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
signal: abortController.signal,
|
||||
cache: 'no-store'
|
||||
}
|
||||
}
|
||||
)
|
||||
return message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -592,21 +598,20 @@ export function getResponseFromEvent(
|
||||
| Anthropic.Messages.RawMessageStreamEvent
|
||||
| OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
| CompletionEvent,
|
||||
aiProvider: AiProviderTypes
|
||||
aiProvider: AIProvider
|
||||
): string {
|
||||
switch (aiProvider) {
|
||||
case 'openai': {
|
||||
const messages = part as OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
return OpenAi.retrieveTextValue(messages)
|
||||
}
|
||||
case 'anthropic': {
|
||||
const messages = part as Anthropic.Messages.RawMessageStreamEvent
|
||||
return AnthropicAi.retrieveTextValue(messages)
|
||||
return AnthropicAI.retrieveTextValue(messages)
|
||||
}
|
||||
case 'mistral': {
|
||||
const messages = part as CompletionEvent
|
||||
return MistralAi.retrieveTextValue(messages.data.choices[0].delta.content)
|
||||
return MistralAI.retrieveTextValue(messages.data.choices[0].delta.content)
|
||||
}
|
||||
default:
|
||||
const messages = part as OpenAI.Chat.Completions.ChatCompletionChunk
|
||||
return OpenAi.retrieveTextValue(messages)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -614,7 +619,7 @@ export async function copilot(
|
||||
scriptOptions: CopilotOptions,
|
||||
generatedCode: Writable<string>,
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes,
|
||||
aiProvider: AIProvider,
|
||||
generatedExplanation?: Writable<string>
|
||||
) {
|
||||
const { prompt, systemPrompt } = await getPrompts(scriptOptions)
|
||||
@@ -704,7 +709,7 @@ export async function deltaCodeCompletion(
|
||||
messages: ChatCompletionMessageParam[],
|
||||
generatedCodeDelta: Writable<string>,
|
||||
abortController: AbortController,
|
||||
aiProvider: AiProviderTypes
|
||||
aiProvider: AIProvider
|
||||
) {
|
||||
const completion = await getCompletion(messages, abortController, aiProvider)
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
{newItem}
|
||||
alwaysOpened={true}
|
||||
/>
|
||||
{:else}
|
||||
<div>
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
|
||||
function resetArgs() {
|
||||
if (!previewSchema) {
|
||||
previewArguments = undefined
|
||||
// previewArguments = undefined
|
||||
savedPreviewArgs = undefined
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,8 +122,13 @@
|
||||
<Toggle
|
||||
options={{
|
||||
right: 'Continue on disapproval/timeout',
|
||||
rightTooltip:
|
||||
'Instead of failing the flow and bubbling up the error, continue to the next step which would allow to put a branchone right after to handle both cases separately. If any disapproval/timeout event is received, the resume payload will be similar to every error result in Winmdill, an object containing an `error` field which you can use to distinguish between approvals and disapproval/timeouts'
|
||||
rightTooltip: `Instead of failing the flow and bubbling up the error, continue to the next step which would allow to put a branchone right after to handle both cases separately.
|
||||
If any disapproval/timeout event is received, the resume payload will be similar to every error result in Windmill, an object containing an "error" field which you can use
|
||||
to distinguish between approvals and disapproval/timeouts.
|
||||
|
||||
We recommend using the expr "resume?.error" to handle null payload values.
|
||||
To filter timeout, use "resume?.error?.name === "SuspendedTimedOut"
|
||||
To filter disapproval, use "resume?.error?.name === "SuspendedDisapproved"`
|
||||
}}
|
||||
checked={Boolean(flowModule.suspend?.continue_on_disapprove_timeout)}
|
||||
disabled={!Boolean(flowModule.suspend)}
|
||||
@@ -133,6 +138,14 @@
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{#if Boolean(flowModule.suspend?.continue_on_disapprove_timeout)}
|
||||
<Alert type="info" title="Continue on disapproval/timeout">
|
||||
We recommend using the expr <code>resume?.error</code> to handle null payload values.
|
||||
<br />
|
||||
To filter timeout, use <code>resume?.error?.name === "SuspendedTimedOut"</code>. <br />
|
||||
To filter disapproval, use <code>resume?.error?.name === "SuspendedDisapproved"</code>
|
||||
</Alert>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if suspendTabSelected === 'permissions'}
|
||||
<div class="flex flex-col mt-4 gap-4">
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
{/if}
|
||||
|
||||
<Menubar let:createMenu>
|
||||
<Menu {createMenu} let:item placement="bottom" bind:this={menu}>
|
||||
<Menu {createMenu} let:item placement="bottom" bind:this={menu} usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<button
|
||||
title="Pick an iteration"
|
||||
|
||||
@@ -88,7 +88,7 @@ function getFlowInput(
|
||||
}
|
||||
} else {
|
||||
let parentFlowInput = getFlowInput(parentModules, flowState, args, schema)
|
||||
if (parentModule.value.type === 'forloopflow') {
|
||||
if (parentModule.value.type === 'forloopflow' || parentModule.value.type === 'whileloopflow') {
|
||||
let parentFlowInputIter = { ...parentFlowInput }
|
||||
if (parentFlowInputIter.hasOwnProperty('iter')) {
|
||||
parentFlowInputIter['iter_parent'] = parentFlowInputIter['iter']
|
||||
@@ -263,9 +263,8 @@ declare const results = ${JSON.stringify(results)};
|
||||
*/
|
||||
declare const previous_result: ${previousId ? JSON.stringify(results[previousId]) : 'any'};
|
||||
|
||||
${
|
||||
resume
|
||||
? `
|
||||
${resume
|
||||
? `
|
||||
/**
|
||||
* resume payload
|
||||
*/
|
||||
@@ -276,8 +275,8 @@ declare const resume: any
|
||||
*/
|
||||
declare const approvers: string
|
||||
`
|
||||
: ''
|
||||
}
|
||||
: ''
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { FlowService, type FlowModule } from '../../gen'
|
||||
import { NODE, type GraphModuleState } from '.'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount, setContext } from 'svelte'
|
||||
import { createEventDispatcher, getContext, onDestroy, onMount, setContext, tick } from 'svelte'
|
||||
|
||||
import { get, writable, type Writable } from 'svelte/store'
|
||||
import '@xyflow/svelte/dist/style.css'
|
||||
@@ -273,7 +273,7 @@
|
||||
return false
|
||||
}
|
||||
|
||||
function updateStores() {
|
||||
async function updateStores() {
|
||||
if (graph.error) {
|
||||
return
|
||||
}
|
||||
@@ -281,6 +281,7 @@
|
||||
|
||||
$nodes = layoutNodes(newGraph.nodes)
|
||||
$edges = newGraph.edges
|
||||
await tick()
|
||||
height = Math.max(...$nodes.map((n) => n.position.y + NODE.height + 100), minHeight)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
version="1.2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1594 1595"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<title>auth0ddd-svg</title>
|
||||
<style>
|
||||
.s0 {
|
||||
fill: #000000;
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
class="s0"
|
||||
d="M160.183716,48.171844 C209.065292,32.885063 258.390625,21.436710 308.871613,15.252386 C317.292297,14.220791 325.753601,13.356311 334.222168,12.919684 C346.325989,12.295630 355.267456,21.589764 353.666168,34.529034 C349.868744,65.213501 348.537781,96.246513 341.092957,126.439857 C326.677368,184.904236 298.682648,235.976761 256.968506,279.307343 C213.326065,324.640991 160.987228,355.220551 99.454086,369.350647 C86.185066,372.397675 72.675255,374.468994 59.210712,376.568817 C48.252338,378.277832 37.322960,368.554504 37.025162,357.267883 C36.888950,352.105377 37.000465,346.936340 37.000454,341.770264 C37.000340,275.444489 37.000156,209.118698 37.000484,142.792908 C37.000629,113.734810 53.605553,87.790001 80.711502,77.096191 C106.802269,66.802895 133.414459,57.831257 160.183716,48.171844 z M473.292908,18.782619 C506.484009,23.757580 538.685669,31.232409 570.535767,40.198071 C606.031982,50.190132 640.835815,62.258595 674.995361,76.160843 C703.688293,87.838272 720.524536,113.183640 720.506226,144.306686 C720.465149,214.123505 720.485657,283.940338 720.495422,353.757172 C720.497620,369.678192 710.628052,378.153381 694.790466,376.099304 C645.966858,369.767212 600.276794,354.609619 558.938660,327.513123 C494.944519,285.566010 450.524231,228.198090 425.068176,156.124725 C416.448669,131.720474 411.208069,106.565437 408.906525,80.760696 C407.739166,67.672127 405.700104,54.662220 404.473694,41.577518 C403.999908,36.522667 403.805481,31.137602 405.026886,26.291929 C407.479736,16.560717 413.882874,12.037321 423.787903,12.980925 C440.178345,14.542362 456.506195,16.760437 473.292908,18.782619 z M578.363403,464.347748 C615.531311,446.366882 654.581116,435.908630 694.772705,429.284882 C697.708374,428.801056 700.752136,428.857452 703.745483,428.859894 C714.899902,428.868988 720.458801,434.582397 718.691895,445.492645 C716.009277,462.057404 713.484558,478.739502 709.260254,494.946198 C694.406738,551.932251 666.318115,602.423340 631.191589,649.183105 C575.914368,722.766968 508.061218,783.052307 432.927094,835.552307 C431.018921,836.885681 429.046265,838.127625 427.153870,839.482422 C422.472626,842.833557 417.533234,844.040344 412.248444,841.064758 C406.852631,838.026794 404.090637,833.218079 404.209808,827.096191 C404.527344,810.783264 404.724487,794.456909 405.573883,778.167480 C407.684601,737.688477 413.126068,697.680664 423.966522,658.536804 C436.856445,611.992310 456.846161,569.063904 488.758606,532.264648 C513.768555,503.424927 543.806946,481.217163 578.363403,464.347748 z M100.761627,612.221680 C75.728386,572.424377 56.110188,530.629822 45.663021,484.961426 C42.630875,471.706848 40.741016,458.167358 38.833408,444.686859 C37.390755,434.492065 42.754341,427.939545 52.986912,429.076324 C69.119164,430.868469 85.245743,433.507080 101.069740,437.122314 C140.869492,446.215149 178.476105,460.947479 212.772552,483.436096 C263.321442,516.581604 297.953369,562.429321 320.055481,618.236816 C334.840118,655.568054 343.272247,694.426331 348.081482,734.171936 C351.726166,764.293335 353.368927,794.520508 353.156128,824.854004 C353.142120,826.849976 353.278076,828.905640 352.863708,830.830078 C350.998230,839.494263 340.934357,847.105835 330.938843,839.874878 C313.414551,827.197754 295.720337,814.735718 278.557312,801.583313 C248.662613,778.674316 220.522430,753.696289 193.948929,727.012268 C159.090790,692.009094 127.388931,654.396790 100.761627,612.221680 z"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
@@ -0,0 +1,26 @@
|
||||
<script lang="ts">
|
||||
export let size: number = 16
|
||||
export let style: string = 'fill: white;'
|
||||
let clazz = ''
|
||||
export { clazz as class }
|
||||
</script>
|
||||
|
||||
<svg
|
||||
version="1.2"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1594 1595"
|
||||
height={`${size}px`}
|
||||
class={clazz}
|
||||
{style}
|
||||
>
|
||||
<title>auth0ddd-svg</title>
|
||||
<style>
|
||||
.s0 {
|
||||
fill: #000000;
|
||||
}
|
||||
</style>
|
||||
<path
|
||||
class="s0"
|
||||
d="M160.183716,48.171844 C209.065292,32.885063 258.390625,21.436710 308.871613,15.252386 C317.292297,14.220791 325.753601,13.356311 334.222168,12.919684 C346.325989,12.295630 355.267456,21.589764 353.666168,34.529034 C349.868744,65.213501 348.537781,96.246513 341.092957,126.439857 C326.677368,184.904236 298.682648,235.976761 256.968506,279.307343 C213.326065,324.640991 160.987228,355.220551 99.454086,369.350647 C86.185066,372.397675 72.675255,374.468994 59.210712,376.568817 C48.252338,378.277832 37.322960,368.554504 37.025162,357.267883 C36.888950,352.105377 37.000465,346.936340 37.000454,341.770264 C37.000340,275.444489 37.000156,209.118698 37.000484,142.792908 C37.000629,113.734810 53.605553,87.790001 80.711502,77.096191 C106.802269,66.802895 133.414459,57.831257 160.183716,48.171844 z M473.292908,18.782619 C506.484009,23.757580 538.685669,31.232409 570.535767,40.198071 C606.031982,50.190132 640.835815,62.258595 674.995361,76.160843 C703.688293,87.838272 720.524536,113.183640 720.506226,144.306686 C720.465149,214.123505 720.485657,283.940338 720.495422,353.757172 C720.497620,369.678192 710.628052,378.153381 694.790466,376.099304 C645.966858,369.767212 600.276794,354.609619 558.938660,327.513123 C494.944519,285.566010 450.524231,228.198090 425.068176,156.124725 C416.448669,131.720474 411.208069,106.565437 408.906525,80.760696 C407.739166,67.672127 405.700104,54.662220 404.473694,41.577518 C403.999908,36.522667 403.805481,31.137602 405.026886,26.291929 C407.479736,16.560717 413.882874,12.037321 423.787903,12.980925 C440.178345,14.542362 456.506195,16.760437 473.292908,18.782619 z M578.363403,464.347748 C615.531311,446.366882 654.581116,435.908630 694.772705,429.284882 C697.708374,428.801056 700.752136,428.857452 703.745483,428.859894 C714.899902,428.868988 720.458801,434.582397 718.691895,445.492645 C716.009277,462.057404 713.484558,478.739502 709.260254,494.946198 C694.406738,551.932251 666.318115,602.423340 631.191589,649.183105 C575.914368,722.766968 508.061218,783.052307 432.927094,835.552307 C431.018921,836.885681 429.046265,838.127625 427.153870,839.482422 C422.472626,842.833557 417.533234,844.040344 412.248444,841.064758 C406.852631,838.026794 404.090637,833.218079 404.209808,827.096191 C404.527344,810.783264 404.724487,794.456909 405.573883,778.167480 C407.684601,737.688477 413.126068,697.680664 423.966522,658.536804 C436.856445,611.992310 456.846161,569.063904 488.758606,532.264648 C513.768555,503.424927 543.806946,481.217163 578.363403,464.347748 z M100.761627,612.221680 C75.728386,572.424377 56.110188,530.629822 45.663021,484.961426 C42.630875,471.706848 40.741016,458.167358 38.833408,444.686859 C37.390755,434.492065 42.754341,427.939545 52.986912,429.076324 C69.119164,430.868469 85.245743,433.507080 101.069740,437.122314 C140.869492,446.215149 178.476105,460.947479 212.772552,483.436096 C263.321442,516.581604 297.953369,562.429321 320.055481,618.236816 C334.840118,655.568054 343.272247,694.426331 348.081482,734.171936 C351.726166,764.293335 353.368927,794.520508 353.156128,824.854004 C353.142120,826.849976 353.278076,828.905640 352.863708,830.830078 C350.998230,839.494263 340.934357,847.105835 330.938843,839.874878 C313.414551,827.197754 295.720337,814.735718 278.557312,801.583313 C248.662613,778.674316 220.522430,753.696289 193.948929,727.012268 C159.090790,692.009094 127.388931,654.396790 100.761627,612.221680 z"
|
||||
/>
|
||||
</svg>
|
||||
@@ -68,6 +68,7 @@ import GraphqlIcon from './GraphqlIcon.svelte'
|
||||
import NocoDbIcon from './NocoDbIcon.svelte'
|
||||
import AzureIcon from './AzureIcon.svelte'
|
||||
import OktaIcon from './OktaIcon.svelte'
|
||||
import Auth0Icon from './Auth0Icon.svelte'
|
||||
import MsSqlServerIcon from './MSSqlServerIcon.svelte'
|
||||
import AuthentikIcon from './AuthentikIcon.svelte'
|
||||
import AutheliaIcon from './AutheliaIcon.svelte'
|
||||
@@ -170,6 +171,7 @@ export const APP_TO_ICON_COMPONENT = {
|
||||
nocodb: NocoDbIcon,
|
||||
azure: AzureIcon,
|
||||
okta: OktaIcon,
|
||||
auth0: Auth0Icon,
|
||||
authentik: AuthentikIcon,
|
||||
authelia: AutheliaIcon,
|
||||
kanidm: KanidmIcon,
|
||||
@@ -268,6 +270,7 @@ export {
|
||||
AzureIcon,
|
||||
MicrosoftIcon,
|
||||
OktaIcon,
|
||||
Auth0Icon,
|
||||
AuthentikIcon,
|
||||
AutheliaIcon,
|
||||
KanidmIcon,
|
||||
|
||||
@@ -8,11 +8,11 @@ export interface Setting {
|
||||
key: string
|
||||
// If value is not specified for first element, it will automatcally use undefined
|
||||
select_items?: {
|
||||
label: string,
|
||||
tooltip?: string,
|
||||
label: string
|
||||
tooltip?: string
|
||||
// If not specified, label will be used
|
||||
value?: any,
|
||||
}[],
|
||||
value?: any
|
||||
}[]
|
||||
fieldType:
|
||||
| 'text'
|
||||
| 'number'
|
||||
@@ -41,7 +41,7 @@ export interface Setting {
|
||||
isValid?: (value: any) => boolean
|
||||
error?: string
|
||||
defaultValue?: () => any
|
||||
codeAreaLang?: string,
|
||||
codeAreaLang?: string
|
||||
}
|
||||
|
||||
export type SettingStorage = 'setting'
|
||||
@@ -81,9 +81,9 @@ export const settings: Record<string, Setting[]> = {
|
||||
isValid: (value: string | undefined) =>
|
||||
value
|
||||
? value?.startsWith('http') &&
|
||||
value.includes('://') &&
|
||||
!value?.endsWith('/') &&
|
||||
!value?.endsWith(' ')
|
||||
value.includes('://') &&
|
||||
!value?.endsWith('/') &&
|
||||
!value?.endsWith(' ')
|
||||
: false
|
||||
},
|
||||
{
|
||||
@@ -182,7 +182,7 @@ export const settings: Record<string, Setting[]> = {
|
||||
{
|
||||
label: 'Azure OpenAI base path',
|
||||
description:
|
||||
'All Windmill AI features will run on the specified deployed model. Format: https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}. <a href="https://www.windmill.dev/docs/core_concepts/ai_generation#azure-openai-advanced-models">Learn more</a>',
|
||||
'All workspaces using an OpenAI resource for Windmill AI will run on the specified deployed model. Format: https://{your-resource-name}.openai.azure.com/openai/deployments/{deployment-id}. <a href="https://www.windmill.dev/docs/core_concepts/ai_generation#azure-openai-advanced-models">Learn more</a>',
|
||||
key: 'openai_azure_base_path',
|
||||
fieldType: 'text',
|
||||
storage: 'setting',
|
||||
@@ -236,24 +236,26 @@ export const settings: Record<string, Setting[]> = {
|
||||
// 2. Change LATEST_STABLE_PY in dockerfile
|
||||
// 3. Change #[default] annotation for PyVersion in backend
|
||||
placeholder: '3.10,3.11,3.12,3.13',
|
||||
select_items: [{
|
||||
label: "Latest Stable",
|
||||
value: "default",
|
||||
tooltip: "python-3.11",
|
||||
},
|
||||
{
|
||||
label: "3.10",
|
||||
},
|
||||
{
|
||||
label: "3.11",
|
||||
},
|
||||
{
|
||||
label: "3.12",
|
||||
},
|
||||
{
|
||||
label: "3.13",
|
||||
}],
|
||||
storage: 'setting',
|
||||
select_items: [
|
||||
{
|
||||
label: 'Latest Stable',
|
||||
value: 'default',
|
||||
tooltip: 'python-3.11'
|
||||
},
|
||||
{
|
||||
label: '3.10'
|
||||
},
|
||||
{
|
||||
label: '3.11'
|
||||
},
|
||||
{
|
||||
label: '3.12'
|
||||
},
|
||||
{
|
||||
label: '3.13'
|
||||
}
|
||||
],
|
||||
storage: 'setting'
|
||||
},
|
||||
{
|
||||
label: 'Pip index url',
|
||||
@@ -294,8 +296,7 @@ export const settings: Record<string, Setting[]> = {
|
||||
},
|
||||
{
|
||||
label: 'Nuget Config',
|
||||
description:
|
||||
'Write a nuget.config file to set custom package sources and credentials',
|
||||
description: 'Write a nuget.config file to set custom package sources and credentials',
|
||||
key: 'nuget_config',
|
||||
fieldType: 'codearea',
|
||||
codeAreaLang: 'xml',
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
import { melt, createSync } from '@melt-ui/svelte'
|
||||
import type { Placement } from '@floating-ui/core'
|
||||
import { pointerDownOutside } from '$lib/utils'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import ResolveOpen from '$lib/components/common/menu/ResolveOpen.svelte'
|
||||
|
||||
@@ -12,6 +13,7 @@
|
||||
export let disabled = false
|
||||
export let createMenu: (any) => any
|
||||
export let invisible: boolean = false
|
||||
export let usePointerDownOutside: boolean = false
|
||||
|
||||
// Use the passed createMenu function
|
||||
const menu = createMenu({
|
||||
@@ -35,17 +37,36 @@
|
||||
export function close() {
|
||||
open = false
|
||||
}
|
||||
|
||||
async function getMenuElements(): Promise<HTMLElement[]> {
|
||||
return Array.from(document.querySelectorAll('[data-menu]')) as HTMLElement[]
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class={twMerge('w-full h-8', $$props.class)}>
|
||||
<ResolveOpen {open} on:open on:close />
|
||||
|
||||
<button class={twMerge('w-full h-full', justifyEnd ? 'flex justify-end' : '')} {disabled}>
|
||||
<button
|
||||
class={twMerge('w-full h-full', justifyEnd ? 'flex justify-end' : '')}
|
||||
{disabled}
|
||||
use:pointerDownOutside={{
|
||||
capture: true,
|
||||
stopPropagation: false,
|
||||
exclude: getMenuElements,
|
||||
customEventName: 'pointerdown_menu'
|
||||
}}
|
||||
on:pointerdown_outside={() => {
|
||||
if (usePointerDownOutside) {
|
||||
close()
|
||||
}
|
||||
}}
|
||||
data-menu
|
||||
>
|
||||
<slot name="trigger" trigger={$trigger} />
|
||||
</button>
|
||||
|
||||
<Portal name="menu-v3">
|
||||
<div class="z-[6000]" use:melt={$menuElement}>
|
||||
<div class="z-[6000]" use:melt={$menuElement} data-menu>
|
||||
<div
|
||||
class={twMerge(
|
||||
'border w-56 origin-top-right rounded-md shadow-md focus:outline-none overflow-y-auto',
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
<script lang="ts">
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
|
||||
export let href: string | undefined = undefined
|
||||
export let disabled: boolean = false
|
||||
|
||||
export let target: string | undefined = undefined
|
||||
export let item: any
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if href}
|
||||
<a {href} use:melt={item} {disabled} class={$$props.class}>
|
||||
<a
|
||||
use:melt={item}
|
||||
{href}
|
||||
class={$$props.class}
|
||||
role="menuitem"
|
||||
aria-disabled={disabled}
|
||||
tabindex={disabled ? -1 : undefined}
|
||||
{target}
|
||||
>
|
||||
<slot />
|
||||
</a>
|
||||
{:else}
|
||||
<button use:melt={item} on:click={(e) => dispatch('click', e)} {disabled} class={$$props.class}>
|
||||
<button use:melt={item} on:click {disabled} class={$$props.class} role="menuitem">
|
||||
<slot />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export { default as Menu } from './Menu.svelte'
|
||||
export { default as Menubar } from './Menubar.svelte'
|
||||
export { default as MenuSingleItem } from './MenuSingleItem.svelte'
|
||||
export { default as MenuItem } from './MenuItem.svelte'
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
size="xs2"
|
||||
variant="border"
|
||||
on:click={collapse}
|
||||
wrapperClasses="inline-flex w-fit"
|
||||
wrapperClasses="!inline-flex w-fit"
|
||||
btnClasses="font-mono h-4 text-2xs px-1 font-thin text-primary rounded-[0.275rem]"
|
||||
>-</Button
|
||||
>
|
||||
@@ -218,7 +218,7 @@
|
||||
size="xs2"
|
||||
variant="border"
|
||||
on:click={collapse}
|
||||
wrapperClasses="inline-flex w-fit"
|
||||
wrapperClasses="!inline-flex w-fit"
|
||||
btnClasses="h-4 text-[9px] px-1 text-primary rounded-[0.275rem]"
|
||||
>
|
||||
{openBracket}{collapsedSymbol}{closeBracket}
|
||||
|
||||
@@ -9,15 +9,14 @@
|
||||
// import { copyToClipboard } from '$lib/utils'
|
||||
// import { deepEqual } from 'fast-equals'
|
||||
|
||||
export let flowStatus: any
|
||||
export let preprocessed: boolean | undefined
|
||||
|
||||
// $: args =
|
||||
// '_metadata' in flowStatus && 'original_args' in flowStatus['_metadata']
|
||||
// ? flowStatus['_metadata']['original_args']
|
||||
// : undefined
|
||||
|
||||
$: hasPreprocessedArgs =
|
||||
'_metadata' in flowStatus && !!flowStatus['_metadata']['preprocessed_args']
|
||||
$: hasPreprocessedArgs = preprocessed === true
|
||||
|
||||
// $: argsStr = args !== undefined ? JSON.stringify(args, null, 4) : undefined
|
||||
|
||||
|
||||
@@ -104,6 +104,7 @@
|
||||
</AddPropertyV2>
|
||||
{/if}
|
||||
<EditableSchemaForm
|
||||
onlyMaskPassword
|
||||
bind:this={editableSchemaForm}
|
||||
bind:schema
|
||||
on:change={() => dispatch('change', schema)}
|
||||
|
||||
@@ -304,7 +304,7 @@
|
||||
{/if}
|
||||
{:else if type === 'object' && format !== 'resource-s3_object'}
|
||||
<Tabs
|
||||
selected={initialObjectSelected}
|
||||
bind:selected={initialObjectSelected}
|
||||
on:selected={(e) => {
|
||||
if (e.detail === 'custom-object') {
|
||||
format = ''
|
||||
@@ -327,7 +327,7 @@
|
||||
</Tabs>
|
||||
{/if}
|
||||
|
||||
{#if !(type === 'object' && oneOf && oneOf.length >= 2)}
|
||||
{#if !(type === 'object' && oneOf && oneOf.length >= 2) && !(type == 'object' && initialObjectSelected == 'custom-object')}
|
||||
<Label label="Default">
|
||||
<ArgInput
|
||||
noDefaultOnSelectFirst
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
export let inputSelected: 'history' | 'captures' | 'saved' | undefined = undefined
|
||||
</script>
|
||||
|
||||
<div class="min-h-[34px]">
|
||||
<div class="min-h-[38px]">
|
||||
<div
|
||||
class="rounded-md flex flex-row gap-2 items-center py-1 px-2 w-fit {classes['info']
|
||||
.bgClass} {inputSelected ? '' : 'hidden'}"
|
||||
|
||||
@@ -160,7 +160,11 @@
|
||||
</Label>
|
||||
|
||||
{#if type == 'array'}
|
||||
<ArrayTypeNarrowing bind:itemsType canEditResourceType={isFlowInput || isAppInput} />
|
||||
<ArrayTypeNarrowing
|
||||
originalType={extra['originalType']}
|
||||
bind:itemsType
|
||||
canEditResourceType={isFlowInput || isAppInput}
|
||||
/>
|
||||
{:else if type == 'string' || ['number', 'integer', 'object'].includes(type ?? '')}
|
||||
<div>
|
||||
<Label label="Field settings">
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { Code2, LayoutDashboard, Star } from 'lucide-svelte'
|
||||
import { CodeXml, LayoutDashboard, Star } from 'lucide-svelte'
|
||||
import BarsStaggered from '$lib/components/icons/BarsStaggered.svelte'
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
import Menu from '$lib/components/meltComponents/Menu.svelte'
|
||||
import MenuButtonMelt from '$lib/components/sidebar/MenuButtonMelt.svelte'
|
||||
import { Menu, MenuItem } from '$lib/components/meltComponents'
|
||||
import MenuButton from '$lib/components/sidebar/MenuButton.svelte'
|
||||
export let lightMode: boolean = false
|
||||
|
||||
export let isCollapsed: boolean = false
|
||||
@@ -15,9 +14,9 @@
|
||||
export let createMenu: any
|
||||
</script>
|
||||
|
||||
<Menu {createMenu} let:item>
|
||||
<Menu {createMenu} let:item usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<MenuButtonMelt
|
||||
<MenuButton
|
||||
class="!text-xs"
|
||||
icon={Star}
|
||||
label={'Favorites'}
|
||||
@@ -37,14 +36,14 @@
|
||||
{:else}
|
||||
<div class="py-1 w-full max-w-full">
|
||||
{#each favoriteLinks ?? [] as favorite (favorite.href)}
|
||||
<a
|
||||
<MenuItem
|
||||
href={favorite.href}
|
||||
use:melt={item}
|
||||
class="w-full inline-flex flex-row px-4 py-2 hover:bg-surface-hover data-[highlighted]:bg-surface-hover"
|
||||
{item}
|
||||
class="w-full inline-flex flex-row px-4 py-2 data-[highlighted]:bg-surface-hover"
|
||||
>
|
||||
<span class="center-center">
|
||||
{#if favorite.kind == 'script'}
|
||||
<Code2 size={16} />
|
||||
<CodeXml size={16} />
|
||||
{:else if favorite.kind == 'flow'}
|
||||
<BarsStaggered size={16} />
|
||||
{:else if favorite.kind == 'app' || favorite.kind == 'raw_app'}
|
||||
@@ -54,7 +53,7 @@
|
||||
<span class="text-primary ml-2 grow min-w-0 text-xs truncate">
|
||||
{favorite.label}
|
||||
</span>
|
||||
</a>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
import Popover from '../Popover.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SideBarNotification from './SideBarNotification.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
import { conditionalMelt } from '$lib/utils'
|
||||
export let label: string | undefined = undefined
|
||||
export let icon: any | undefined = undefined
|
||||
export let isCollapsed: boolean
|
||||
@@ -12,6 +14,9 @@
|
||||
export let shortcut: string = ''
|
||||
export let notificationsCount: number = 0
|
||||
export let color: string | null = null
|
||||
export let trigger: any | undefined = undefined
|
||||
export let href: string | undefined = undefined
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
@@ -26,19 +31,24 @@
|
||||
<button
|
||||
on:click={(e) => {
|
||||
if (stopPropagationOnClick) e.preventDefault()
|
||||
if (href) {
|
||||
goto(href)
|
||||
}
|
||||
dispatch('click')
|
||||
}}
|
||||
class={twMerge(
|
||||
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
|
||||
lightMode
|
||||
? 'text-primary hover:bg-surface-hover '
|
||||
: 'hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
|
||||
? 'text-primary data-[highlighted]:bg-surface-hover hover:bg-surface-hover'
|
||||
: 'data-[highlighted]:bg-[#2A3648] hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
|
||||
color ? 'border-4' : '',
|
||||
'transition-all',
|
||||
'transition-all relative',
|
||||
$$props.class
|
||||
)}
|
||||
style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 4px);` : ''}
|
||||
use:conditionalMelt={trigger}
|
||||
title={isCollapsed ? undefined : label}
|
||||
{...trigger}
|
||||
>
|
||||
{#if icon}
|
||||
<svelte:component
|
||||
|
||||
@@ -1,100 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import SideBarNotification from './SideBarNotification.svelte'
|
||||
import { goto } from '$app/navigation'
|
||||
|
||||
export let label: string | undefined = undefined
|
||||
export let icon: any | undefined = undefined
|
||||
export let isCollapsed: boolean
|
||||
export let disabled: boolean = false
|
||||
export let lightMode: boolean = false
|
||||
export let stopPropagationOnClick: boolean = false
|
||||
export let shortcut: string = ''
|
||||
export let notificationsCount: number = 0
|
||||
export let color: string | null = null
|
||||
export let trigger: any | undefined = undefined
|
||||
export let href: string | undefined = undefined
|
||||
export let openHref: boolean = false
|
||||
|
||||
let dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
{#if !disabled}
|
||||
<Popover
|
||||
appearTimeout={0}
|
||||
disappearTimeout={0}
|
||||
class="w-full"
|
||||
disablePopup={!isCollapsed}
|
||||
placement="right"
|
||||
>
|
||||
<button
|
||||
on:click={(e) => {
|
||||
if (stopPropagationOnClick) e.preventDefault()
|
||||
if (openHref && href) {
|
||||
goto(href)
|
||||
}
|
||||
dispatch('click')
|
||||
}}
|
||||
class={twMerge(
|
||||
'group flex items-center px-2 py-2 font-light rounded-md h-8 gap-3 w-full',
|
||||
lightMode
|
||||
? 'text-primary data-[highlighted]:bg-surface-hover hover:bg-surface-hover'
|
||||
: 'data-[highlighted]:bg-[#2A3648] hover:bg-[#2A3648] text-primary-inverse dark:text-primary',
|
||||
color ? 'border-4' : '',
|
||||
'transition-all relative',
|
||||
$$props.class
|
||||
)}
|
||||
style={color ? `border-color: ${color}; padding: 0 calc(0.5rem - 4px);` : ''}
|
||||
use:melt={trigger}
|
||||
title={isCollapsed ? undefined : label}
|
||||
>
|
||||
{#if icon}
|
||||
<svelte:component
|
||||
this={icon}
|
||||
size={16}
|
||||
class={twMerge(
|
||||
'flex-shrink-0',
|
||||
lightMode
|
||||
? 'text-primary group-hover:text-secondary'
|
||||
: 'text-primary-inverse group-hover:text-secondary-inverse dark:group-hover:text-secondary dark:text-primary',
|
||||
'transition-all'
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !isCollapsed && label}
|
||||
<span
|
||||
class={twMerge(
|
||||
'whitespace-pre truncate',
|
||||
lightMode ? 'text-primary' : 'text-primary-inverse dark:text-primary',
|
||||
'transition-all',
|
||||
$$props.class
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
<span class="pl-2 text-xs dark:text-secondary light:text-secondary-inverse font-semibold">
|
||||
{shortcut}
|
||||
</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
{#if isCollapsed && notificationsCount > 0}
|
||||
<div class="absolute translate-x-1/2 translate-y-1/2 -top-2 right-1 flex h-fit w-fit">
|
||||
<SideBarNotification notificationCount={notificationsCount} small={true} />
|
||||
</div>
|
||||
{:else if notificationsCount > 0}
|
||||
<div class="ml-auto">
|
||||
<SideBarNotification notificationCount={notificationsCount} small={false} />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
<svelte:fragment slot="text">
|
||||
{#if label}
|
||||
{label}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { classNames, conditionalMelt } from '$lib/utils'
|
||||
import { navigating, page } from '$app/stores'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { base } from '$app/paths'
|
||||
@@ -10,6 +10,7 @@
|
||||
export let isCollapsed: boolean
|
||||
export let disabled: boolean = false
|
||||
export let lightMode: boolean = false
|
||||
export let item: any | undefined = undefined
|
||||
|
||||
let isSelected = false
|
||||
|
||||
@@ -23,30 +24,25 @@
|
||||
</script>
|
||||
|
||||
{#if !disabled}
|
||||
<Popover
|
||||
appearTimeout={0}
|
||||
disappearTimeout={0}
|
||||
class="w-full"
|
||||
disablePopup={!isCollapsed}
|
||||
placement="right"
|
||||
>
|
||||
<Popover appearTimeout={0} disappearTimeout={0} class="w-full" disablePopup={!isCollapsed}>
|
||||
<a
|
||||
{href}
|
||||
class={classNames(
|
||||
'group flex items-center px-2 py-2 text-sm font-light rounded-md h-8 gap-3',
|
||||
isSelected
|
||||
? lightMode
|
||||
? 'bg-surface-selected hover:bg-surface-hover rounded-none'
|
||||
: 'bg-frost-700 hover:bg-[#30404e]'
|
||||
? 'bg-surface-selected hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
|
||||
: 'bg-frost-700 hover:bg-[#30404e] data-[highlighted]:bg-[#30404e]'
|
||||
: lightMode
|
||||
? 'hover:bg-surface-hover rounded-none'
|
||||
: 'hover:bg-[#2A3648]',
|
||||
? 'hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
|
||||
: 'hover:bg-[#2A3648] data-[highlighted]:bg-[#2A3648]',
|
||||
|
||||
'hover:transition-all',
|
||||
$$props.class
|
||||
)}
|
||||
target={href.includes('http') ? '_blank' : null}
|
||||
title={isCollapsed ? undefined : label}
|
||||
use:conditionalMelt={item}
|
||||
>
|
||||
{#if icon}
|
||||
<svelte:component
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { classNames } from '$lib/utils'
|
||||
import { navigating, page } from '$app/stores'
|
||||
import Popover from '../Popover.svelte'
|
||||
import { base } from '$app/paths'
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
export let label: string
|
||||
export let href: string
|
||||
export let icon: any | undefined = undefined
|
||||
export let isCollapsed: boolean
|
||||
export let disabled: boolean = false
|
||||
export let lightMode: boolean = false
|
||||
export let item: any
|
||||
let isSelected = false
|
||||
|
||||
navigating.subscribe(() => {
|
||||
if (href === `${base}/`) {
|
||||
isSelected = $page.url.pathname === href
|
||||
} else {
|
||||
isSelected = $page.url.pathname.startsWith(href)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
{#if !disabled}
|
||||
<Popover appearTimeout={0} disappearTimeout={0} class="w-full" disablePopup={!isCollapsed}>
|
||||
<a
|
||||
{href}
|
||||
class={classNames(
|
||||
'group flex items-center px-2 py-2 text-sm font-light rounded-md h-8 gap-3',
|
||||
isSelected
|
||||
? lightMode
|
||||
? 'bg-surface-selected hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
|
||||
: 'bg-frost-700 hover:bg-[#30404e] data-[highlighted]:bg-[#30404e]'
|
||||
: lightMode
|
||||
? 'hover:bg-surface-hover rounded-none data-[highlighted]:bg-surface-hover'
|
||||
: 'hover:bg-[#2A3648] data-[highlighted]:bg-[#2A3648]',
|
||||
|
||||
'hover:transition-all',
|
||||
$$props.class
|
||||
)}
|
||||
target={href.includes('http') ? '_blank' : null}
|
||||
title={isCollapsed ? undefined : label}
|
||||
use:melt={item}
|
||||
>
|
||||
{#if icon}
|
||||
<svelte:component
|
||||
this={icon}
|
||||
size={16}
|
||||
class={classNames(
|
||||
'flex-shrink-0',
|
||||
isSelected
|
||||
? lightMode
|
||||
? 'text-primary group-hover:text-secondary'
|
||||
: 'text-frost-200 group-hover:text-white'
|
||||
: lightMode
|
||||
? 'text-primary group-hover:text-secondary'
|
||||
: 'text-gray-100 group-hover:text-white',
|
||||
'transition-all'
|
||||
)}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
{#if !isCollapsed}
|
||||
<span
|
||||
class={classNames(
|
||||
'whitespace-pre truncate',
|
||||
isSelected
|
||||
? lightMode
|
||||
? 'text-primary group-hover:text-secondary'
|
||||
: 'text-frost-200 group-hover:text-white'
|
||||
: lightMode
|
||||
? 'text-primary group-hover:text-secondary'
|
||||
: 'text-gray-100 group-hover:text-white',
|
||||
'transition-all duration-75'
|
||||
)}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
<svelte:fragment slot="text">
|
||||
{label}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
@@ -22,10 +22,9 @@
|
||||
import { logout } from '$lib/logout'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
import BarsStaggered from '../icons/BarsStaggered.svelte'
|
||||
import Menu from '$lib/components/meltComponents/Menu.svelte'
|
||||
import Menubar from '$lib/components/meltComponents/Menubar.svelte'
|
||||
import MenuButtonMelt from './MenuButtonMelt.svelte'
|
||||
import MenuLinkMelt from './MenuLinkMelt.svelte'
|
||||
import { Menu, Menubar } from '$lib/components/meltComponents'
|
||||
import MenuButton from './MenuButton.svelte'
|
||||
import MenuLink from './MenuLink.svelte'
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
let darkMode: boolean = false
|
||||
|
||||
@@ -116,9 +115,9 @@
|
||||
</script>
|
||||
|
||||
<Menubar let:createMenu>
|
||||
<Menu {createMenu} let:item>
|
||||
<Menu {createMenu} let:item usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<MenuButtonMelt
|
||||
<MenuButton
|
||||
class="!text-xs"
|
||||
icon={MenuIcon}
|
||||
{isCollapsed}
|
||||
@@ -154,7 +153,7 @@
|
||||
</div>
|
||||
|
||||
{#each mainMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
|
||||
<MenuLinkMelt class="!text-xs" {...menuLink} {isCollapsed} lightMode {item} />
|
||||
<MenuLink class="!text-xs" {...menuLink} {isCollapsed} lightMode {item} />
|
||||
{/each}
|
||||
|
||||
<div class="divide-y" role="none">
|
||||
@@ -283,7 +282,13 @@
|
||||
</div>
|
||||
</div>
|
||||
{#if $enterpriseLicense}
|
||||
<MultiplayerMenu />
|
||||
<div
|
||||
on:mouseenter={() => (moreOpen = true)}
|
||||
on:mouseleave={() => (moreOpen = false)}
|
||||
role="none"
|
||||
>
|
||||
<MultiplayerMenu />
|
||||
</div>
|
||||
{/if}
|
||||
</Menu>
|
||||
</Menubar>
|
||||
|
||||
@@ -50,12 +50,9 @@
|
||||
import SideBarNotification from './SideBarNotification.svelte'
|
||||
import KafkaIcon from '../icons/KafkaIcon.svelte'
|
||||
import NatsIcon from '../icons/NatsIcon.svelte'
|
||||
import Menubar from '../meltComponents/Menubar.svelte'
|
||||
import Menu from '../meltComponents/Menu.svelte'
|
||||
import { Menubar, Menu, MenuSingleItem, MenuItem } from '$lib/components/meltComponents'
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
import MenuButtonMelt from './MenuButtonMelt.svelte'
|
||||
import MenuSingleItem from '../meltComponents/MenuSingleItem.svelte'
|
||||
import MenuLinkMelt from './MenuLinkMelt.svelte'
|
||||
import MenuButton from './MenuButton.svelte'
|
||||
export let numUnacknowledgedCriticalAlerts = 0
|
||||
|
||||
$: mainMenuLinks = [
|
||||
@@ -88,7 +85,7 @@
|
||||
goto('/user/workspaces')
|
||||
}
|
||||
|
||||
const defaultExtraTriggerLinks = [
|
||||
$: defaultExtraTriggerLinks = [
|
||||
{
|
||||
label: 'HTTP',
|
||||
href: '/routes',
|
||||
@@ -292,7 +289,6 @@
|
||||
]
|
||||
|
||||
export let isCollapsed: boolean = false
|
||||
export let noGap: boolean = false
|
||||
|
||||
let leaveWorkspaceModal = false
|
||||
|
||||
@@ -303,15 +299,18 @@
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
const itemClass = twMerge(
|
||||
'text-secondary font-normal w-full block px-4 py-2 text-2xs data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
)
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class={twMerge(
|
||||
'grow flex flex-col overflow-x-hidden scrollbar-hidden px-2 md:pb-2 justify-between',
|
||||
noGap ? 'gap-0' : 'gap-16'
|
||||
'grow flex flex-col overflow-x-hidden scrollbar-hidden px-2 md:pb-2 justify-between gap-2'
|
||||
)}
|
||||
>
|
||||
<div class={twMerge('pt-4 ', noGap ? 'md:mb-0 mb-0' : 'mb-6 md:mb-10')}>
|
||||
<div class={twMerge('pt-4 mb-6 md:mb-10')}>
|
||||
<div class="space-y-1">
|
||||
{#each mainMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
|
||||
<MenuLink class="!text-xs" {...menuLink} {isCollapsed} />
|
||||
@@ -322,220 +321,148 @@
|
||||
class="text-gray-400 text-[0.5rem] uppercase transition-opacity"
|
||||
class:opacity-0={isCollapsed}>Triggers</div
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<Menubar let:createMenu>
|
||||
{#each triggerMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
|
||||
<MenuLink class="!text-xs" {...menuLink} {isCollapsed} />
|
||||
{/each}
|
||||
{#if extraTriggerLinks.length > 0 && !$userStore?.operator}
|
||||
<Menu {createMenu} let:item>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<div
|
||||
slot="trigger"
|
||||
class={twMerge(
|
||||
'w-full text-gray-400 text-2xs flex flex-row gap-1 py-1 items-center px-2 hover:bg-[#2A3648] dark:hover:bg-[#30404e] rounded',
|
||||
'data-[highlighted]:bg-[#2A3648] dark:data-[highlighted]:bg-[#30404e]'
|
||||
)}
|
||||
use:melt={trigger}
|
||||
>
|
||||
<Plus size={14} />
|
||||
<Menubar let:createMenu class="flex flex-col gap-1">
|
||||
{#each triggerMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
|
||||
<MenuLink class="!text-xs" {...menuLink} {isCollapsed} />
|
||||
{/each}
|
||||
{#if extraTriggerLinks.length > 0 && !$userStore?.operator}
|
||||
<Menu {createMenu} let:item usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<div
|
||||
slot="trigger"
|
||||
class={twMerge(
|
||||
'w-full text-gray-400 text-2xs flex flex-row gap-1 py-1 items-center px-2 hover:bg-[#2A3648] dark:hover:bg-[#30404e] rounded',
|
||||
'data-[highlighted]:bg-[#2A3648] dark:data-[highlighted]:bg-[#30404e]'
|
||||
)}
|
||||
use:melt={trigger}
|
||||
>
|
||||
<Plus size={14} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{#each extraTriggerLinks as subItem (subItem.href ?? subItem.label)}
|
||||
<MenuItem
|
||||
href={subItem.disabled ? '' : subItem.href}
|
||||
class={twMerge(itemClass, subItem.disabled ? 'pointer-events-none opacity-50' : '')}
|
||||
{item}
|
||||
disabled={subItem.disabled}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{#if subItem.icon}
|
||||
<svelte:component this={subItem.icon} size={16} />
|
||||
{/if}
|
||||
{subItem.label}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
{#each extraTriggerLinks as subItem (subItem.href ?? subItem.label)}
|
||||
<div>
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href={subItem.disabled ? '' : subItem.href}
|
||||
class={twMerge(
|
||||
'text-secondary block px-4 py-2 text-2xs hover:bg-surface-hover hover:text-primary',
|
||||
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary',
|
||||
subItem.disabled ? 'pointer-events-none opacity-50' : ''
|
||||
)}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
use:melt={item}
|
||||
data-disabled={subItem.disabled ? true : undefined}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{#if subItem.icon}
|
||||
<svelte:component this={subItem.icon} size={16} />
|
||||
{/if}
|
||||
{subItem.label}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</Menu>
|
||||
{/if}
|
||||
</Menubar>
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</Menu>
|
||||
{/if}
|
||||
</Menubar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col h-full justify-end">
|
||||
<div class={twMerge('space-y-0.5 mb-6 md:mb-10', noGap ? 'md:mb-0 mb-0' : 'mb-6 md:mb-10')}>
|
||||
<Menubar let:createMenu>
|
||||
<UserMenu {isCollapsed} {createMenu} />
|
||||
{#each secondaryMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
|
||||
{#if menuLink.subItems}
|
||||
{@const notificationsCount = computeAllNotificationsCount(menuLink.subItems)}
|
||||
<Menu {createMenu} let:item>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<MenuButtonMelt
|
||||
class="!text-2xs"
|
||||
{...menuLink}
|
||||
{isCollapsed}
|
||||
{notificationsCount}
|
||||
{trigger}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<div>
|
||||
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
|
||||
<div class="py-1" role="none">
|
||||
{#if subItem?.['action']}
|
||||
<button
|
||||
class="text-secondary font-normal w-full block px-4 py-2 text-2xs data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary"
|
||||
on:click={subItem?.['action']}
|
||||
use:melt={item}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{#if subItem.icon}
|
||||
<svelte:component this={subItem.icon} size={16} />
|
||||
{/if}
|
||||
<Menubar let:createMenu class="flex flex-col gap-1 mb-6 md:mb-10">
|
||||
<UserMenu {isCollapsed} {createMenu} />
|
||||
|
||||
{subItem.label}
|
||||
{#if subItem?.['notificationCount']}
|
||||
<div class="ml-auto">
|
||||
<SideBarNotification
|
||||
notificationCount={subItem['notificationCount']}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
{:else}
|
||||
<a
|
||||
href={subItem.href}
|
||||
class="text-secondary font-normal block px-4 py-2 text-2xs data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
use:melt={item}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{#if subItem.icon}
|
||||
<svelte:component this={subItem.icon} size={16} />
|
||||
{/if}
|
||||
{#each secondaryMenuLinks as menuLink (menuLink.href ?? menuLink.label)}
|
||||
{#if menuLink.subItems}
|
||||
{@const notificationsCount = computeAllNotificationsCount(menuLink.subItems)}
|
||||
<Menu {createMenu} let:item usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<MenuButton
|
||||
class="!text-2xs"
|
||||
{...menuLink}
|
||||
{isCollapsed}
|
||||
{notificationsCount}
|
||||
{trigger}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
|
||||
{subItem.label}
|
||||
|
||||
{#if subItem?.['notificationCount']}
|
||||
<div class="ml-auto">
|
||||
<SideBarNotification
|
||||
notificationCount={subItem['notificationCount']}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Menu>
|
||||
{:else}
|
||||
<div>
|
||||
<MenuSingleItem {createMenu} let:item>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<div class="w-full">
|
||||
<MenuButtonMelt
|
||||
class="!text-2xs"
|
||||
{...menuLink}
|
||||
{isCollapsed}
|
||||
{trigger}
|
||||
openHref={true}
|
||||
/>
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<MenuLinkMelt class="!text-2xs" {...menuLink} {isCollapsed} {item} />
|
||||
</MenuSingleItem>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</Menubar>
|
||||
</div>
|
||||
<div class="space-y-0.5">
|
||||
<Menubar let:createMenu>
|
||||
{#each thirdMenuLinks as menuLink (menuLink)}
|
||||
{#if menuLink.subItems}
|
||||
<Menu {createMenu} let:item>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<button
|
||||
class="relative w-full"
|
||||
on:click={() => {
|
||||
if (menuLink.label === 'Help') {
|
||||
openChangelogs()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuButtonMelt class="!text-2xs" {...menuLink} {isCollapsed} {trigger} />
|
||||
{#if menuLink.label === 'Help' && hasNewChangelogs}
|
||||
<span class="absolute top-1 right-1 flex h-2 w-2">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-frost-400 opacity-75"
|
||||
/>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-frost-500" />
|
||||
</span>
|
||||
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
|
||||
<MenuItem
|
||||
class={itemClass}
|
||||
href={subItem.href}
|
||||
{item}
|
||||
on:click={() => {
|
||||
subItem?.['action']?.()
|
||||
}}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{#if subItem.icon}
|
||||
<svelte:component this={subItem.icon} size={16} />
|
||||
{/if}
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href={subItem.href}
|
||||
class={twMerge(
|
||||
'text-secondary block px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary relative',
|
||||
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
)}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
use:melt={item}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{#if subItem.icon}
|
||||
<svelte:component this={subItem.icon} size={16} />
|
||||
{/if}
|
||||
|
||||
{subItem.label}
|
||||
{subItem.label}
|
||||
{#if subItem?.['notificationCount']}
|
||||
<div class="ml-auto">
|
||||
<SideBarNotification notificationCount={subItem['notificationCount']} />
|
||||
</div>
|
||||
</a>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
{#if recentChangelogs.length > 0}
|
||||
<div class="w-full h-1 border-t" />
|
||||
<span class="text-xs px-4 font-bold"> Latest changelogs </span>
|
||||
{#each recentChangelogs as changelog}
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href={changelog.href}
|
||||
class="text-secondary block px-4 py-2 text-xs hover:bg-surface-hover hover:text-primary relative"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
target="_blank"
|
||||
use:melt={item}
|
||||
>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{changelog.label}
|
||||
</div>
|
||||
</a>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
</Menu>
|
||||
{:else}
|
||||
<MenuSingleItem {createMenu} let:item>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<div class="w-full">
|
||||
<MenuButton class="!text-2xs" {...menuLink} {isCollapsed} {trigger} />
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
<MenuLink class="!text-2xs" {...menuLink} {isCollapsed} {item} />
|
||||
</MenuSingleItem>
|
||||
{/if}
|
||||
{/each}
|
||||
</Menubar>
|
||||
|
||||
<Menubar let:createMenu class="flex flex-col gap-1">
|
||||
{#each thirdMenuLinks as menuLink (menuLink)}
|
||||
{#if menuLink.subItems}
|
||||
<Menu {createMenu} let:item usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<button
|
||||
class="relative w-full"
|
||||
on:click={() => {
|
||||
if (menuLink.label === 'Help') {
|
||||
openChangelogs()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<MenuButton class="!text-2xs" {...menuLink} {isCollapsed} {trigger} />
|
||||
{#if menuLink.label === 'Help' && hasNewChangelogs}
|
||||
<span class="absolute top-1 right-1 flex h-2 w-2">
|
||||
<span
|
||||
class="animate-ping absolute inline-flex h-full w-full rounded-full bg-frost-400 opacity-75"
|
||||
/>
|
||||
<span class="relative inline-flex rounded-full h-2 w-2 bg-frost-500" />
|
||||
</span>
|
||||
{/if}
|
||||
</button>
|
||||
</svelte:fragment>
|
||||
{#each menuLink.subItems as subItem (subItem.href ?? subItem.label)}
|
||||
<MenuItem href={subItem.href} class={itemClass} target="_blank" {item}>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{#if subItem.icon}
|
||||
<svelte:component this={subItem.icon} size={16} />
|
||||
{/if}
|
||||
|
||||
{subItem.label}
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
{#if recentChangelogs.length > 0}
|
||||
<div class="w-full h-1 border-t" />
|
||||
<span class="text-xs px-4 font-bold"> Latest changelogs </span>
|
||||
{#each recentChangelogs as changelog}
|
||||
<MenuItem href={changelog.href} class={itemClass} target="_blank" {item}>
|
||||
<div class="flex flex-row items-center gap-2">
|
||||
{changelog.label}
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</Menu>
|
||||
{/if}
|
||||
{/each}
|
||||
</Menubar>
|
||||
</div>
|
||||
</MenuItem>
|
||||
{/each}
|
||||
{/if}
|
||||
</Menu>
|
||||
{/if}
|
||||
{/each}
|
||||
</Menubar>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
@@ -13,19 +13,25 @@
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import { Crown, HardHat, LogOut, Moon, Settings, Sun, User } from 'lucide-svelte'
|
||||
import DarkModeObserver from '../DarkModeObserver.svelte'
|
||||
import MenuButtonMelt from './MenuButtonMelt.svelte'
|
||||
import MenuButton from './MenuButton.svelte'
|
||||
import { Menu, MenuItem } from '$lib/components/meltComponents'
|
||||
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
import Menu from '$lib/components/meltComponents/Menu.svelte'
|
||||
let darkMode: boolean = false
|
||||
|
||||
export let isCollapsed: boolean = false
|
||||
export let lightMode: boolean = false
|
||||
export let createMenu: (any) => any
|
||||
|
||||
const itemClass = twMerge(
|
||||
'text-secondary text-left font-normal text-xs ',
|
||||
'flex flex-row items-center gap-2 px-4 py-3 w-full',
|
||||
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
)
|
||||
</script>
|
||||
|
||||
<Menu {createMenu} let:item>
|
||||
<Menu {createMenu} let:item usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<MenuButtonMelt
|
||||
<MenuButton
|
||||
class="!text-xs"
|
||||
icon={User}
|
||||
label={`User (${$userStore?.username ?? $userStore?.email})`}
|
||||
@@ -34,130 +40,104 @@
|
||||
{trigger}
|
||||
/>
|
||||
</svelte:fragment>
|
||||
<div class="divide-y z-20">
|
||||
<div class="px-4 py-3" role="none">
|
||||
<p class="text-sm font-medium text-primary truncate" role="none">
|
||||
{$userStore?.email}
|
||||
</p>
|
||||
<span class="text-xs text-tertiary flex flex-row gap-2 items-center">
|
||||
{#if $userStore?.is_admin}
|
||||
Admin of this workspace <Crown size={14} />
|
||||
{:else if $userStore?.operator}
|
||||
Operator in this workspace <HardHat size={14} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="py-1" role="none">
|
||||
<a
|
||||
href={USER_SETTINGS_HASH}
|
||||
class={twMerge(
|
||||
'flex flex-row gap-2 items-center px-4 py-2 ',
|
||||
'text-secondary text-sm',
|
||||
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
)}
|
||||
use:melt={item}
|
||||
>
|
||||
<Settings size={14} />
|
||||
Account settings
|
||||
</a>
|
||||
</div>
|
||||
<div class="px-4 py-3 border-b" role="none">
|
||||
<p class="text-sm font-medium text-primary truncate" role="none">
|
||||
{$userStore?.email}
|
||||
</p>
|
||||
<span class="text-xs text-tertiary flex flex-row gap-2 items-center">
|
||||
{#if $userStore?.is_admin}
|
||||
Admin of this workspace <Crown size={14} />
|
||||
{:else if $userStore?.operator}
|
||||
Operator in this workspace <HardHat size={14} />
|
||||
{/if}
|
||||
</span>
|
||||
</div>
|
||||
<div class="py-1">
|
||||
<MenuItem href={USER_SETTINGS_HASH} class={itemClass} {item}>
|
||||
<Settings size={16} />
|
||||
Account settings
|
||||
</MenuItem>
|
||||
|
||||
<div class="py-1" role="none">
|
||||
<button
|
||||
on:click={() => {
|
||||
if (!document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.add('dark')
|
||||
window.localStorage.setItem('dark-mode', 'dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
window.localStorage.setItem('dark-mode', 'light')
|
||||
}
|
||||
}}
|
||||
class={twMerge(
|
||||
'text-secondary block text-left px-4 py-2 font-normal text-sm w-full',
|
||||
'flex flex-row items-center gap-2',
|
||||
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
)}
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
use:melt={item}
|
||||
>
|
||||
{#if darkMode}
|
||||
<Sun size={14} />
|
||||
{:else}
|
||||
<Moon size={14} />
|
||||
{/if}
|
||||
Switch theme
|
||||
</button>
|
||||
</div>
|
||||
<MenuItem
|
||||
on:click={() => {
|
||||
if (!document.documentElement.classList.contains('dark')) {
|
||||
document.documentElement.classList.add('dark')
|
||||
window.localStorage.setItem('dark-mode', 'dark')
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark')
|
||||
window.localStorage.setItem('dark-mode', 'light')
|
||||
}
|
||||
}}
|
||||
class={itemClass}
|
||||
{item}
|
||||
>
|
||||
{#if darkMode}
|
||||
<Sun size={16} />
|
||||
{:else}
|
||||
<Moon size={16} />
|
||||
{/if}
|
||||
Switch theme
|
||||
</MenuItem>
|
||||
|
||||
<div class="py-1" role="none">
|
||||
<button
|
||||
on:click={() => logout()}
|
||||
class={twMerge(
|
||||
'flex flex-row gap-2 items-center px-4 py-2 w-full',
|
||||
'text-secondary text-sm',
|
||||
'data-[highlighted]:bg-surface-hover data-[highlighted]:text-primary'
|
||||
)}
|
||||
use:melt={item}
|
||||
>
|
||||
<LogOut size={14} />
|
||||
Sign out
|
||||
</button>
|
||||
</div>
|
||||
<MenuItem
|
||||
on:click={() => logout()}
|
||||
class={twMerge(itemClass, 'text-primary font-semibold')}
|
||||
{item}
|
||||
>
|
||||
<LogOut size={16} />
|
||||
Sign out
|
||||
</MenuItem>
|
||||
</div>
|
||||
|
||||
{#if isCloudHosted()}
|
||||
{#if isCloudHosted()}
|
||||
<div class="border-t">
|
||||
{#if !$isPremiumStore}
|
||||
<div class="py-1" role="none">
|
||||
<span class="text-secondary block w-full text-left px-4 py-2 text-sm"
|
||||
>{$usageStore}/1000 user execs</span
|
||||
>
|
||||
<div class="w-full bg-gray-200 h-1">
|
||||
<div class="bg-blue-400 h-1" style="width: {Math.min($usageStore, 1000) / 10}%" />
|
||||
<span class="text-secondary block w-full text-left px-4 py-2 text-xs"
|
||||
>{$usageStore}/1000 user execs</span
|
||||
>
|
||||
<div class="px-4 w-full h-1 mb-1">
|
||||
<div class="bg-gray-200 h-full rounded-sm overflow-hidden">
|
||||
<div class="bg-blue-400 h-full" style="width: {Math.min($usageStore, 1000) / 10}%" />
|
||||
</div>
|
||||
{#if $workspaceStore != 'demo'}
|
||||
<span class="text-secondary block w-full text-left px-4 py-2 text-xs"
|
||||
>{$workspaceUsageStore}/1000 free workspace execs</span
|
||||
>
|
||||
<div class="w-full bg-gray-200 h-1">
|
||||
</div>
|
||||
{#if $workspaceStore != 'demo'}
|
||||
<span class="text-secondary block w-full text-left px-4 py-2 text-xs"
|
||||
>{$workspaceUsageStore}/1000 free workspace execs</span
|
||||
>
|
||||
<div class="px-4 w-full h-1 mb-1">
|
||||
<div class="bg-gray-200 h-full rounded-sm overflow-hidden">
|
||||
<div
|
||||
class="bg-blue-400 h-1"
|
||||
class="bg-blue-400 h-full"
|
||||
style="width: {Math.min($workspaceUsageStore, 1000) / 10}%"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $userStore?.is_admin}
|
||||
<button
|
||||
type="button"
|
||||
class="text-secondary block font-normal w-full text-left px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
on:click={() => {
|
||||
goto('/workspace_settings?tab=premium')
|
||||
}}
|
||||
>
|
||||
Upgrade
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
<div class="py-1" role="none">
|
||||
<button
|
||||
type="button"
|
||||
class="text-secondary block font-normal w-full text-left px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900"
|
||||
role="menuitem"
|
||||
tabindex="-1"
|
||||
</div>
|
||||
{/if}
|
||||
{#if $userStore?.is_admin}
|
||||
<MenuItem
|
||||
class={twMerge(itemClass, 'py-2')}
|
||||
on:click={() => {
|
||||
goto('/workspace_settings?tab=premium')
|
||||
}}
|
||||
{item}
|
||||
>
|
||||
Premium plan
|
||||
</button>
|
||||
</div>
|
||||
Upgrade
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{:else}
|
||||
<MenuItem
|
||||
class={twMerge(itemClass, 'py-2')}
|
||||
on:click={() => {
|
||||
goto('/workspace_settings?tab=premium')
|
||||
}}
|
||||
{item}
|
||||
>
|
||||
Premium plan
|
||||
</MenuItem>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Menu>
|
||||
|
||||
<DarkModeObserver bind:darkMode />
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
workspaceUsageStore
|
||||
} from '$lib/stores'
|
||||
import { Building, Plus, Settings } from 'lucide-svelte'
|
||||
import MenuButtonMelt from '$lib/components/sidebar/MenuButtonMelt.svelte'
|
||||
import Menu from '$lib/components/meltComponents/Menu.svelte'
|
||||
import MenuButton from '$lib/components/sidebar/MenuButton.svelte'
|
||||
import { Menu } from '$lib/components/meltComponents'
|
||||
import { melt } from '@melt-ui/svelte'
|
||||
import { goto } from '$lib/navigation'
|
||||
import { base } from '$lib/base'
|
||||
@@ -18,7 +18,7 @@
|
||||
import MultiplayerMenu from './MultiplayerMenu.svelte'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { initAllAiWorkspace } from '../copilot/lib'
|
||||
import { workspaceAIClients } from '../copilot/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
|
||||
export let isCollapsed: boolean = false
|
||||
@@ -31,7 +31,7 @@
|
||||
if ($workspaceStore === id) {
|
||||
return
|
||||
}
|
||||
initAllAiWorkspace(id, true)
|
||||
workspaceAIClients.init(id)
|
||||
const editPages = [
|
||||
'/scripts/edit/',
|
||||
'/flows/edit/',
|
||||
@@ -54,9 +54,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Menu {createMenu} let:item>
|
||||
<Menu {createMenu} let:item usePointerDownOutside>
|
||||
<svelte:fragment slot="trigger" let:trigger>
|
||||
<MenuButtonMelt
|
||||
<MenuButton
|
||||
class="!text-xs"
|
||||
icon={Building}
|
||||
label={$workspaceStore ?? ''}
|
||||
|
||||
@@ -200,8 +200,6 @@
|
||||
/>
|
||||
{:else if captureType === 'http'}
|
||||
<RouteEditorConfigSection
|
||||
{isFlow}
|
||||
{path}
|
||||
{showCapture}
|
||||
can_write={true}
|
||||
runnableArgs={data?.args}
|
||||
|
||||
@@ -21,23 +21,26 @@
|
||||
export let canHavePreprocessor: boolean = false
|
||||
export let hasPreprocessor: boolean = false
|
||||
export let newItem: boolean
|
||||
export let openForm: boolean = false
|
||||
export let alwaysOpened: boolean = false
|
||||
export let showCapture: boolean = false
|
||||
|
||||
const captureTypeLabels: Record<CaptureTriggerKind, string> = {
|
||||
http: 'New custom HTTP route',
|
||||
websocket: 'New WebSocket trigger',
|
||||
http: '+ New custom HTTP route',
|
||||
websocket: '+ New WebSocket trigger',
|
||||
webhook: 'Webhook',
|
||||
kafka: 'New Kafka trigger',
|
||||
kafka: '+ New Kafka trigger',
|
||||
email: 'Email trigger',
|
||||
nats: 'NATS trigger'
|
||||
nats: '+ New NATS trigger'
|
||||
}
|
||||
|
||||
const { captureOn } = getContext<TriggerContext>('TriggerContext')
|
||||
|
||||
let args: Record<string, any> = {}
|
||||
$: collapsed = !openForm
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
let showCapture = false
|
||||
let init = false
|
||||
$: updateShowCapture(!!$captureOn)
|
||||
function updateShowCapture(show: boolean) {
|
||||
@@ -49,55 +52,57 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<Section label={captureTypeLabels[triggerType]}>
|
||||
<Section label={captureTypeLabels[triggerType]} collapsable={!alwaysOpened} bind:collapsed>
|
||||
<svelte:fragment slot="action">
|
||||
<div class="flex flex-row grow w-min-0 gap-2 items-center justify-end">
|
||||
{#if isEditor}
|
||||
<Button
|
||||
size="xs2"
|
||||
on:click={() => {
|
||||
showCapture = !showCapture
|
||||
}}
|
||||
variant="border"
|
||||
color="light"
|
||||
endIcon={{
|
||||
icon: ChevronDown,
|
||||
classes: twMerge('transition', showCapture ? 'rotate-180' : '')
|
||||
}}
|
||||
>
|
||||
Test trigger
|
||||
</Button>
|
||||
{/if}
|
||||
|
||||
{#if !noSave}
|
||||
{@const disabled = newItem || cloudDisabled}
|
||||
<Popover notClickable>
|
||||
{#if !collapsed || alwaysOpened}
|
||||
<div class="flex flex-row grow w-min-0 gap-2 items-center justify-end">
|
||||
{#if isEditor}
|
||||
<Button
|
||||
size="xs2"
|
||||
{disabled}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={() => {
|
||||
dispatch('saveTrigger', {
|
||||
config: args
|
||||
})
|
||||
showCapture = !showCapture
|
||||
}}
|
||||
variant="border"
|
||||
color="light"
|
||||
endIcon={{
|
||||
icon: ChevronDown,
|
||||
classes: twMerge('transition', showCapture ? 'rotate-180' : '')
|
||||
}}
|
||||
>
|
||||
Save
|
||||
Test trigger
|
||||
</Button>
|
||||
<svelte:fragment slot="text">
|
||||
{#if disabled}
|
||||
{#if newItem}
|
||||
Deploy the runnable to enable trigger creation
|
||||
{:else if cloudDisabled}
|
||||
{capitalize(triggerType)} triggers are disabled in the multi-tenant cloud
|
||||
{/if}
|
||||
|
||||
{#if !noSave}
|
||||
{@const disabled = newItem || cloudDisabled}
|
||||
<Popover notClickable>
|
||||
<Button
|
||||
size="xs2"
|
||||
{disabled}
|
||||
startIcon={{ icon: Save }}
|
||||
on:click={() => {
|
||||
dispatch('saveTrigger', {
|
||||
config: args
|
||||
})
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<svelte:fragment slot="text">
|
||||
{#if disabled}
|
||||
{#if newItem}
|
||||
Deploy the runnable to enable trigger creation
|
||||
{:else if cloudDisabled}
|
||||
{capitalize(triggerType)} triggers are disabled in the multi-tenant cloud
|
||||
{/if}
|
||||
{:else}
|
||||
Create new {captureTypeLabels[triggerType].toLowerCase()}
|
||||
{/if}
|
||||
{:else}
|
||||
Create new {captureTypeLabels[triggerType].toLowerCase()}
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
</svelte:fragment>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
{#if isEditor}
|
||||
|
||||
@@ -47,8 +47,6 @@
|
||||
bind:route_path={args.route_path}
|
||||
bind:http_method={args.http_method}
|
||||
headless
|
||||
{isFlow}
|
||||
{path}
|
||||
/>
|
||||
{:else if triggerType === 'email'}
|
||||
<EmailTriggerConfigSection
|
||||
|
||||
@@ -10,15 +10,14 @@
|
||||
import { bash } from 'svelte-highlight/languages'
|
||||
import { HttpTriggerService } from '$lib/gen'
|
||||
// import { page } from '$app/stores'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { base } from '$lib/base'
|
||||
import type { CaptureInfo } from '../CaptureSection.svelte'
|
||||
import CaptureSection from '../CaptureSection.svelte'
|
||||
import CaptureTable from '../CaptureTable.svelte'
|
||||
import ClipboardPanel from '../../details/ClipboardPanel.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
|
||||
export let isFlow: boolean
|
||||
export let path: string
|
||||
export let initialTriggerPath: string | undefined = undefined
|
||||
export let dirtyRoutePath: boolean = false
|
||||
export let route_path: string | undefined
|
||||
export let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' | undefined
|
||||
@@ -26,7 +25,6 @@
|
||||
export let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
|
||||
undefined
|
||||
export let showCapture = false
|
||||
export let initialRoutePath: string = ''
|
||||
export let headless: boolean = false
|
||||
export let captureInfo: CaptureInfo | undefined = undefined
|
||||
export let captureTable: CaptureTable | undefined = undefined
|
||||
@@ -36,16 +34,16 @@
|
||||
|
||||
let routeError: string = ''
|
||||
async function validateRoute(
|
||||
path: string | undefined,
|
||||
routePath: string | undefined,
|
||||
method: typeof http_method
|
||||
): Promise<void> {
|
||||
if (validateTimeout) {
|
||||
clearTimeout(validateTimeout)
|
||||
}
|
||||
validateTimeout = setTimeout(async () => {
|
||||
if (!path || !method || !/^[\w-:]+(\/[\w-:]+)*$/.test(path)) {
|
||||
if (!routePath || !method || !/^:?[-\w]+(\/:?[-\w]+)*$/.test(routePath)) {
|
||||
routeError = 'Endpoint not valid'
|
||||
} else if (initialRoutePath !== path && (await routeExists(path, method))) {
|
||||
} else if (await routeExists(routePath, method)) {
|
||||
routeError = 'Endpoint already taken'
|
||||
} else {
|
||||
routeError = ''
|
||||
@@ -59,14 +57,15 @@
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
route_path,
|
||||
http_method: method
|
||||
http_method: method,
|
||||
trigger_path: initialTriggerPath
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
$: captureURL = `${location.origin}${base}/api/w/${$workspaceStore}/capture_u/http/${
|
||||
isFlow ? 'flow' : 'script'
|
||||
}/${path.replaceAll('/', '.')}/${route_path}`
|
||||
$: validateRoute(route_path, http_method)
|
||||
|
||||
$: isValid = routeError === ''
|
||||
|
||||
function getHttpRoute(route_path: string | undefined) {
|
||||
return `${location.origin}${base}/api/r/${isCloudHosted() ? $workspaceStore + '/' : ''}${
|
||||
@@ -74,10 +73,6 @@
|
||||
}`
|
||||
}
|
||||
|
||||
$: validateRoute(route_path, http_method)
|
||||
|
||||
$: isValid = routeError === ''
|
||||
|
||||
$: fullRoute = getHttpRoute(route_path)
|
||||
|
||||
$: !http_method && (http_method = 'post')
|
||||
@@ -86,6 +81,9 @@
|
||||
|
||||
<div>
|
||||
{#if showCapture && captureInfo}
|
||||
{@const captureURL = `${location.origin}${base}/api/w/${$workspaceStore}/capture_u/http/${
|
||||
captureInfo.isFlow ? 'flow' : 'script'
|
||||
}/${captureInfo.path.replaceAll('/', '.')}/${route_path}`}
|
||||
<CaptureSection
|
||||
captureType="http"
|
||||
disabled={!isValid}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import RouteEditorConfigSection from './RouteEditorConfigSection.svelte'
|
||||
import SimpleEditor from '$lib/components/SimpleEditor.svelte'
|
||||
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
let is_flow: boolean = false
|
||||
let initialPath = ''
|
||||
let edit = true
|
||||
@@ -66,7 +66,6 @@
|
||||
itemKind = nis_flow ? 'flow' : 'script'
|
||||
is_async = false
|
||||
requires_auth = false
|
||||
initialRoutePath = ''
|
||||
route_path = defaultValues?.route_path ?? ''
|
||||
dirtyRoutePath = false
|
||||
http_method = defaultValues?.http_method ?? 'post'
|
||||
@@ -78,6 +77,7 @@
|
||||
path = ''
|
||||
initialPath = ''
|
||||
dirtyPath = false
|
||||
is_static_website = false
|
||||
} finally {
|
||||
drawerLoading = false
|
||||
}
|
||||
@@ -89,11 +89,11 @@
|
||||
let dirtyRoutePath = false
|
||||
let is_async = false
|
||||
let requires_auth = false
|
||||
let initialRoutePath = ''
|
||||
let route_path = ''
|
||||
let http_method: 'get' | 'post' | 'put' | 'patch' | 'delete' = 'post'
|
||||
let static_asset_config: { s3: string; storage?: string; filename?: string } | undefined =
|
||||
undefined
|
||||
let is_static_website = false
|
||||
|
||||
let s3FilePicker: S3FilePicker
|
||||
let s3FileUploadRawMode = false
|
||||
@@ -113,12 +113,14 @@
|
||||
is_flow = s.is_flow
|
||||
path = s.path
|
||||
route_path = s.route_path
|
||||
initialRoutePath = s.route_path
|
||||
http_method = s.http_method ?? 'post'
|
||||
is_async = s.is_async
|
||||
requires_auth = s.requires_auth
|
||||
static_asset_config = s.static_asset_config
|
||||
s3FileUploadRawMode = !!static_asset_config
|
||||
if (!isCloudHosted()) {
|
||||
static_asset_config = s.static_asset_config
|
||||
s3FileUploadRawMode = !!static_asset_config
|
||||
is_static_website = s.is_static_website
|
||||
}
|
||||
|
||||
can_write = canWrite(s.path, s.extra_perms, $userStore)
|
||||
}
|
||||
@@ -136,7 +138,8 @@
|
||||
requires_auth,
|
||||
route_path: $userStore?.is_admin || $userStore?.is_super_admin ? route_path : undefined,
|
||||
http_method,
|
||||
static_asset_config
|
||||
static_asset_config,
|
||||
is_static_website
|
||||
}
|
||||
})
|
||||
sendUserToast(`Route ${path} updated`)
|
||||
@@ -151,7 +154,8 @@
|
||||
requires_auth,
|
||||
route_path,
|
||||
http_method,
|
||||
static_asset_config
|
||||
static_asset_config,
|
||||
is_static_website
|
||||
}
|
||||
})
|
||||
sendUserToast(`Route ${path} created`)
|
||||
@@ -166,11 +170,14 @@
|
||||
let drawer: Drawer
|
||||
|
||||
let dirtyPath = false
|
||||
|
||||
$: !static_asset_config && (is_static_website = false)
|
||||
</script>
|
||||
|
||||
{#if static_asset_config}
|
||||
<S3FilePicker
|
||||
bind:this={s3FilePicker}
|
||||
folderOnly={is_static_website}
|
||||
bind:selectedFileKey={static_asset_config}
|
||||
on:close={() => {
|
||||
s3Editor?.setCode(JSON.stringify(static_asset_config, null, 2))
|
||||
@@ -220,102 +227,125 @@
|
||||
</div>
|
||||
|
||||
<RouteEditorConfigSection
|
||||
isFlow={is_flow}
|
||||
{path}
|
||||
initialTriggerPath={initialPath}
|
||||
bind:route_path
|
||||
bind:isValid
|
||||
bind:dirtyRoutePath
|
||||
bind:http_method
|
||||
{can_write}
|
||||
bind:static_asset_config
|
||||
{initialRoutePath}
|
||||
/>
|
||||
|
||||
<Section label="Target">
|
||||
<ToggleButtonGroup
|
||||
disabled={fixedScriptPath != '' || !can_write}
|
||||
selected={static_asset_config ? 'static_asset' : 'runnable'}
|
||||
on:selected={(ev) => {
|
||||
if (ev.detail === 'static_asset') {
|
||||
static_asset_config = { s3: '' }
|
||||
script_path = ''
|
||||
initialScriptPath = ''
|
||||
is_flow = false
|
||||
http_method = 'get'
|
||||
} else {
|
||||
static_asset_config = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton label="Runnable" value="runnable" />
|
||||
<ToggleButton label="Static asset" value="static_asset" />
|
||||
</ToggleButtonGroup>
|
||||
{#if !isCloudHosted()}
|
||||
<ToggleButtonGroup
|
||||
disabled={fixedScriptPath != '' || !can_write}
|
||||
selected={static_asset_config
|
||||
? is_static_website
|
||||
? 'static_website'
|
||||
: 'static_asset'
|
||||
: 'runnable'}
|
||||
on:selected={(ev) => {
|
||||
if (ev.detail === 'static_asset' || ev.detail === 'static_website') {
|
||||
static_asset_config = { s3: '' }
|
||||
s3Editor?.setCode(JSON.stringify(static_asset_config, null, 2))
|
||||
script_path = ''
|
||||
initialScriptPath = ''
|
||||
is_flow = false
|
||||
http_method = 'get'
|
||||
is_async = false
|
||||
is_static_website = ev.detail === 'static_website'
|
||||
if (is_static_website) {
|
||||
requires_auth = false
|
||||
}
|
||||
} else if (ev.detail === 'runnable') {
|
||||
static_asset_config = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton label="Runnable" value="runnable" />
|
||||
<ToggleButton label="Static asset" value="static_asset" />
|
||||
<ToggleButton label="Static website" value="static_website" />
|
||||
</ToggleButtonGroup>
|
||||
{/if}
|
||||
|
||||
{#if static_asset_config}
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
{#if can_write}
|
||||
<Toggle
|
||||
class="flex justify-end"
|
||||
bind:checked={s3FileUploadRawMode}
|
||||
size="xs"
|
||||
options={{ left: 'Existing file' }}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{/if}
|
||||
{#if s3FileUploadRawMode}
|
||||
{#if is_static_website}
|
||||
<p class="text-xs my-1 text-tertiary">
|
||||
Upload or specify a <b>folder</b> on S3. All its files will be served under the path
|
||||
above. Use this full path as the base URL of your website to ensure relative imports
|
||||
work correctly.
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col w-full gap-1">
|
||||
{#if can_write}
|
||||
<JsonEditor
|
||||
bind:editor={s3Editor}
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
|
||||
bind:value={static_asset_config}
|
||||
/>
|
||||
{:else}
|
||||
<Highlight
|
||||
language={json}
|
||||
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
|
||||
/>
|
||||
{/if}
|
||||
{#if can_write}
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
<Toggle
|
||||
class="flex justify-end"
|
||||
bind:checked={s3FileUploadRawMode}
|
||||
size="xs"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
s3FilePicker?.open?.(static_asset_config)
|
||||
}}
|
||||
startIcon={{ icon: Pipette }}
|
||||
>
|
||||
Choose an object from the catalog
|
||||
</Button>
|
||||
options={{ left: 'Existing file' }}
|
||||
disabled={!can_write}
|
||||
/>
|
||||
{/if}
|
||||
{:else}
|
||||
<FileUpload
|
||||
allowMultiple={false}
|
||||
randomFileKey={true}
|
||||
on:addition={(evt) => {
|
||||
static_asset_config = {
|
||||
s3: evt.detail?.path ?? '',
|
||||
filename: evt.detail?.filename ?? undefined
|
||||
}
|
||||
s3FileUploadRawMode = true
|
||||
}}
|
||||
on:deletion={(evt) => {
|
||||
static_asset_config = {
|
||||
s3: ''
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{#if s3FileUploadRawMode}
|
||||
{#if can_write}
|
||||
<JsonEditor
|
||||
bind:editor={s3Editor}
|
||||
on:focus={(e) => {
|
||||
dispatch('focus')
|
||||
}}
|
||||
on:blur={(e) => {
|
||||
dispatch('blur')
|
||||
}}
|
||||
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
|
||||
bind:value={static_asset_config}
|
||||
/>
|
||||
{:else}
|
||||
<Highlight
|
||||
language={json}
|
||||
code={JSON.stringify(static_asset_config ?? { s3: '' }, null, 2)}
|
||||
/>
|
||||
{/if}
|
||||
{#if can_write}
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
size="xs"
|
||||
btnClasses="mt-1"
|
||||
on:click={() => {
|
||||
s3FilePicker?.open?.(!is_static_website ? static_asset_config : undefined)
|
||||
}}
|
||||
startIcon={{ icon: Pipette }}
|
||||
>
|
||||
Choose an object from the catalog
|
||||
</Button>
|
||||
{/if}
|
||||
{:else}
|
||||
{#key is_static_website}
|
||||
<FileUpload
|
||||
folderOnly={is_static_website}
|
||||
allowMultiple={false}
|
||||
randomFileKey={true}
|
||||
on:addition={(evt) => {
|
||||
static_asset_config = {
|
||||
s3: evt.detail?.path ?? '',
|
||||
filename: evt.detail?.filename ?? undefined
|
||||
}
|
||||
s3FileUploadRawMode = true
|
||||
}}
|
||||
on:deletion={(evt) => {
|
||||
static_asset_config = {
|
||||
s3: ''
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-xs mb-1 text-tertiary">
|
||||
<p class="text-xs mt-0.5 mb-1 text-tertiary">
|
||||
Pick a script or flow to be triggered<Required required={true} /><br />
|
||||
To handle headers, query or path parameters, add a preprocessor to your runnable.
|
||||
</p>
|
||||
@@ -346,48 +376,52 @@
|
||||
{/if}
|
||||
</Section>
|
||||
|
||||
<Section label="Advanced">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row justify-between">
|
||||
<Label label="Request type" class="w-full">
|
||||
{#if !is_static_website}
|
||||
<Section label="Advanced">
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if !static_asset_config}
|
||||
<div class="flex flex-row justify-between">
|
||||
<Label label="Request type" class="w-full">
|
||||
<svelte:fragment slot="action">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
bind:selected={is_async}
|
||||
disabled={!can_write || !!static_asset_config}
|
||||
>
|
||||
<ToggleButton
|
||||
label="Async"
|
||||
value={true}
|
||||
tooltip="The returning value is the uuid of the job assigned to execute the job."
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Sync"
|
||||
value={false}
|
||||
tooltip="Triggers the execution, wait for the job to complete and return it as a response."
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
</div>
|
||||
{/if}
|
||||
<Label label="Authentication" class="w-full">
|
||||
<svelte:fragment slot="action">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
bind:selected={is_async}
|
||||
bind:selected={requires_auth}
|
||||
disabled={!can_write}
|
||||
>
|
||||
<ToggleButton label="None" value={false} />
|
||||
<ToggleButton
|
||||
label="Async"
|
||||
label="Required"
|
||||
value={true}
|
||||
tooltip="The returning value is the uuid of the job assigned to execute the job."
|
||||
/>
|
||||
<ToggleButton
|
||||
label="Sync"
|
||||
value={false}
|
||||
tooltip="Triggers the execution, wait for the job to complete and return it as a response."
|
||||
tooltip="Requires authentication with read access on the route"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
</div>
|
||||
<Label label="Authentication" class="w-full">
|
||||
<svelte:fragment slot="action">
|
||||
<ToggleButtonGroup
|
||||
class="w-auto h-full"
|
||||
bind:selected={requires_auth}
|
||||
disabled={!can_write}
|
||||
>
|
||||
<ToggleButton label="None" value={false} />
|
||||
<ToggleButton
|
||||
label="Required"
|
||||
value={true}
|
||||
tooltip="Requires authentication with read access on the route"
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</svelte:fragment>
|
||||
</Label>
|
||||
</div>
|
||||
</Section>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</DrawerContent>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { canWrite } from '$lib/utils'
|
||||
import { getContext, onMount } from 'svelte'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Alert, Skeleton } from '$lib/components/common'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersEditorSection from '../TriggersEditorSection.svelte'
|
||||
@@ -19,6 +19,8 @@
|
||||
export let args: Record<string, any> = {}
|
||||
|
||||
let routeEditor: RouteEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
|
||||
$: path && loadTriggers()
|
||||
const { triggersCount, selectedTrigger, defaultValues } =
|
||||
@@ -48,6 +50,7 @@
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), http_routes_count: httpTriggers?.length }
|
||||
openForm = httpTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load http routes', e)
|
||||
}
|
||||
@@ -61,11 +64,42 @@
|
||||
bind:this={routeEditor}
|
||||
/>
|
||||
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/http_routing">
|
||||
Routes expose your scripts and flows as HTTP endpoints. Each route can be configured with a
|
||||
specific HTTP method and path.
|
||||
</Description>
|
||||
{#if !newItem && httpTriggers && httpTriggers.length > 0}
|
||||
<Section label="Routes">
|
||||
{#if !$userStore?.is_admin && !$userStore?.is_super_admin}
|
||||
<Alert title="Only workspace admins can create routes" type="warning" size="xs" />
|
||||
{/if}
|
||||
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each httpTriggers as httpTriggers (httpTriggers.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{httpTriggers.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{httpTriggers.http_method.toUpperCase()} /{httpTriggers.route_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => routeEditor?.openEdit(httpTriggers.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if httpTriggers.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<TriggersEditorSection
|
||||
on:saveTrigger={(e) => {
|
||||
routeEditor?.openNew(isFlow, path, e.detail.config)
|
||||
@@ -74,6 +108,7 @@
|
||||
on:addPreprocessor
|
||||
on:updateSchema
|
||||
on:testWithArgs
|
||||
bind:showCapture={dontCloseOnLoad}
|
||||
cloudDisabled={false}
|
||||
triggerType="http"
|
||||
{isFlow}
|
||||
@@ -83,43 +118,6 @@
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
data={{ args }}
|
||||
bind:openForm
|
||||
/>
|
||||
{#if !newItem}
|
||||
<Section label="Routes">
|
||||
{#if !$userStore?.is_admin && !$userStore?.is_super_admin}
|
||||
<Alert title="Only workspace admins can create routes" type="warning" size="xs" />
|
||||
{/if}
|
||||
|
||||
{#if httpTriggers}
|
||||
{#if httpTriggers.length == 0}
|
||||
<div class="text-xs text-secondary text-center"> No http routes </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each httpTriggers as httpTriggers (httpTriggers.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{httpTriggers.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{httpTriggers.http_method.toUpperCase()} /{httpTriggers.route_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => routeEditor?.openEdit(httpTriggers.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if httpTriggers.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import KafkaTriggerEditor from './KafkaTriggerEditor.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
@@ -20,6 +19,8 @@
|
||||
export let hasPreprocessor: boolean = false
|
||||
|
||||
let kafkaTriggerEditor: KafkaTriggerEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
|
||||
$: path && loadTriggers()
|
||||
|
||||
@@ -50,6 +51,7 @@
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), kafka_count: kafkaTriggers?.length }
|
||||
openForm = kafkaTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load Kafka triggers', e)
|
||||
}
|
||||
@@ -85,6 +87,32 @@
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/kafka_triggers">
|
||||
Kafka triggers execute scripts and flows in response to messages published to Kafka topics.
|
||||
</Description>
|
||||
{#if !newItem && kafkaTriggers && kafkaTriggers.length > 0}
|
||||
<Section label="Kafka triggers">
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each kafkaTriggers as kafkaTrigger (kafkaTrigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{kafkaTrigger.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{kafkaTrigger.kafka_resource_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => kafkaTriggerEditor?.openEdit(kafkaTrigger.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if kafkaTrigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
<TriggersEditorSection
|
||||
on:saveTrigger={(e) => {
|
||||
saveTrigger(path, e.detail.config)
|
||||
@@ -102,41 +130,8 @@
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
{openForm}
|
||||
bind:showCapture={dontCloseOnLoad}
|
||||
/>
|
||||
|
||||
{#if !newItem}
|
||||
{#if kafkaTriggers}
|
||||
<Section label="Kafka triggers">
|
||||
{#if kafkaTriggers.length == 0}
|
||||
<div class="text-xs text-secondary text-center"> No Kafka triggers </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each kafkaTriggers as kafkaTrigger (kafkaTrigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{kafkaTrigger.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{kafkaTrigger.kafka_resource_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => kafkaTriggerEditor?.openEdit(kafkaTrigger.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if kafkaTrigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
import NatsTriggerEditor from './NatsTriggerEditor.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import Skeleton from '$lib/components/common/skeleton/Skeleton.svelte'
|
||||
import Alert from '$lib/components/common/alert/Alert.svelte'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
@@ -20,6 +19,8 @@
|
||||
export let hasPreprocessor: boolean = false
|
||||
|
||||
let natsTriggerEditor: NatsTriggerEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
|
||||
$: path && loadTriggers()
|
||||
|
||||
@@ -50,6 +51,7 @@
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), nats_count: natsTriggers?.length }
|
||||
openForm = natsTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load nats triggers', e)
|
||||
}
|
||||
@@ -85,6 +87,34 @@
|
||||
<Description link="https://www.windmill.dev/docs/core_concepts/nats_triggers">
|
||||
NATS triggers execute scripts and flows in response to messages published to NATS subjects.
|
||||
</Description>
|
||||
|
||||
{#if !newItem && natsTriggers && natsTriggers.length > 0}
|
||||
<Section label="NATS Triggers">
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each natsTriggers as natsTrigger (natsTrigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{natsTrigger.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{natsTrigger.nats_resource_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => natsTriggerEditor?.openEdit(natsTrigger.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if natsTrigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<TriggersEditorSection
|
||||
on:saveTrigger={(e) => {
|
||||
saveTrigger(path, e.detail.config)
|
||||
@@ -100,41 +130,8 @@
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
{openForm}
|
||||
bind:showCapture={dontCloseOnLoad}
|
||||
/>
|
||||
|
||||
{#if !newItem}
|
||||
{#if natsTriggers}
|
||||
<Section label="NATS Triggers">
|
||||
{#if natsTriggers.length == 0}
|
||||
<div class="text-xs text-secondary text-center"> No nats triggers </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each natsTriggers as natsTrigger (natsTrigger.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{natsTrigger.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{natsTrigger.nats_resource_path}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => natsTriggerEditor?.openEdit(natsTrigger.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if natsTrigger.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Section>
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -53,5 +53,6 @@
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
alwaysOpened={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import WebsocketTriggerEditor from './WebsocketTriggerEditor.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import Section from '$lib/components/Section.svelte'
|
||||
import { Alert, Skeleton } from '$lib/components/common'
|
||||
import { Alert } from '$lib/components/common'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import type { TriggerContext } from '$lib/components/triggers'
|
||||
import TriggersEditorSection from '../TriggersEditorSection.svelte'
|
||||
@@ -19,6 +19,8 @@
|
||||
export let hasPreprocessor: boolean = false
|
||||
|
||||
let wsTriggerEditor: WebsocketTriggerEditor
|
||||
let openForm = true
|
||||
let dontCloseOnLoad = false
|
||||
|
||||
$: path && loadTriggers()
|
||||
|
||||
@@ -49,6 +51,7 @@
|
||||
return { canWrite: canWrite(x.path, x.extra_perms!, $userStore), ...x }
|
||||
})
|
||||
$triggersCount = { ...($triggersCount ?? {}), websocket_count: wsTriggers?.length }
|
||||
openForm = wsTriggers?.length === 0 || dontCloseOnLoad
|
||||
} catch (e) {
|
||||
console.error('impossible to load WS triggers', e)
|
||||
}
|
||||
@@ -72,6 +75,36 @@
|
||||
WebSocket triggers allow real-time bidirectional communication between your scripts/flows and
|
||||
external systems. Each trigger creates a unique WebSocket endpoint.
|
||||
</Description>
|
||||
|
||||
{#if !newItem && wsTriggers && wsTriggers.length > 0}
|
||||
<Section label="WebSockets">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each wsTriggers as wsTriggers (wsTriggers.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{wsTriggers.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{wsTriggers.url}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => wsTriggerEditor?.openEdit(wsTriggers.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if wsTriggers.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
|
||||
<TriggersEditorSection
|
||||
on:applyArgs
|
||||
on:saveTrigger={(e) => {
|
||||
@@ -88,43 +121,7 @@
|
||||
{canHavePreprocessor}
|
||||
{hasPreprocessor}
|
||||
{newItem}
|
||||
{openForm}
|
||||
/>
|
||||
|
||||
{#if !newItem}
|
||||
<Section label="WebSockets">
|
||||
<div class="flex flex-col gap-4">
|
||||
{#if wsTriggers}
|
||||
{#if wsTriggers.length == 0}
|
||||
<div class="text-xs text-secondary text-center"> No WebSocket triggers </div>
|
||||
{:else}
|
||||
<div class="flex flex-col divide-y pt-2">
|
||||
{#each wsTriggers as wsTriggers (wsTriggers.path)}
|
||||
<div class="grid grid-cols-5 text-2xs items-center py-2">
|
||||
<div class="col-span-2 truncate">{wsTriggers.path}</div>
|
||||
<div class="col-span-2 truncate">
|
||||
{wsTriggers.url}
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
on:click={() => wsTriggerEditor?.openEdit(wsTriggers.path, isFlow)}
|
||||
class="px-2"
|
||||
>
|
||||
{#if wsTriggers.canWrite}
|
||||
Edit
|
||||
{:else}
|
||||
View
|
||||
{/if}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<Skeleton layout={[[8]]} />
|
||||
{/if}
|
||||
</div>
|
||||
</Section>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -5,23 +5,23 @@ export function argSigToJsonSchemaType(
|
||||
| string
|
||||
| { resource: string | null }
|
||||
| {
|
||||
list:
|
||||
| (string | { object: { key: string; typ: any }[] })
|
||||
| { str: any }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| null
|
||||
}
|
||||
list:
|
||||
| (string | { object: { key: string; typ: any }[] })
|
||||
| { str: any }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| null
|
||||
}
|
||||
| { dynselect: string }
|
||||
| { str: string[] | null }
|
||||
| { object: { key: string; typ: any }[] }
|
||||
| {
|
||||
oneof: [
|
||||
{
|
||||
label: string
|
||||
properties: { key: string; typ: any }[]
|
||||
}
|
||||
]
|
||||
},
|
||||
oneof: [
|
||||
{
|
||||
label: string
|
||||
properties: { key: string; typ: any }[]
|
||||
}
|
||||
]
|
||||
},
|
||||
oldS: SchemaProperty
|
||||
): void {
|
||||
const newS: SchemaProperty = { type: '' }
|
||||
@@ -105,17 +105,22 @@ export function argSigToJsonSchemaType(
|
||||
newS.type = 'array'
|
||||
if (t.list === 'int' || t.list === 'float') {
|
||||
newS.items = { type: 'number' }
|
||||
newS.originalType = 'number[]'
|
||||
} else if (t.list === 'bytes') {
|
||||
newS.items = { type: 'string', contentEncoding: 'base64' }
|
||||
} else if (t.list == 'string') {
|
||||
newS.items = { type: 'string' }
|
||||
} else if (t.list && typeof t.list == 'object' && 'str' in t.list) {
|
||||
newS.originalType = 'bytes[]'
|
||||
} else if (t.list && typeof t.list == 'object' && 'str' in t.list && t.list.str) {
|
||||
newS.items = { type: 'string', enum: t.list.str }
|
||||
newS.originalType = 'enum[]'
|
||||
} else if (t.list == 'string' || (t.list && typeof t.list == 'object' && 'str' in t.list)) {
|
||||
newS.items = { type: 'string', enum: oldS.items?.enum }
|
||||
newS.originalType = 'string[]'
|
||||
} else if (t.list && typeof t.list == 'object' && 'resource' in t.list && t.list.resource) {
|
||||
newS.items = {
|
||||
type: 'resource',
|
||||
resourceType: t.list.resource as string
|
||||
}
|
||||
newS.originalType = 'resource[]'
|
||||
} else if (
|
||||
t.list &&
|
||||
typeof t.list == 'object' &&
|
||||
@@ -131,8 +136,10 @@ export function argSigToJsonSchemaType(
|
||||
}
|
||||
|
||||
newS.items = { type: 'object', properties: properties }
|
||||
newS.originalType = 'record[]'
|
||||
} else {
|
||||
newS.items = { type: 'object' }
|
||||
newS.originalType = 'object[]'
|
||||
}
|
||||
} else {
|
||||
newS.type = 'object'
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { BROWSER } from 'esm-env'
|
||||
import { derived, type Readable, writable } from 'svelte/store'
|
||||
|
||||
import type { IntrospectionQuery } from 'graphql'
|
||||
import {
|
||||
type WorkspaceDefaultScripts,
|
||||
type AIProvider,
|
||||
type OperatorSettings,
|
||||
type TokenResponse,
|
||||
type UserWorkspaceList,
|
||||
type OperatorSettings
|
||||
type WorkspaceDefaultScripts
|
||||
} from './gen'
|
||||
import type { IntrospectionQuery } from 'graphql'
|
||||
import { getLocalSetting } from './utils'
|
||||
|
||||
export interface UserExt {
|
||||
@@ -82,13 +83,14 @@ export const userWorkspaces: Readable<
|
||||
}
|
||||
})
|
||||
export const copilotInfo = writable<{
|
||||
ai_provider: string
|
||||
ai_provider: AIProvider
|
||||
exists_ai_resource: boolean
|
||||
code_completion_enabled: boolean
|
||||
code_completion_model?: string
|
||||
ai_models: string[]
|
||||
}>({
|
||||
ai_provider: '',
|
||||
ai_provider: 'openai',
|
||||
exists_ai_resource: false,
|
||||
code_completion_enabled: false
|
||||
ai_models: []
|
||||
})
|
||||
export const codeCompletionLoading = writable<boolean>(false)
|
||||
export const metadataCompletionEnabled = writable<boolean>(true)
|
||||
@@ -103,6 +105,9 @@ export const vimMode = writable<boolean>(getLocalSetting(VIM_MODE_SETTING_NAME)
|
||||
export const codeCompletionSessionEnabled = writable<boolean>(
|
||||
getLocalSetting(CODE_COMPLETION_SETTING_NAME) != 'false'
|
||||
)
|
||||
export const copilotSessionModel = writable<string | undefined>(
|
||||
getLocalSetting(CODE_COMPLETION_SETTING_NAME) ?? undefined
|
||||
)
|
||||
export const usedTriggerKinds = writable<string[]>([])
|
||||
|
||||
type SQLBaseSchema = {
|
||||
|
||||
@@ -246,6 +246,8 @@ export function pointerDownOutside(
|
||||
options?: ClickOutsideOptions
|
||||
): { destroy(): void; update(newOptions: ClickOutsideOptions): void } {
|
||||
const handlePointerDown = async (event: PointerEvent) => {
|
||||
if (!event.isTrusted) return
|
||||
|
||||
if (options?.customEventName) {
|
||||
node.dispatchEvent(
|
||||
new CustomEvent<PointerEvent>(options.customEventName, {
|
||||
@@ -351,14 +353,13 @@ export function removeItemAll<T>(arr: T[], value: T) {
|
||||
}
|
||||
|
||||
export function emptyString(str: string | undefined | null): boolean {
|
||||
return str === undefined || str === null || str === ''
|
||||
return str === undefined || str === null || str === ''
|
||||
}
|
||||
|
||||
export function emptyStringTrimmed(str: string | undefined | null): boolean {
|
||||
return str === undefined || str === null || str === '' || str.trim().length === 0
|
||||
}
|
||||
|
||||
|
||||
export function defaultIfEmptyString(str: string | undefined, dflt: string): string {
|
||||
return emptyString(str) ? dflt : str!
|
||||
}
|
||||
@@ -1104,3 +1105,10 @@ export function isScriptPreview(job_kind: Job['job_kind'] | undefined) {
|
||||
!!job_kind && (job_kind === 'preview' || job_kind === 'flowscript' || job_kind === 'appscript')
|
||||
)
|
||||
}
|
||||
|
||||
export function conditionalMelt(node: HTMLElement, meltElement: any) {
|
||||
if (meltElement) {
|
||||
return meltElement.action(node)
|
||||
}
|
||||
return { destroy: () => {} }
|
||||
}
|
||||
|
||||
@@ -44,14 +44,14 @@
|
||||
import { syncTutorialsTodos } from '$lib/tutorialUtils'
|
||||
import { ArrowLeft, Search } from 'lucide-svelte'
|
||||
import { getUserExt } from '$lib/user'
|
||||
import { initAllAiWorkspace } from '$lib/components/copilot/lib'
|
||||
import { workspaceAIClients } from '$lib/components/copilot/lib'
|
||||
import { twMerge } from 'tailwind-merge'
|
||||
import OperatorMenu from '$lib/components/sidebar/OperatorMenu.svelte'
|
||||
import GlobalSearchModal from '$lib/components/search/GlobalSearchModal.svelte'
|
||||
import MenuButton from '$lib/components/sidebar/MenuButton.svelte'
|
||||
import { setContext } from 'svelte'
|
||||
import { base } from '$app/paths'
|
||||
import Menubar from '$lib/components/meltComponents/Menubar.svelte'
|
||||
import { Menubar } from '$lib/components/meltComponents'
|
||||
|
||||
OpenAPI.WITH_CREDENTIALS = true
|
||||
let menuOpen = false
|
||||
@@ -241,15 +241,19 @@
|
||||
let devOnly = $page.url.pathname.startsWith(base + '/scripts/dev')
|
||||
|
||||
async function loadCopilot(workspace: string) {
|
||||
initAllAiWorkspace(workspace)
|
||||
workspaceAIClients.init(workspace)
|
||||
try {
|
||||
copilotInfo.set(await WorkspaceService.getCopilotInfo({ workspace }))
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
const info = await WorkspaceService.getCopilotInfo({ workspace })
|
||||
copilotInfo.set({
|
||||
ai_provider: '',
|
||||
...info,
|
||||
ai_provider: info.ai_provider ?? 'openai'
|
||||
})
|
||||
} catch (err) {
|
||||
copilotInfo.set({
|
||||
ai_provider: 'openai',
|
||||
exists_ai_resource: false,
|
||||
code_completion_enabled: false
|
||||
code_completion_model: undefined,
|
||||
ai_models: []
|
||||
})
|
||||
console.error('Could not get copilot info')
|
||||
}
|
||||
@@ -380,7 +384,7 @@
|
||||
>
|
||||
<div
|
||||
class={classNames(
|
||||
'absolute top-0 right-0 -mr-12 pt-2 ease-in-out duration-300',
|
||||
'absolute top-0 right-4 -mr-12 pt-2 ease-in-out duration-300',
|
||||
menuOpen ? 'opacity-100' : 'opacity-0'
|
||||
)}
|
||||
>
|
||||
@@ -389,10 +393,10 @@
|
||||
on:click={() => {
|
||||
menuOpen = !menuOpen
|
||||
}}
|
||||
class="ml-1 flex items-center justify-center h-8 w-8 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white border border-white"
|
||||
class="ml-1 flex items-center justify-center h-6 w-6 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white border border-white"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6 text-white"
|
||||
class="h-4 w-4 text-white"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -408,12 +412,12 @@
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="dark:bg-[#1e232e] bg-[#202125] h-full !dark">
|
||||
<div class="dark:bg-[#1e232e] bg-[#202125] h-full !dark flex flex-col">
|
||||
<div class="flex gap-x-2 flex-shrink-0 p-4 font-semibold text-gray-200 w-40">
|
||||
<WindmillIcon white={true} height="20px" width="20px" />
|
||||
Windmill
|
||||
</div>
|
||||
<div class="px-2 py-4 space-y-2 border-y border-gray-500">
|
||||
<div class="px-2 py-4 border-y border-gray-500">
|
||||
<Menubar let:createMenu>
|
||||
<WorkspaceMenu {createMenu} />
|
||||
<FavoriteMenu {createMenu} {favoriteLinks} />
|
||||
@@ -468,8 +472,8 @@
|
||||
{/if}
|
||||
</div>
|
||||
</button>
|
||||
<div class="px-2 py-4 space-y-2 border-y border-gray-700">
|
||||
<Menubar let:createMenu>
|
||||
<div class="px-2 py-4 border-y border-gray-700 flex flex-col gap-1">
|
||||
<Menubar let:createMenu class="flex flex-col gap-1">
|
||||
<WorkspaceMenu {createMenu} {isCollapsed} />
|
||||
<FavoriteMenu {createMenu} {favoriteLinks} {isCollapsed} />
|
||||
</Menubar>
|
||||
@@ -515,6 +519,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Legacy menu -->
|
||||
<div
|
||||
class={classNames(
|
||||
'fixed inset-0 dark:bg-[#1e232e] bg-[#202125] dark:bg-opacity-75 bg-opacity-75 transition-opacity ease-linear duration-300 !dark',
|
||||
|
||||
@@ -109,7 +109,11 @@
|
||||
new Set(filteredItems?.map((x) => x.path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
).sort()
|
||||
: Array.from(
|
||||
new Set(filteredItems?.map((x) => x.script_path.split('/').slice(0, 2).join('/')) ?? [])
|
||||
new Set(
|
||||
filteredItems
|
||||
?.filter((x) => x.script_path)
|
||||
.map((x) => x.script_path.split('/').slice(0, 2).join('/')) ?? []
|
||||
)
|
||||
).sort()
|
||||
|
||||
$: items = filter !== '' ? filteredItems : preFilteredItems
|
||||
@@ -156,176 +160,177 @@
|
||||
f={(x) => (x.summary ?? '') + ' ' + x.path + ' (' + x.script_path + ')'}
|
||||
/>
|
||||
|
||||
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find(_ => _.id === $workspaceStore)?.operator_settings?.triggers}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
|
||||
<p class="font-bold">Unauthorized</p>
|
||||
<p>Page not available for operators</p>
|
||||
</div>
|
||||
{:else}
|
||||
<CenteredPage>
|
||||
<PageHeader
|
||||
title="Custom HTTP routes"
|
||||
tooltip="Every script and flow already has a canonical HTTP API endpoint/webhook attached to it, this is to create additional parametrizable ones."
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/http_routing"
|
||||
>
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Button size="md" startIcon={{ icon: Plus }} on:click={() => routeEditor.openNew(false)}>
|
||||
New route
|
||||
</Button>
|
||||
{/if}
|
||||
</PageHeader>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="w-full pb-4 pt-6">
|
||||
<input type="text" placeholder="Search routes" bind:value={filter} class="search-item" />
|
||||
<div class="flex flex-row items-center gap-2 mt-6">
|
||||
<div class="text-sm shrink-0"> Filter by path of </div>
|
||||
<ToggleButtonGroup bind:selected={selectedFilterKind}>
|
||||
<ToggleButton small value="trigger" label="Route" icon={Route} />
|
||||
<ToggleButton small value="script_flow" label="Script/Flow" icon={Code} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<ListFilters syncQuery bind:selectedFilter={ownerFilter} filters={owners} />
|
||||
|
||||
<div class="flex flex-row items-center justify-end gap-4">
|
||||
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
|
||||
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
|
||||
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={filterUserFolders}
|
||||
options={{ right: `Only u/${$userStore.username} and f/*` }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if loading}
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[6], 0.4]} />
|
||||
{/each}
|
||||
{:else if !triggers?.length}
|
||||
<div class="text-center text-sm text-tertiary mt-2"> No routes </div>
|
||||
{:else if items?.length}
|
||||
<div class="border rounded-md divide-y">
|
||||
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, route_path, is_flow, extra_perms, canWrite, marked, http_method, static_asset_config } (path)}
|
||||
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
|
||||
|
||||
<div
|
||||
class="hover:bg-surface-hover w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md flex flex-col"
|
||||
>
|
||||
<div class="w-full flex gap-5 items-center">
|
||||
<RowIcon kind={is_flow ? 'flow' : 'script'} />
|
||||
|
||||
<a
|
||||
href="#{path}"
|
||||
on:click={() => routeEditor?.openEdit(path, is_flow)}
|
||||
class="min-w-0 grow hover:underline decoration-gray-400"
|
||||
>
|
||||
<div class="text-primary flex-wrap text-left text-md font-semibold mb-1 truncate">
|
||||
{#if marked}
|
||||
<span class="text-xs">
|
||||
{@html marked}
|
||||
</span>
|
||||
{:else}
|
||||
{http_method.toUpperCase()} /{route_path}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-secondary text-xs truncate text-left font-light">
|
||||
{path}
|
||||
</div>
|
||||
<div class="text-secondary text-xs truncate text-left font-light">
|
||||
{#if static_asset_config}
|
||||
file: {static_asset_config.s3}
|
||||
{:else}
|
||||
runnable: {script_path}
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="hidden lg:flex flex-row gap-1 items-center">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-center justify-end">
|
||||
<Button
|
||||
on:click={() => routeEditor?.openEdit(path, is_flow)}
|
||||
size="xs"
|
||||
startIcon={canWrite
|
||||
? { icon: Pen }
|
||||
: {
|
||||
icon: Eye
|
||||
}}
|
||||
color="dark"
|
||||
>
|
||||
{canWrite ? 'Edit' : 'View'}
|
||||
</Button>
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
displayName: `View ${is_flow ? 'Flow' : 'Script'}`,
|
||||
icon: Eye,
|
||||
action: () => {
|
||||
goto(href)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
type: 'delete',
|
||||
icon: Trash,
|
||||
disabled: !canWrite || !($userStore?.is_admin || $userStore?.is_super_admin),
|
||||
action: async () => {
|
||||
await HttpTriggerService.deleteHttpTrigger({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path
|
||||
})
|
||||
loadTriggers()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Edit' : 'View',
|
||||
icon: canWrite ? Pen : Eye,
|
||||
action: () => {
|
||||
routeEditor?.openEdit(path, is_flow)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Audit logs',
|
||||
icon: Eye,
|
||||
href: `${base}/audit_logs?resource=${path}`
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
icon: Share,
|
||||
action: () => {
|
||||
shareModal.openDrawer(path, 'http_trigger')
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex justify-between items-baseline">
|
||||
<div
|
||||
class="flex flex-wrap text-[0.7em] text-tertiary gap-1 items-center justify-end truncate pr-2"
|
||||
>
|
||||
<div class="truncate">edited by {edited_by}</div>
|
||||
<div class="truncate">at {displayDate(edited_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<NoItemFound />
|
||||
{/if}
|
||||
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.triggers}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
|
||||
<p class="font-bold">Unauthorized</p>
|
||||
<p>Page not available for operators</p>
|
||||
</div>
|
||||
{#if items && items?.length > 15 && nbDisplayed < items.length}
|
||||
<span class="text-xs"
|
||||
>{nbDisplayed} items out of {items.length}
|
||||
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button></span
|
||||
{:else}
|
||||
<CenteredPage>
|
||||
<PageHeader
|
||||
title="Custom HTTP routes"
|
||||
tooltip="Every script and flow already has a canonical HTTP API endpoint/webhook attached to it, this is to create additional parametrizable ones."
|
||||
documentationLink="https://www.windmill.dev/docs/core_concepts/http_routing"
|
||||
>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
{#if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Button size="md" startIcon={{ icon: Plus }} on:click={() => routeEditor.openNew(false)}>
|
||||
New route
|
||||
</Button>
|
||||
{/if}
|
||||
</PageHeader>
|
||||
<div class="w-full h-full flex flex-col">
|
||||
<div class="w-full pb-4 pt-6">
|
||||
<input type="text" placeholder="Search routes" bind:value={filter} class="search-item" />
|
||||
<div class="flex flex-row items-center gap-2 mt-6">
|
||||
<div class="text-sm shrink-0"> Filter by path of </div>
|
||||
<ToggleButtonGroup bind:selected={selectedFilterKind}>
|
||||
<ToggleButton small value="trigger" label="Route" icon={Route} />
|
||||
<ToggleButton small value="script_flow" label="Script/Flow" icon={Code} />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
<ListFilters syncQuery bind:selectedFilter={ownerFilter} filters={owners} />
|
||||
|
||||
<div class="flex flex-row items-center justify-end gap-4">
|
||||
{#if $userStore?.is_super_admin && $userStore.username.includes('@')}
|
||||
<Toggle size="xs" bind:checked={filterUserFolders} options={{ right: 'Only f/*' }} />
|
||||
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
|
||||
<Toggle
|
||||
size="xs"
|
||||
bind:checked={filterUserFolders}
|
||||
options={{ right: `Only u/${$userStore.username} and f/*` }}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{#if loading}
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[6], 0.4]} />
|
||||
{/each}
|
||||
{:else if !triggers?.length}
|
||||
<div class="text-center text-sm text-tertiary mt-2"> No routes </div>
|
||||
{:else if items?.length}
|
||||
<div class="border rounded-md divide-y">
|
||||
{#each items.slice(0, nbDisplayed) as { path, edited_by, edited_at, script_path, route_path, is_flow, extra_perms, canWrite, marked, http_method, static_asset_config } (path)}
|
||||
{@const href = `${is_flow ? '/flows/get' : '/scripts/get'}/${script_path}`}
|
||||
|
||||
<div
|
||||
class="hover:bg-surface-hover w-full items-center px-4 py-2 gap-4 first-of-type:!border-t-0
|
||||
first-of-type:rounded-t-md last-of-type:rounded-b-md flex flex-col"
|
||||
>
|
||||
<div class="w-full flex gap-5 items-center">
|
||||
<RowIcon kind={is_flow ? 'flow' : 'script'} />
|
||||
|
||||
<a
|
||||
href="#{path}"
|
||||
on:click={() => routeEditor?.openEdit(path, is_flow)}
|
||||
class="min-w-0 grow hover:underline decoration-gray-400"
|
||||
>
|
||||
<div class="text-primary flex-wrap text-left text-md font-semibold mb-1 truncate">
|
||||
{#if marked}
|
||||
<span class="text-xs">
|
||||
{@html marked}
|
||||
</span>
|
||||
{:else}
|
||||
{http_method.toUpperCase()} /{route_path}
|
||||
{/if}
|
||||
</div>
|
||||
<div class="text-secondary text-xs truncate text-left font-light">
|
||||
{path}
|
||||
</div>
|
||||
<div class="text-secondary text-xs truncate text-left font-light">
|
||||
{#if static_asset_config}
|
||||
file: {static_asset_config.s3}
|
||||
{:else}
|
||||
runnable: {script_path}
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<div class="hidden lg:flex flex-row gap-1 items-center">
|
||||
<SharedBadge {canWrite} extraPerms={extra_perms} />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2 items-center justify-end">
|
||||
<Button
|
||||
on:click={() => routeEditor?.openEdit(path, is_flow)}
|
||||
size="xs"
|
||||
startIcon={canWrite
|
||||
? { icon: Pen }
|
||||
: {
|
||||
icon: Eye
|
||||
}}
|
||||
color="dark"
|
||||
>
|
||||
{canWrite ? 'Edit' : 'View'}
|
||||
</Button>
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
displayName: `View ${is_flow ? 'Flow' : 'Script'}`,
|
||||
icon: Eye,
|
||||
action: () => {
|
||||
goto(href)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
type: 'delete',
|
||||
icon: Trash,
|
||||
disabled:
|
||||
!canWrite || !($userStore?.is_admin || $userStore?.is_super_admin),
|
||||
action: async () => {
|
||||
await HttpTriggerService.deleteHttpTrigger({
|
||||
workspace: $workspaceStore ?? '',
|
||||
path
|
||||
})
|
||||
loadTriggers()
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Edit' : 'View',
|
||||
icon: canWrite ? Pen : Eye,
|
||||
action: () => {
|
||||
routeEditor?.openEdit(path, is_flow)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Audit logs',
|
||||
icon: Eye,
|
||||
href: `${base}/audit_logs?resource=${path}`
|
||||
},
|
||||
{
|
||||
displayName: canWrite ? 'Share' : 'See Permissions',
|
||||
icon: Share,
|
||||
action: () => {
|
||||
shareModal.openDrawer(path, 'http_trigger')
|
||||
}
|
||||
}
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full flex justify-between items-baseline">
|
||||
<div
|
||||
class="flex flex-wrap text-[0.7em] text-tertiary gap-1 items-center justify-end truncate pr-2"
|
||||
>
|
||||
<div class="truncate">edited by {edited_by}</div>
|
||||
<div class="truncate">at {displayDate(edited_at)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<NoItemFound />
|
||||
{/if}
|
||||
</div>
|
||||
{#if items && items?.length > 15 && nbDisplayed < items.length}
|
||||
<span class="text-xs"
|
||||
>{nbDisplayed} items out of {items.length}
|
||||
<button class="ml-4" on:click={() => (nbDisplayed += 30)}>load 30 more</button></span
|
||||
>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
{/if}
|
||||
|
||||
<ShareModal
|
||||
|
||||
@@ -726,7 +726,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{#if job && job.flow_status && job.job_kind === 'script'}
|
||||
<PreprocessedArgsDisplay flowStatus={job.flow_status} />
|
||||
<PreprocessedArgsDisplay preprocessed={job.preprocessed} />
|
||||
{/if}
|
||||
{#if persistentScriptDefinition}
|
||||
<button on:click={() => persistentScriptDrawer.open?.(persistentScriptDefinition)}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { generateRandomString } from '$lib/utils'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import WorkspaceMenu from '$lib/components/sidebar/WorkspaceMenu.svelte'
|
||||
import Menubar from '$lib/components/meltComponents/Menubar.svelte'
|
||||
import { Menubar } from '$lib/components/meltComponents'
|
||||
|
||||
let itemPath: string | undefined = undefined
|
||||
let itemKind: 'script' | 'flow' | 'app' = 'script'
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
SettingService,
|
||||
UserService,
|
||||
VariableService,
|
||||
WorkspaceService
|
||||
WorkspaceService,
|
||||
type AIProvider
|
||||
} from '$lib/gen'
|
||||
import { validateUsername } from '$lib/utils'
|
||||
import { logoutWithRedirect } from '$lib/logout'
|
||||
@@ -18,12 +19,12 @@
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import { onMount } from 'svelte'
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import TestAiKey from '$lib/components/copilot/TestAiKey.svelte'
|
||||
import TestAIKey from '$lib/components/copilot/TestAIKey.svelte'
|
||||
import { switchWorkspace } from '$lib/storeUtils'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import type { AiProviderTypes } from '$lib/components/copilot/lib'
|
||||
import { AI_DEFAULT_MODELS } from '$lib/components/copilot/lib'
|
||||
|
||||
const rd = $page.url.searchParams.get('rd')
|
||||
|
||||
@@ -41,8 +42,12 @@
|
||||
let colorEnabled = false
|
||||
|
||||
function generateRandomColor() {
|
||||
const randomColor = '#' + Math.floor(Math.random() * 16777215).toString(16).padStart(6, '0');
|
||||
workspaceColor = randomColor;
|
||||
const randomColor =
|
||||
'#' +
|
||||
Math.floor(Math.random() * 16777215)
|
||||
.toString(16)
|
||||
.padStart(6, '0')
|
||||
workspaceColor = randomColor
|
||||
}
|
||||
|
||||
$: id = name.toLowerCase().replace(/\s/gi, '-')
|
||||
@@ -111,7 +116,8 @@
|
||||
workspace: id,
|
||||
requestBody: {
|
||||
ai_resource: { path, provider: selected },
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
ai_models: aiKey ? AI_DEFAULT_MODELS[selected].slice(0, 1) : [],
|
||||
code_completion_model: codeCompletionEnabled ? AI_DEFAULT_MODELS[selected][0] : undefined
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -178,7 +184,7 @@
|
||||
|
||||
let auto_invite = false
|
||||
let operatorOnly = false
|
||||
let selected: AiProviderTypes = 'openai'
|
||||
let selected: Exclude<AIProvider, 'customai'> = 'openai'
|
||||
</script>
|
||||
|
||||
<CenteredModal title="New Workspace">
|
||||
@@ -198,11 +204,23 @@
|
||||
</label>
|
||||
<label class="block pb-4">
|
||||
<span class="text-secondary text-sm">Workspace color</span>
|
||||
<span class="ml-5 text-tertiary text-xs">Color to identify the current workspace in the list of workspaces</span>
|
||||
<span class="ml-5 text-tertiary text-xs"
|
||||
>Color to identify the current workspace in the list of workspaces</span
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Toggle bind:checked={colorEnabled} options={{ right: 'Enable' }} />
|
||||
{#if colorEnabled}<input class="w-10" type="color" bind:value={workspaceColor} disabled={!colorEnabled} />{/if}
|
||||
<input type="text" class="w-24 text-sm" bind:value={workspaceColor} disabled={!colorEnabled} />
|
||||
{#if colorEnabled}<input
|
||||
class="w-10"
|
||||
type="color"
|
||||
bind:value={workspaceColor}
|
||||
disabled={!colorEnabled}
|
||||
/>{/if}
|
||||
<input
|
||||
type="text"
|
||||
class="w-24 text-sm"
|
||||
bind:value={workspaceColor}
|
||||
disabled={!colorEnabled}
|
||||
/>
|
||||
<Button on:click={generateRandomColor} size="xs" disabled={!colorEnabled}>Random</Button>
|
||||
</div>
|
||||
</label>
|
||||
@@ -230,9 +248,12 @@
|
||||
</span>
|
||||
<div class="pb-2">
|
||||
<ToggleButtonGroup bind:selected>
|
||||
<ToggleButton value="openai" label="OpenAi" />
|
||||
<ToggleButton value="openai" label="OpenAI" />
|
||||
<ToggleButton value="anthropic" label="Anthropic" />
|
||||
<ToggleButton value="mistral" label="Mistral" />
|
||||
<ToggleButton value="deepseek" label="DeepSeek" />
|
||||
<ToggleButton value="groq" label="Groq" />
|
||||
<ToggleButton value="openrouter" label="OpenRouter" />
|
||||
</ToggleButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
@@ -243,7 +264,12 @@
|
||||
bind:value={aiKey}
|
||||
on:keyup={handleKeyUp}
|
||||
/>
|
||||
<TestAiKey apiKey={aiKey} disabled={!aiKey} aiProvider={selected} />
|
||||
<TestAIKey
|
||||
apiKey={aiKey}
|
||||
disabled={!aiKey}
|
||||
aiProvider={selected}
|
||||
model={AI_DEFAULT_MODELS[selected][0]}
|
||||
/>
|
||||
</div>
|
||||
{#if aiKey}
|
||||
<Toggle
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { page } from '$app/stores'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import CenteredPage from '$lib/components/CenteredPage.svelte'
|
||||
import { Alert, Badge, Button, Tab, Tabs } from '$lib/components/common'
|
||||
import { Alert, Badge, Button, Skeleton, Tab, Tabs } from '$lib/components/common'
|
||||
|
||||
import DeployToSetting from '$lib/components/DeployToSetting.svelte'
|
||||
import ErrorOrRecoveryHandler from '$lib/components/ErrorOrRecoveryHandler.svelte'
|
||||
@@ -20,7 +20,8 @@
|
||||
WorkspaceService,
|
||||
JobService,
|
||||
ResourceService,
|
||||
SettingService
|
||||
SettingService,
|
||||
type AIProvider
|
||||
} from '$lib/gen'
|
||||
import {
|
||||
enterpriseLicense,
|
||||
@@ -50,7 +51,7 @@
|
||||
|
||||
import PremiumInfo from '$lib/components/settings/PremiumInfo.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import TestAiKey from '$lib/components/copilot/TestAiKey.svelte'
|
||||
import TestAIKey from '$lib/components/copilot/TestAIKey.svelte'
|
||||
import Portal from '$lib/components/Portal.svelte'
|
||||
|
||||
import { fade } from 'svelte/transition'
|
||||
@@ -66,8 +67,11 @@
|
||||
import { hubPaths } from '$lib/hub'
|
||||
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
|
||||
import type { AiProviderTypes } from '$lib/components/copilot/lib'
|
||||
import { AI_DEFAULT_MODELS } from '$lib/components/copilot/lib'
|
||||
import Description from '$lib/components/Description.svelte'
|
||||
import MultiSelect from 'svelte-multiselect'
|
||||
import Label from '$lib/components/Label.svelte'
|
||||
import ArgEnum from '$lib/components/ArgEnum.svelte'
|
||||
|
||||
type GitSyncTypeMap = {
|
||||
scripts: boolean
|
||||
@@ -113,8 +117,11 @@
|
||||
let errorHandlerMutedOnCancel: boolean | undefined = undefined
|
||||
let criticalAlertUIMuted: boolean | undefined = undefined
|
||||
let initialCriticalAlertUIMuted: boolean | undefined = undefined
|
||||
let aiResourceInitialPath: string | undefined = undefined
|
||||
let aiResourceInitialProvider: string | undefined = undefined
|
||||
|
||||
let aiResourcePath: string | undefined = undefined
|
||||
let aiProvider: AIProvider = 'openai'
|
||||
let aiModels: string[] = []
|
||||
let codeCompletionModel: string | undefined = undefined
|
||||
|
||||
let s3ResourceSettings: S3ResourceSettings = {
|
||||
resourceType: 's3',
|
||||
@@ -142,8 +149,6 @@
|
||||
let editedWorkspaceEncryptionKey: string | undefined = undefined
|
||||
let workspaceReencryptionInProgress: boolean = false
|
||||
let encryptionKeyRegex = /^[a-zA-Z0-9]{64}$/
|
||||
let codeCompletionEnabled: boolean = false
|
||||
let selected: AiProviderTypes = 'openai'
|
||||
let tab =
|
||||
($page.url.searchParams.get('tab') as
|
||||
| 'users'
|
||||
@@ -191,10 +196,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function editCopilotConfig(aiResourcePath: string, aiProvider: string): Promise<void> {
|
||||
// in JS, an empty string is also falsy
|
||||
aiResourceInitialPath = aiResourcePath
|
||||
aiResourceInitialProvider = aiProvider
|
||||
async function editCopilotConfig(): Promise<void> {
|
||||
if (aiResourcePath) {
|
||||
await WorkspaceService.editCopilotConfig({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -203,26 +205,30 @@
|
||||
path: aiResourcePath,
|
||||
provider: aiProvider
|
||||
},
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
code_completion_model: codeCompletionModel,
|
||||
ai_models: aiModels
|
||||
}
|
||||
})
|
||||
copilotInfo.set({
|
||||
ai_provider: aiProvider,
|
||||
exists_ai_resource: true,
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
code_completion_model: codeCompletionModel,
|
||||
ai_models: aiModels
|
||||
})
|
||||
} else {
|
||||
await WorkspaceService.editCopilotConfig({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
ai_resource: undefined,
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
code_completion_model: codeCompletionModel,
|
||||
ai_models: []
|
||||
}
|
||||
})
|
||||
copilotInfo.set({
|
||||
ai_provider: '',
|
||||
ai_provider: 'openai',
|
||||
exists_ai_resource: false,
|
||||
code_completion_enabled: codeCompletionEnabled
|
||||
code_completion_model: codeCompletionModel,
|
||||
ai_models: []
|
||||
})
|
||||
}
|
||||
sendUserToast(`Copilot settings updated`)
|
||||
@@ -382,6 +388,7 @@
|
||||
}, 1000 - (timeEnd - timeStart))
|
||||
}
|
||||
|
||||
let loadedSettings = false
|
||||
async function loadSettings(): Promise<void> {
|
||||
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
|
||||
team_name = settings.slack_name
|
||||
@@ -395,9 +402,12 @@
|
||||
customer_id = settings.customer_id
|
||||
workspaceToDeployTo = settings.deploy_to
|
||||
webhook = settings.webhook
|
||||
aiResourceInitialPath = settings.ai_resource?.path
|
||||
aiResourceInitialProvider = settings.ai_resource?.provider
|
||||
selected = (aiResourceInitialProvider as AiProviderTypes) ?? 'openai'
|
||||
|
||||
aiResourcePath = settings.ai_resource?.path
|
||||
aiProvider = settings.ai_resource?.provider ?? 'openai'
|
||||
codeCompletionModel = settings.code_completion_model
|
||||
aiModels = settings.ai_models
|
||||
|
||||
errorHandlerItemKind = settings.error_handler?.split('/')[0] as 'flow' | 'script'
|
||||
errorHandlerScriptPath = (settings.error_handler ?? '').split('/').slice(1).join('/')
|
||||
errorHandlerInitialScriptPath = errorHandlerScriptPath
|
||||
@@ -415,7 +425,6 @@
|
||||
: 'custom'
|
||||
}
|
||||
errorHandlerExtraArgs = settings.error_handler_extra_args ?? {}
|
||||
codeCompletionEnabled = settings.code_completion_enabled
|
||||
workspaceDefaultAppPath = settings.default_app
|
||||
|
||||
s3ResourceSettings = convertBackendSettingsToFrontendSettings(settings.large_file_storage)
|
||||
@@ -508,6 +517,8 @@
|
||||
workspace: $workspaceStore!,
|
||||
path: 'openai_client_credentials_oauth'
|
||||
})
|
||||
|
||||
loadedSettings = true
|
||||
}
|
||||
|
||||
let deployUiSettings: {
|
||||
@@ -698,7 +709,9 @@
|
||||
</Tab>
|
||||
</Tabs>
|
||||
</div>
|
||||
{#if tab == 'users'}
|
||||
{#if !loadedSettings}
|
||||
<Skeleton layout={[1, [40]]} />
|
||||
{:else if tab == 'users'}
|
||||
<WorkspaceUserSettings />
|
||||
{:else if tab == 'deploy_to'}
|
||||
<div class="flex flex-col gap-4 my-8">
|
||||
@@ -1044,43 +1057,104 @@
|
||||
</Description>
|
||||
</div>
|
||||
</div>
|
||||
<ToggleButtonGroup
|
||||
bind:selected
|
||||
on:selected={() => {
|
||||
aiResourceInitialPath = ''
|
||||
aiResourceInitialProvider = ''
|
||||
}}
|
||||
>
|
||||
<ToggleButton value="openai" label="OpenAI" />
|
||||
<ToggleButton value="anthropic" label="Anthropic" />
|
||||
<ToggleButton value="mistral" label="Mistral" />
|
||||
</ToggleButtonGroup>
|
||||
<div class="mt-5 flex gap-1">
|
||||
{#key [aiResourceInitialPath, aiResourceInitialProvider, usingOpenaiClientCredentialsOauth, selected]}
|
||||
<ResourcePicker
|
||||
resourceType={usingOpenaiClientCredentialsOauth
|
||||
? 'openai_client_credentials_oauth'
|
||||
: selected}
|
||||
initialValue={aiResourceInitialPath}
|
||||
on:change={(ev) => {
|
||||
editCopilotConfig(ev.detail, selected)
|
||||
}}
|
||||
/>
|
||||
<TestAiKey
|
||||
disabled={!aiResourceInitialPath || aiResourceInitialProvider != selected}
|
||||
aiProvider={selected}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<Toggle
|
||||
class="mr-2"
|
||||
bind:checked={codeCompletionEnabled}
|
||||
options={{ right: 'Enable code completion' }}
|
||||
on:change={() => {
|
||||
editCopilotConfig(aiResourceInitialPath || '', aiResourceInitialProvider || '')
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<ToggleButtonGroup
|
||||
bind:selected={aiProvider}
|
||||
on:selected={() => {
|
||||
aiResourcePath = ''
|
||||
aiModels = []
|
||||
codeCompletionModel = undefined
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<ToggleButton value="openai" label="OpenAI" />
|
||||
<ToggleButton value="anthropic" label="Anthropic" />
|
||||
<ToggleButton value="mistral" label="Mistral" />
|
||||
<ToggleButton value="deepseek" label="DeepSeek" />
|
||||
<ToggleButton value="groq" label="Groq" />
|
||||
<ToggleButton value="openrouter" label="OpenRouter" />
|
||||
<ToggleButton
|
||||
value="customai"
|
||||
label={'Custom AI' + ($enterpriseLicense ? '' : ' (EE)')}
|
||||
disabled={!$enterpriseLicense}
|
||||
tooltip="Configure a custom AI provider that is OpenAI API compatible"
|
||||
showTooltipIcon
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
<div class="flex gap-1">
|
||||
{#key aiProvider}
|
||||
<ResourcePicker
|
||||
resourceType={usingOpenaiClientCredentialsOauth
|
||||
? 'openai_client_credentials_oauth'
|
||||
: aiProvider}
|
||||
initialValue={aiResourcePath}
|
||||
bind:value={aiResourcePath}
|
||||
on:change={() => {
|
||||
if (aiResourcePath && aiModels.length === 0) {
|
||||
if (aiProvider !== 'customai') {
|
||||
aiModels = AI_DEFAULT_MODELS[aiProvider].slice(0, 1)
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<TestAIKey
|
||||
disabled={!aiResourcePath || (aiProvider === 'customai' && aiModels.length === 0)}
|
||||
resourcePath={aiResourcePath}
|
||||
{aiProvider}
|
||||
model={aiProvider === 'customai' ? aiModels[0] : AI_DEFAULT_MODELS[aiProvider][0]}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
{#if aiResourcePath}
|
||||
<Label label="Enabled models">
|
||||
<MultiSelect
|
||||
options={AI_DEFAULT_MODELS[aiProvider]}
|
||||
ulOptionsClass={'!bg-surface-secondary'}
|
||||
allowUserOptions="append"
|
||||
bind:selected={aiModels}
|
||||
/>
|
||||
</Label>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<Toggle
|
||||
on:change={() => {
|
||||
if (codeCompletionModel != undefined) {
|
||||
codeCompletionModel = undefined
|
||||
} else {
|
||||
codeCompletionModel = AI_DEFAULT_MODELS[aiProvider][0] ?? ''
|
||||
}
|
||||
}}
|
||||
checked={codeCompletionModel != undefined}
|
||||
options={{
|
||||
right: 'Code completion'
|
||||
}}
|
||||
/>
|
||||
|
||||
{#if codeCompletionModel != undefined}
|
||||
<Label label="Code completion model">
|
||||
<ArgEnum
|
||||
enum_={AI_DEFAULT_MODELS[aiProvider]}
|
||||
bind:value={codeCompletionModel}
|
||||
disabled={false}
|
||||
autofocus={false}
|
||||
defaultValue={undefined}
|
||||
valid={true}
|
||||
create={true}
|
||||
required={false}
|
||||
/>
|
||||
</Label>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
<Button
|
||||
disabled={(aiResourcePath && aiModels.length === 0) ||
|
||||
(codeCompletionModel != undefined && codeCompletionModel.length === 0)}
|
||||
size="sm"
|
||||
on:click={editCopilotConfig}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
{:else if tab == 'windmill_lfs'}
|
||||
<div class="flex flex-col gap-4 my-8">
|
||||
|
||||
Reference in New Issue
Block a user