mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-13 08:05:23 +00:00
* fix: manual resource type sync fetches from hub first, cache as fallback The superadmin "Synchronize resource types" endpoint (POST /api/settings/sync_cached_resource_types) was cache-first: it read the on-disk hub_rt cache and only fell back to the hub when no cache file existed. Since that cache is refreshed by a daily cron, a newly-published hub type could not be pulled on demand, the button replayed the stale cache and reported "Synced 0", so the type never landed in the admins workspace. The manual endpoint is now hub-first: it fetches the live list and upserts it into admins, falling back to reading the on-disk cache only when the hub is unreachable (airgapped install / network error), logging which path it took. The startup/offline sync in main.rs (SYNC_CACHED_RT + the cache-rt cron) stays cache-based and owns writing the cache, so this endpoint never touches it. Adds an optional `name` query param: when a specific type is requested and is still absent from the hub after syncing, the endpoint returns an explicit not-found instead of a silent "Synced 0". The three not-found frontend call sites (ResourceForm, AppConnectInner, ApiConnectForm) thread the type name through SyncResourceTypes; the global instance-settings button stays name-less. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: run whole-list sync before the targeted not-found check, word 404 by source Address review nits: the optional `name` check ran before the upsert loop, so a `?name=<absent>` request skipped the whole-list refresh; move it after the loop so the sync always happens. Also word the not-found 404 by source, the cache-fallback path (hub unreachable) no longer claims it checked the hub. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
285 lines
7.7 KiB
Svelte
285 lines
7.7 KiB
Svelte
<script lang="ts">
|
|
import { OauthService, type ResourceType } from '$lib/gen'
|
|
import FilesetEditor from './FilesetEditor.svelte'
|
|
import { workspaceStore } from '$lib/stores'
|
|
import { emptySchema, emptyString } from '$lib/utils'
|
|
import SchemaForm from './SchemaForm.svelte'
|
|
import Toggle from './Toggle.svelte'
|
|
import TestConnection from './TestConnection.svelte'
|
|
import SupabaseIcon from './icons/SupabaseIcon.svelte'
|
|
import Popover from './meltComponents/Popover.svelte'
|
|
import Button from './common/button/Button.svelte'
|
|
import { Loader2 } from 'lucide-svelte'
|
|
import { untrack } from 'svelte'
|
|
import { base } from '$lib/base'
|
|
import GitHubAppIntegration from './GitHubAppIntegration.svelte'
|
|
import BedrockCredentialsCheck from './BedrockCredentialsCheck.svelte'
|
|
import { isCloudHosted } from '$lib/cloud'
|
|
import ResourceGen from './copilot/ResourceGen.svelte'
|
|
import SyncResourceTypes from './SyncResourceTypes.svelte'
|
|
|
|
interface Props {
|
|
resourceType: string
|
|
resourceTypeInfo: ResourceType | undefined
|
|
args?: Record<string, any> | any
|
|
linkedSecrets?: string[]
|
|
isValid?: boolean
|
|
linkedSecretCandidates?: string[] | undefined
|
|
description?: string | undefined
|
|
onSynced?: () => void
|
|
}
|
|
|
|
let {
|
|
resourceType,
|
|
resourceTypeInfo,
|
|
args = $bindable({}),
|
|
linkedSecrets = $bindable([]),
|
|
isValid = $bindable(true),
|
|
linkedSecretCandidates = undefined,
|
|
description = $bindable(undefined),
|
|
onSynced = undefined
|
|
}: Props = $props()
|
|
|
|
let schema = $state(emptySchema())
|
|
let notFound = $state(false)
|
|
|
|
let supabaseWizard = $state(false)
|
|
|
|
async function isSupabaseAvailable() {
|
|
try {
|
|
supabaseWizard = ((await OauthService.listOauthConnects()) ?? []).some(
|
|
(c) => c.name === 'supabase_wizard'
|
|
)
|
|
} catch (error) {}
|
|
}
|
|
async function loadSchema() {
|
|
if (!resourceTypeInfo) return
|
|
rawCode = '{}'
|
|
viewJsonSchema = false
|
|
try {
|
|
schema = resourceTypeInfo.schema as any
|
|
schema.order = schema.order ?? Object.keys(schema.properties).sort()
|
|
notFound = false
|
|
} catch (e) {
|
|
notFound = true
|
|
}
|
|
}
|
|
|
|
function parseJson() {
|
|
try {
|
|
args = JSON.parse(rawCode)
|
|
error = ''
|
|
isValid = true
|
|
} catch (e) {
|
|
isValid = false
|
|
error = e.message
|
|
}
|
|
}
|
|
let error = $state('')
|
|
let rawCode = $state('')
|
|
let viewJsonSchema = $state(false)
|
|
|
|
function switchTab(asJson: boolean) {
|
|
viewJsonSchema = asJson
|
|
if (asJson) {
|
|
rawCode = JSON.stringify(args, null, 2)
|
|
} else {
|
|
parseJson()
|
|
if (resourceTypeInfo?.format_extension && !resourceTypeInfo?.is_fileset) {
|
|
textFileContent = args.content
|
|
}
|
|
}
|
|
}
|
|
|
|
let connectionString = $state('')
|
|
let validConnectionString = $state(true)
|
|
function parseConnectionString(close: (_: any) => void) {
|
|
const regex =
|
|
/postgres(?:ql)?:\/\/(?<user>[^:@]+)(?::(?<password>[^@]+))?@(?<host>[^:\/?]+)(?::(?<port>\d+))?\/(?<dbname>[^\?]+)?(?:\?.*sslmode=(?<sslmode>[^&]+))?/
|
|
const match = connectionString.match(regex)
|
|
if (match) {
|
|
validConnectionString = true
|
|
const { user, password, host, port, dbname, sslmode } = match.groups!
|
|
rawCode = JSON.stringify(
|
|
{
|
|
...args,
|
|
user,
|
|
password: password || args?.password,
|
|
host,
|
|
port: (port ? Number(port) : undefined) || args?.port,
|
|
dbname: dbname || args?.dbname,
|
|
sslmode: sslmode || args?.sslmode
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
rawCodeEditor?.setCode(rawCode)
|
|
close(null)
|
|
} else {
|
|
validConnectionString = false
|
|
}
|
|
}
|
|
|
|
let rawCodeEditor: { setCode: (code: string) => void } | undefined = $state(undefined)
|
|
let textFileContent: string | undefined = $state(undefined)
|
|
|
|
function parseTextFileContent() {
|
|
args = {
|
|
content: textFileContent
|
|
}
|
|
}
|
|
$effect(() => {
|
|
$workspaceStore && untrack(() => loadSchema())
|
|
})
|
|
$effect(() => {
|
|
notFound && rawCode && untrack(() => parseJson())
|
|
})
|
|
$effect(() => {
|
|
rawCode && untrack(() => parseJson())
|
|
})
|
|
$effect(() => {
|
|
textFileContent && untrack(() => parseTextFileContent())
|
|
})
|
|
$effect(() => {
|
|
resourceType == 'postgresql' && untrack(() => isSupabaseAvailable())
|
|
})
|
|
</script>
|
|
|
|
{#if !notFound}
|
|
<div class="w-full flex gap-2 flex-row-reverse items-center">
|
|
<Toggle
|
|
on:change={(e) => switchTab(e.detail)}
|
|
options={{
|
|
right: 'As JSON'
|
|
}}
|
|
class="as-json-toggle"
|
|
/>
|
|
<ResourceGen
|
|
bind:args
|
|
{resourceType}
|
|
resourceSchema={notFound ? undefined : schema}
|
|
isFileset={resourceTypeInfo?.is_fileset ?? false}
|
|
/>
|
|
<TestConnection {resourceType} {args} />
|
|
{#if resourceType == 'postgresql'}
|
|
<Popover
|
|
floatingConfig={{
|
|
placement: 'bottom'
|
|
}}
|
|
>
|
|
{#snippet trigger()}
|
|
<Button spacingSize="sm" size="xs" variant="default" nonCaptureEvent>
|
|
From connection string
|
|
</Button>
|
|
{/snippet}
|
|
{#snippet content({ close })}
|
|
<div class="block text-primary p-4">
|
|
<div class="w-[550px] flex flex-col items-start gap-1">
|
|
<div class="flex flex-row gap-1 w-full">
|
|
<input
|
|
type="text"
|
|
bind:value={connectionString}
|
|
placeholder="postgres://user:password@host:5432/dbname?sslmode=disable"
|
|
/>
|
|
<Button
|
|
size="xs"
|
|
color="blue"
|
|
buttonType="button"
|
|
on:click={() => {
|
|
parseConnectionString(close)
|
|
}}
|
|
disabled={connectionString.length <= 0}
|
|
>
|
|
Apply
|
|
</Button>
|
|
</div>
|
|
{#if !validConnectionString}
|
|
<p class="text-red-500 text-xs">Could not parse connection string</p>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/snippet}
|
|
</Popover>
|
|
{/if}
|
|
{#if resourceType == 'postgresql' && supabaseWizard}
|
|
<a
|
|
target="_blank"
|
|
href="{base}/api/oauth/connect/supabase_wizard"
|
|
class="border rounded-lg flex flex-row gap-2 items-center text-xs px-3 py-1.5 h-8 bg-[#F1F3F5] hover:bg-[#E6E8EB] dark:bg-[#1C1C1C] dark:hover:bg-black"
|
|
>
|
|
<SupabaseIcon height="16px" width="16px" />
|
|
<div class="text-[#11181C] dark:text-[#EDEDED] font-semibold">Connect Supabase</div>
|
|
</a>
|
|
{/if}
|
|
<GitHubAppIntegration
|
|
{resourceType}
|
|
{args}
|
|
{description}
|
|
onArgsUpdate={(newArgs) => {
|
|
args = newArgs
|
|
rawCode = JSON.stringify(args, null, 2)
|
|
rawCodeEditor?.setCode(rawCode)
|
|
}}
|
|
onDescriptionUpdate={(newDescription) => (description = newDescription)}
|
|
/>
|
|
</div>
|
|
{#if resourceType?.includes('bedrock') && !isCloudHosted()}
|
|
<BedrockCredentialsCheck />
|
|
{/if}
|
|
{:else}
|
|
<p class="text-primary font-normal text-xs mb-4"
|
|
>No corresponding resource type found in your workspace for {resourceType}. Define the value in
|
|
JSON directly</p
|
|
>
|
|
<SyncResourceTypes {resourceType} {onSynced} />
|
|
{/if}
|
|
{#if notFound || viewJsonSchema}
|
|
{#if !emptyString(error)}<span class="text-red-400 text-xs mb-1 flex flex-row-reverse"
|
|
>{error}</span
|
|
>{:else}<div class="py-2"></div>{/if}
|
|
<div class="h-full w-full border p-1 rounded">
|
|
{#await import('$lib/components/SimpleEditor.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default
|
|
bind:this={rawCodeEditor}
|
|
autoHeight
|
|
lang="json"
|
|
bind:code={rawCode}
|
|
fixedOverflowWidgets={false}
|
|
/>
|
|
{/await}
|
|
</div>
|
|
{:else if resourceTypeInfo?.is_fileset}
|
|
<h5 class="mt-1 inline-flex items-center gap-4"> Fileset </h5>
|
|
<FilesetEditor bind:args />
|
|
{:else if resourceTypeInfo?.format_extension}
|
|
<h5 class="mt-4 inline-flex items-center gap-4">
|
|
File content ({resourceTypeInfo.format_extension})
|
|
</h5>
|
|
<div class="py-2"></div>
|
|
<div class="h-full w-full border p-1 rounded">
|
|
{#await import('$lib/components/SimpleEditor.svelte')}
|
|
<Loader2 class="animate-spin" />
|
|
{:then Module}
|
|
<Module.default
|
|
bind:this={rawCodeEditor}
|
|
autoHeight
|
|
lang={resourceTypeInfo.format_extension}
|
|
bind:code={textFileContent}
|
|
fixedOverflowWidgets={false}
|
|
/>
|
|
{/await}
|
|
</div>
|
|
{:else}
|
|
<SchemaForm
|
|
onlyMaskPassword
|
|
noDelete
|
|
{linkedSecretCandidates}
|
|
bind:linkedSecrets
|
|
isValid
|
|
{schema}
|
|
bind:args
|
|
/>
|
|
{/if}
|