mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-09-05 08:02:18 +00:00
feat: Resource type description is now displayed when creating a new resource (#2834)
This commit is contained in:
+3
-2
@@ -84,7 +84,8 @@ async function pull(opts: GlobalOptions) {
|
||||
(y) =>
|
||||
y.name === x.name &&
|
||||
typeof y.schema !== "string" &&
|
||||
deepEqual(y.schema, x.schema)
|
||||
deepEqual(y.schema, x.schema) &&
|
||||
y.description === x.description
|
||||
)
|
||||
) {
|
||||
log.info("skipping " + x.name + " (same as current)");
|
||||
@@ -106,7 +107,7 @@ const command = new Command()
|
||||
.name("hub")
|
||||
.description("Hub related commands. EXPERIMENTAL. INTERNAL USE ONLY.")
|
||||
.command("pull")
|
||||
.description("pull any supported defintions. EXPERIMENTAL.")
|
||||
.description("pull any supported definitions. EXPERIMENTAL.")
|
||||
.action(pull as any);
|
||||
|
||||
export default command;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { OauthService, ResourceService } from '$lib/gen'
|
||||
import { OauthService, type ResourceType } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema, emptyString } from '$lib/utils'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
@@ -10,7 +10,8 @@
|
||||
import Popup from './common/popup/Popup.svelte'
|
||||
import Button from './common/button/Button.svelte'
|
||||
|
||||
export let resource_type: string
|
||||
export let resourceType: string
|
||||
export let resourceTypeInfo: ResourceType | undefined
|
||||
export let args: Record<string, any> | any = {}
|
||||
export let linkedSecret: string | undefined = undefined
|
||||
export let isValid = true
|
||||
@@ -25,14 +26,11 @@
|
||||
supabaseWizard = (await OauthService.listOAuthConnects())['supabase_wizard'] != undefined
|
||||
}
|
||||
async function loadSchema() {
|
||||
if (!resourceTypeInfo) return
|
||||
rawCode = '{}'
|
||||
viewJsonSchema = false
|
||||
try {
|
||||
const rt = await ResourceService.getResourceType({
|
||||
workspace: $workspaceStore!,
|
||||
path: resource_type
|
||||
})
|
||||
schema = rt.schema
|
||||
schema = resourceTypeInfo.schema
|
||||
notFound = false
|
||||
} catch (e) {
|
||||
notFound = true
|
||||
@@ -68,7 +66,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
$: resource_type == 'postgresql' && isSupabaseAvailable()
|
||||
$: resourceType == 'postgresql' && isSupabaseAvailable()
|
||||
|
||||
let connectionString = ''
|
||||
let validConnectionString = true
|
||||
@@ -111,8 +109,8 @@
|
||||
right: 'As JSON'
|
||||
}}
|
||||
/>
|
||||
<TestConnection {resource_type} {args} />
|
||||
{#if resource_type == 'postgresql'}
|
||||
<TestConnection {resourceType} {args} />
|
||||
{#if resourceType == 'postgresql'}
|
||||
<Popup
|
||||
let:close
|
||||
floatingConfig={{
|
||||
@@ -158,7 +156,7 @@
|
||||
</div>
|
||||
</Popup>
|
||||
{/if}
|
||||
{#if resource_type == 'postgresql' && supabaseWizard}
|
||||
{#if resourceType == 'postgresql' && supabaseWizard}
|
||||
<a
|
||||
target="_blank"
|
||||
href="/api/oauth/connect/supabase_wizard"
|
||||
@@ -171,7 +169,7 @@
|
||||
</div>
|
||||
{:else}
|
||||
<p class="italic text-tertiary text-xs mb-4"
|
||||
>No corresponding resource type found in your workspace for {resource_type}. Define the value in
|
||||
>No corresponding resource type found in your workspace for {resourceType}. Define the value in
|
||||
JSON directly</p
|
||||
>
|
||||
{/if}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
airtable: {
|
||||
img: '/airtable_connect.png',
|
||||
instructions: [
|
||||
'Click to <a href="https://airtable.com/create/tokens" target="_blank" rel=”noopener noreferrer”>https://airtable.com/create/tokens</a>',
|
||||
'Go to <a href="https://airtable.com/create/tokens" target="_blank" rel=”noopener noreferrer”>https://airtable.com/create/tokens</a>',
|
||||
'Click on "Create new token"',
|
||||
'Set a name, specify the scopes or the access level and click on "Create token"',
|
||||
'Copy the token'
|
||||
@@ -24,11 +24,6 @@
|
||||
'Find "API Token"'
|
||||
]
|
||||
},
|
||||
git_repository: {
|
||||
instructions: [
|
||||
'Only token-based authentication it supported for this resource type right now. When authentication is needed, the username and token can be included directly in the URL (i.e. https://[USERNAME]:[TOKEN]@github.com/[ORG|USER]/[REPO_NAME].git).'
|
||||
]
|
||||
},
|
||||
mailchimp: {
|
||||
img: '/mailchimp_connect.png',
|
||||
instructions: [
|
||||
@@ -63,8 +58,14 @@
|
||||
<script lang="ts">
|
||||
import { oauthStore, workspaceStore } from '$lib/stores'
|
||||
import IconedResourceType from './IconedResourceType.svelte'
|
||||
import { OauthService, ResourceService, VariableService, type TokenResponse } from '$lib/gen'
|
||||
import { emptyString, truncateRev } from '$lib/utils'
|
||||
import {
|
||||
OauthService,
|
||||
ResourceService,
|
||||
VariableService,
|
||||
type TokenResponse,
|
||||
type ResourceType
|
||||
} from '$lib/gen'
|
||||
import { emptyString, truncateRev, urlize } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Path from './Path.svelte'
|
||||
import { Button, Drawer, Skeleton } from './common'
|
||||
@@ -76,6 +77,7 @@
|
||||
import { sendUserToast } from '$lib/toast'
|
||||
import OauthScopes from './OauthScopes.svelte'
|
||||
import DarkModeObserver from './DarkModeObserver.svelte'
|
||||
import Markdown from 'svelte-exmarkdown'
|
||||
|
||||
export let newPageOAuth = false
|
||||
|
||||
@@ -100,8 +102,8 @@
|
||||
| undefined = undefined
|
||||
let args: any = {}
|
||||
|
||||
$: linkedSecretCandidates = apiTokenApps[resource_type]?.linkedSecret
|
||||
? ([apiTokenApps[resource_type]?.linkedSecret] as string[])
|
||||
$: linkedSecretCandidates = apiTokenApps[resourceType]?.linkedSecret
|
||||
? ([apiTokenApps[resourceType]?.linkedSecret] as string[])
|
||||
: args != undefined
|
||||
? Object.keys(args).filter((x) =>
|
||||
['token', 'secret', 'key', 'pass', 'private'].some((y) => x.toLowerCase().includes(y))
|
||||
@@ -142,7 +144,8 @@
|
||||
let description = ''
|
||||
|
||||
let drawer: Drawer
|
||||
let resource_type = ''
|
||||
let resourceType = ''
|
||||
let resourceTypeInfo: ResourceType | undefined = undefined
|
||||
let step = 1
|
||||
|
||||
let no_back = false
|
||||
@@ -154,11 +157,11 @@
|
||||
value = ''
|
||||
description = ''
|
||||
no_back = false
|
||||
resource_type = rt ?? ''
|
||||
resourceType = rt ?? ''
|
||||
|
||||
await loadConnects()
|
||||
|
||||
const connect = connects?.[resource_type]
|
||||
const connect = connects?.[resourceType]
|
||||
if (connect) {
|
||||
scopes = connect.scopes
|
||||
extra_params = Object.entries(connect.extra_params ?? {})
|
||||
@@ -172,7 +175,7 @@
|
||||
}
|
||||
|
||||
export function openFromOauth(rt: string) {
|
||||
resource_type = rt
|
||||
resourceType = rt
|
||||
value = $oauthStore?.access_token!
|
||||
valueToken = $oauthStore!
|
||||
$oauthStore = undefined
|
||||
@@ -220,10 +223,14 @@
|
||||
|
||||
async function next() {
|
||||
if (step == 1 && manual) {
|
||||
resourceTypeInfo = await ResourceService.getResourceType({
|
||||
workspace: $workspaceStore!,
|
||||
path: resourceType
|
||||
})
|
||||
step += 1
|
||||
args = {}
|
||||
} else if (step == 1 && !manual) {
|
||||
const url = new URL(`/api/oauth/connect/${resource_type}`, window.location.origin)
|
||||
const url = new URL(`/api/oauth/connect/${resourceType}`, window.location.origin)
|
||||
url.searchParams.append('scopes', scopes.join('+'))
|
||||
if (extra_params.length > 0) {
|
||||
extra_params.forEach(([key, value]) => url.searchParams.append(key, value))
|
||||
@@ -260,7 +267,7 @@
|
||||
refresh_token: valueToken.refresh_token ?? '',
|
||||
expires_in: valueToken.expires_in,
|
||||
owner: path.split('/').slice(0, 2).join('/'),
|
||||
client: resource_type
|
||||
client: resourceType
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -278,7 +285,7 @@
|
||||
value: v,
|
||||
is_secret: true,
|
||||
description: emptyString(description)
|
||||
? `${manual ? 'Token' : 'OAuth token'} for ${resource_type}`
|
||||
? `${manual ? 'Token' : 'OAuth token'} for ${resourceType}`
|
||||
: description,
|
||||
is_oauth: !manual,
|
||||
account: account
|
||||
@@ -291,7 +298,7 @@
|
||||
await ResourceService.createResource({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
resource_type,
|
||||
resource_type: resourceType,
|
||||
path,
|
||||
value: resourceValue,
|
||||
description
|
||||
@@ -313,14 +320,14 @@
|
||||
|
||||
$: isGoogleSignin =
|
||||
step == 1 &&
|
||||
(resource_type == 'google' ||
|
||||
resource_type == 'gmail' ||
|
||||
resource_type == 'gcal' ||
|
||||
resource_type == 'gdrive' ||
|
||||
resource_type == 'gsheets')
|
||||
(resourceType == 'google' ||
|
||||
resourceType == 'gmail' ||
|
||||
resourceType == 'gcal' ||
|
||||
resourceType == 'gdrive' ||
|
||||
resourceType == 'gsheets')
|
||||
|
||||
$: disabled =
|
||||
(step == 1 && resource_type == '') ||
|
||||
(step == 1 && resourceType == '') ||
|
||||
(step == 2 &&
|
||||
value == '' &&
|
||||
args &&
|
||||
@@ -388,11 +395,11 @@
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={key === resource_type ? 'blue' : 'light'}
|
||||
btnClasses={key === resource_type ? '!border-2' : 'm-[1px]'}
|
||||
color={key === resourceType ? 'blue' : 'light'}
|
||||
btnClasses={key === resourceType ? '!border-2' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
manual = false
|
||||
resource_type = key
|
||||
resourceType = key
|
||||
scopes = values.scopes
|
||||
extra_params = Object.entries(values.extra_params ?? {})
|
||||
}}
|
||||
@@ -414,9 +421,9 @@
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if manual == false && resource_type != ''}
|
||||
{#if manual == false && resourceType != ''}
|
||||
<h3>Scopes</h3>
|
||||
{#if !manual && resource_type != ''}
|
||||
{#if !manual && resourceType != ''}
|
||||
<OauthScopes bind:scopes />
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -437,11 +444,11 @@
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={key === resource_type ? 'blue' : 'light'}
|
||||
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
color={key === resourceType ? 'blue' : 'light'}
|
||||
btnClasses={key === resourceType ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
manual = true
|
||||
resource_type = key
|
||||
resourceType = key
|
||||
next()
|
||||
dispatch('click')
|
||||
}}
|
||||
@@ -462,11 +469,11 @@
|
||||
<Button
|
||||
size="sm"
|
||||
variant="border"
|
||||
color={key === resource_type ? 'blue' : 'light'}
|
||||
btnClasses={key === resource_type ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
color={key === resourceType ? 'blue' : 'light'}
|
||||
btnClasses={key === resourceType ? '!border-2 !bg-blue-50/75' : 'm-[1px]'}
|
||||
on:click={() => {
|
||||
manual = true
|
||||
resource_type = key
|
||||
resourceType = key
|
||||
next()
|
||||
dispatch('click')
|
||||
}}
|
||||
@@ -486,38 +493,44 @@
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
initialPath=""
|
||||
namePlaceholder={resource_type}
|
||||
namePlaceholder={resourceType}
|
||||
kind="resource"
|
||||
/>
|
||||
|
||||
{#if apiTokenApps[resource_type]}
|
||||
{#if apiTokenApps[resourceType]}
|
||||
<h2 class="mt-4 mb-2">Instructions</h2>
|
||||
<div class="pl-10">
|
||||
<ol class="list-decimal">
|
||||
{#each apiTokenApps[resource_type].instructions as step}
|
||||
{#each apiTokenApps[resourceType].instructions as step}
|
||||
<li>
|
||||
{@html step}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
{#if apiTokenApps[resource_type].img}
|
||||
{#if apiTokenApps[resourceType].img}
|
||||
<div class="mt-4 w-full overflow-hidden">
|
||||
<img class="m-auto max-h-60" alt="connect" src={apiTokenApps[resource_type].img} />
|
||||
<img class="m-auto max-h-60" alt="connect" src={apiTokenApps[resourceType].img} />
|
||||
</div>
|
||||
{/if}
|
||||
{:else if !emptyString(resourceTypeInfo?.description)}
|
||||
<h2 class="mt-4 mb-2">Resource type description</h2>
|
||||
<div>
|
||||
<Markdown md={urlize(resourceTypeInfo?.description ?? '', 'md')} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if resource_type == 'postgresql' || resource_type == 'mysql' || resource_type == 'mongodb'}
|
||||
{#if resourceType == 'postgresql' || resourceType == 'mysql' || resourceType == 'mongodb'}
|
||||
<WhitelistIp />
|
||||
{/if}
|
||||
|
||||
<h2 class="mt-4">Value</h2>
|
||||
<div class="mt-4">
|
||||
{#key resource_type}
|
||||
{#key resourceTypeInfo}
|
||||
<ApiConnectForm
|
||||
{linkedSecret}
|
||||
{linkedSecretCandidates}
|
||||
{resource_type}
|
||||
{resourceType}
|
||||
{resourceTypeInfo}
|
||||
bind:args
|
||||
bind:isValid
|
||||
/>
|
||||
@@ -529,15 +542,15 @@
|
||||
{:else}
|
||||
<Path
|
||||
initialPath=""
|
||||
namePlaceholder={resource_type}
|
||||
namePlaceholder={resourceType}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
kind="resource"
|
||||
/>
|
||||
{#if apiTokenApps[resource_type] || !manual}
|
||||
{#if apiTokenApps[resourceType] || !manual}
|
||||
<ul class="mt-10">
|
||||
<li>
|
||||
1. A secret variable containing the {apiTokenApps[resource_type]?.linkedSecret ??
|
||||
1. A secret variable containing the {apiTokenApps[resourceType]?.linkedSecret ??
|
||||
'token'}
|
||||
<span class="font-bold">{truncateRev(value, 5, '*****')}</span>
|
||||
will be stored a
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
right: 'As JSON'
|
||||
}}
|
||||
/>
|
||||
<TestConnection resource_type={resourceToEdit?.resource_type} {args} />
|
||||
<TestConnection resourceType={resourceToEdit?.resource_type} {args} />
|
||||
</div>
|
||||
<div class="text-sm">
|
||||
{#if loadingSchema}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { tryEvery } from '$lib/utils'
|
||||
|
||||
export let resource_type: string | undefined
|
||||
export let resourceType: string | undefined
|
||||
export let args: Record<string, any> | any = {}
|
||||
|
||||
const scripts: {
|
||||
@@ -70,10 +70,10 @@
|
||||
|
||||
let loading = false
|
||||
async function testConnection() {
|
||||
if (!resource_type) return
|
||||
if (!resourceType) return
|
||||
loading = true
|
||||
|
||||
const resourceScript = scripts[resource_type]
|
||||
const resourceScript = scripts[resourceType]
|
||||
|
||||
const job = await JobService.runScriptPreview({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -124,7 +124,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if Object.keys(scripts).includes(resource_type || '')}
|
||||
{#if Object.keys(scripts).includes(resourceType || '')}
|
||||
<Button
|
||||
spacingSize="sm"
|
||||
size="xs"
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { urlize } from '$lib/utils'
|
||||
|
||||
export let text: string
|
||||
const parseURL = (input) => {
|
||||
if (input.startsWith('http://') || input.startsWith('https://')) {
|
||||
return `<a href="${input}" target="_blank" rel="noopener noreferrer">${input}</a>`
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
||||
const parseInput = (input) => {
|
||||
if (!input) return ''
|
||||
const words = input.split(' ')
|
||||
return words.map((word) => parseURL(word)).join(' ')
|
||||
}
|
||||
|
||||
$: parsed = text ? text.split('\n').map(parseInput).join('\n') : ''
|
||||
$: parsed = text ? urlize(text, 'html') : ''
|
||||
</script>
|
||||
|
||||
{@html parsed}
|
||||
|
||||
@@ -764,3 +764,30 @@ export function computeShow(argName: string, expr: string | undefined, args: any
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function urlizeTokenInternal(token: string, formatter: "html" | "md"): string {
|
||||
if (token.startsWith('http://') || token.startsWith('https://')) {
|
||||
if (formatter == "html") {
|
||||
return `<a href="${token}" target="_blank" rel="noopener noreferrer">${token}</a>`
|
||||
} else {
|
||||
return `[${token}](${token})`
|
||||
}
|
||||
} else {
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
export function urlize(input: string, formatter: "html" | "md"): string {
|
||||
if (!input) return ''
|
||||
|
||||
return input
|
||||
.split('\n')
|
||||
.map(line => {
|
||||
return line
|
||||
.split(' ')
|
||||
.map((word) => urlizeTokenInternal(word, formatter))
|
||||
.join(' ')
|
||||
})
|
||||
.join('\n')
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user