mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 00:00:33 +00:00
feat: support to set linked secret variable to any field of a newly created resource
This commit is contained in:
@@ -10,8 +10,9 @@
|
||||
|
||||
export let resource_type: string
|
||||
export let args: Record<string, any> | any = {}
|
||||
export let password: string
|
||||
export let linkedSecret: string | undefined = undefined
|
||||
export let isValid = true
|
||||
export let linkedSecretCandidates: string[] | undefined = undefined
|
||||
|
||||
let schema = emptySchema()
|
||||
let notFound = false
|
||||
@@ -100,5 +101,5 @@
|
||||
>{:else}<div class="py-2" />{/if}
|
||||
<SimpleEditor autoHeight lang="json" bind:code={rawCode} fixedOverflowWidgets={false} />
|
||||
{:else}
|
||||
<SchemaForm noDelete {password} isValid {schema} bind:args />
|
||||
<SchemaForm noDelete {linkedSecretCandidates} bind:linkedSecret isValid {schema} bind:args />
|
||||
{/if}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script lang="ts" context="module">
|
||||
const apiTokenApps: Record<string, { img?: string; instructions: string[]; key?: string }> = {
|
||||
const apiTokenApps: Record<
|
||||
string,
|
||||
{ img?: string; instructions: string[]; linkedSecret?: string }
|
||||
> = {
|
||||
airtable: {
|
||||
img: '/airtable_connect.png',
|
||||
instructions: [
|
||||
@@ -12,7 +15,7 @@
|
||||
discord_webhook: {
|
||||
img: '/discord_webhook.png',
|
||||
instructions: ['Click on Server Settings', 'Click on Integration', 'Find "Webhooks"'],
|
||||
key: 'webhook_url'
|
||||
linkedSecret: 'webhook_url'
|
||||
},
|
||||
toggl: {
|
||||
img: '/toggl_connect.png',
|
||||
@@ -83,11 +86,39 @@
|
||||
| undefined = undefined
|
||||
let args: any = {}
|
||||
|
||||
$: key =
|
||||
apiTokenApps[resource_type]?.key ??
|
||||
(args != undefined
|
||||
? Object.keys(args).filter((x) => ['token', 'password', 'api_key', 'key'].includes(x))[0]
|
||||
: undefined)
|
||||
$: linkedSecretCandidates = apiTokenApps[resource_type]?.linkedSecret
|
||||
? ([apiTokenApps[resource_type]?.linkedSecret] as string[])
|
||||
: args != undefined
|
||||
? Object.keys(args).filter((x) =>
|
||||
['token', 'secret', 'key', 'pass', 'private'].some((y) => x.toLowerCase().includes(y))
|
||||
)
|
||||
: undefined
|
||||
|
||||
function linkedSecretValue(x: string) {
|
||||
let r = 0
|
||||
if (x.includes('secret')) {
|
||||
r += 10
|
||||
}
|
||||
if (x.includes('password')) {
|
||||
r += 5
|
||||
}
|
||||
if (x.includes('private')) {
|
||||
r += 4
|
||||
}
|
||||
if (x.includes('key')) {
|
||||
r += 3
|
||||
}
|
||||
if (x.includes('token')) {
|
||||
r += 2
|
||||
}
|
||||
if (x.includes('pass')) {
|
||||
r += 1
|
||||
}
|
||||
return r
|
||||
}
|
||||
$: linkedSecret = linkedSecretCandidates?.sort(
|
||||
(ua, ub) => linkedSecretValue(ua) - linkedSecretValue(ub)
|
||||
)?.[0]
|
||||
|
||||
let scopes: string[] = []
|
||||
let extra_params: [string, string][] = []
|
||||
@@ -159,7 +190,7 @@
|
||||
apiTokenApps[x] ?? {
|
||||
instructions: '',
|
||||
img: undefined,
|
||||
key: undefined
|
||||
linkedSecret: undefined
|
||||
}
|
||||
])
|
||||
}
|
||||
@@ -214,12 +245,12 @@
|
||||
|
||||
const resourceValue = args
|
||||
|
||||
if (!manual || key != undefined) {
|
||||
if (!manual || linkedSecret != undefined) {
|
||||
await VariableService.createVariable({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
value: manual ? args[key ?? ''] : value,
|
||||
value: manual ? args[linkedSecret ?? ''] : value,
|
||||
is_secret: true,
|
||||
description: emptyString(description)
|
||||
? `${manual ? 'Token' : 'OAuth token'} for ${resource_type}`
|
||||
@@ -228,7 +259,7 @@
|
||||
account: account
|
||||
}
|
||||
})
|
||||
resourceValue[key ?? 'token'] = `$var:${path}`
|
||||
resourceValue[linkedSecret ?? 'token'] = `$var:${path}`
|
||||
}
|
||||
|
||||
await ResourceService.createResource({
|
||||
@@ -271,7 +302,7 @@
|
||||
args['password'] == '' &&
|
||||
args['api_key'] == '' &&
|
||||
args['key'] == '' &&
|
||||
key != undefined) ||
|
||||
linkedSecret != undefined) ||
|
||||
(step == 3 && pathError != '') ||
|
||||
!isValid
|
||||
|
||||
@@ -450,7 +481,13 @@
|
||||
<h2 class="mt-4">Value</h2>
|
||||
<div class="mt-4">
|
||||
{#key resource_type}
|
||||
<ApiConnectForm password={key ?? ''} {resource_type} bind:args bind:isValid />
|
||||
<ApiConnectForm
|
||||
{linkedSecret}
|
||||
{linkedSecretCandidates}
|
||||
{resource_type}
|
||||
bind:args
|
||||
bind:isValid
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
@@ -467,7 +504,8 @@
|
||||
{#if apiTokenApps[resource_type] || !manual}
|
||||
<ul class="mt-10">
|
||||
<li>
|
||||
1. A secret variable containing the {apiTokenApps[resource_type]?.key ?? 'token'}
|
||||
1. A secret variable containing the {apiTokenApps[resource_type]?.linkedSecret ??
|
||||
'token'}
|
||||
<span class="font-bold">{truncateRev(value, 5, '*****')}</span>
|
||||
will be stored a
|
||||
<span class="font-mono whitespace-nowrap">{path}</span>.
|
||||
|
||||
@@ -450,7 +450,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if variableEditor}
|
||||
<div class="text-sm text-tertiary-inverse">
|
||||
<div class="text-sm text-tertiary">
|
||||
{#if value && typeof value == 'string' && value?.startsWith('$var:')}
|
||||
Linked to variable <button
|
||||
class="text-blue-500 underline"
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
<Button
|
||||
variant="border"
|
||||
color="light"
|
||||
btnClasses="text-tertiary {small ? 'text-xs' : ''} "
|
||||
btnClasses="text-primary {small ? 'text-xs' : ''} "
|
||||
on:click={() => (open = !open)}
|
||||
>
|
||||
{text} <Icon data={open ? faChevronUp : faChevronDown} scale={0.5} />
|
||||
|
||||
@@ -103,7 +103,7 @@
|
||||
>
|
||||
{#if stepDetail == undefined}
|
||||
<div>
|
||||
<p class="font-medium text-tertiary text-center pt-4 pb-8">
|
||||
<p class="font-medium text-secondary text-center pt-4 pb-8">
|
||||
Click on a step to see its details
|
||||
</p>
|
||||
<h3 class="mb-2 font-semibold">Flow Inputs</h3>
|
||||
@@ -112,7 +112,7 @@
|
||||
{:else if stepDetail == 'Input'}
|
||||
<SchemaViewer schema={flow?.schema} />
|
||||
{:else if stepDetail == 'Result'}
|
||||
<p class="font-medium text-tertiary text-center pt-4 pb-8"> End of the flow </p>
|
||||
<p class="font-medium text-secondary text-center pt-4 pb-8"> End of the flow </p>
|
||||
{:else if typeof stepDetail != 'string' && stepDetail.value}
|
||||
<div class="">
|
||||
<div class="sticky top-0 bg-surface w-full flex items-center py-2">
|
||||
@@ -154,7 +154,7 @@
|
||||
{/if}
|
||||
</div>
|
||||
{#if stepDetail.value.type == 'identity'}
|
||||
<p class="font-medium text-tertiary text-center pt-4 pb-8">
|
||||
<p class="font-medium text-secondary text-center pt-4 pb-8">
|
||||
An identity step returns its inputs as outputs
|
||||
</p>
|
||||
{:else if stepDetail.value.type == 'rawscript'}
|
||||
@@ -204,7 +204,7 @@
|
||||
{/if}
|
||||
{:else if stepDetail.value.type == 'forloopflow'}
|
||||
<div>
|
||||
<p class="font-medium text-tertiary pb-2"> Iterator expression: </p>
|
||||
<p class="font-medium text-secondary pb-2"> Iterator expression: </p>
|
||||
{#if stepDetail.value.iterator.type == 'static'}
|
||||
<ObjectViewer json={stepDetail.value.iterator.value} />
|
||||
{:else}
|
||||
@@ -214,18 +214,18 @@
|
||||
{/if}
|
||||
</div>
|
||||
{:else if stepDetail.value.type == 'branchall'}
|
||||
<p class="font-medium text-tertiary text-center pt-4 pb-8">
|
||||
<p class="font-medium text-secondary text-center pt-4 pb-8">
|
||||
All branches will run, regardless of the inputs
|
||||
</p>
|
||||
{:else if stepDetail.value.type == 'branchone'}
|
||||
<p class="font-medium text-tertiary text-center pt-4 pb-8">
|
||||
<p class="font-medium text-secondary text-center pt-4 pb-8">
|
||||
Only one branch will run based on a predicate
|
||||
</p>
|
||||
{:else if stepDetail.value.type == 'flow'}
|
||||
<FlowPathViewer noSide path={stepDetail.value.path} />
|
||||
{/if}
|
||||
{:else}
|
||||
<p class="font-medium text-tertiary text-center pt-4 pb-8">
|
||||
<p class="font-medium text-secondary text-center pt-4 pb-8">
|
||||
Step {stepDetail} selected
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
</Cell>
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="flex flex-col text-xs text-tertiary">
|
||||
<div class="flex flex-col text-xs text-secondary">
|
||||
{#each Object.entries(usage) as [k, v]}
|
||||
<div>
|
||||
{k}: {v}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
>
|
||||
</div>
|
||||
<input
|
||||
class="block w-full py-2 px-2 {red ? '!border-red-500' : ''} text-sm js-password h-12"
|
||||
class="block w-full px-2 py-1 {red ? '!border-red-500' : ''} text-sm js-password h-9"
|
||||
id="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
import { Button } from './common'
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
import ToggleButtonGroup from './common/toggleButton-v2/ToggleButtonGroup.svelte'
|
||||
import ToggleButton from './common/toggleButton-v2/ToggleButton.svelte'
|
||||
|
||||
export let schema: Schema | any
|
||||
export let args: Record<string, any> = {}
|
||||
@@ -20,7 +22,8 @@
|
||||
|
||||
export let shouldHideNoInputs: boolean = false
|
||||
export let compact = false
|
||||
export let password: string | undefined = undefined
|
||||
export let linkedSecret: string | undefined = undefined
|
||||
export let linkedSecretCandidates: string[] | undefined = undefined
|
||||
export let noVariablePicker = false
|
||||
export let flexWrap = false
|
||||
export let noDelete = false
|
||||
@@ -99,7 +102,6 @@
|
||||
disabled={disabledArgs.includes(argName) || disabled}
|
||||
{editableSchema}
|
||||
{compact}
|
||||
password={argName == password}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
@@ -125,14 +127,43 @@
|
||||
properties={schema.properties[argName].properties}
|
||||
itemsType={schema.properties[argName].items}
|
||||
disabled={disabledArgs.includes(argName) || disabled}
|
||||
{editableSchema}
|
||||
{compact}
|
||||
password={argName == password}
|
||||
{variableEditor}
|
||||
{itemPicker}
|
||||
bind:pickForField
|
||||
password={linkedSecret == argName}
|
||||
extra={schema.properties[argName]}
|
||||
/>
|
||||
>
|
||||
<svelte:fragment slot="actions">
|
||||
{#if linkedSecretCandidates?.includes(argName)}
|
||||
<div>
|
||||
<ToggleButtonGroup
|
||||
selected={linkedSecret == argName}
|
||||
on:selected={(e) => {
|
||||
if (e.detail) {
|
||||
linkedSecret = argName
|
||||
} else if (linkedSecret == argName) {
|
||||
linkedSecret = undefined
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ToggleButton
|
||||
value={false}
|
||||
size="sm"
|
||||
label="Inlined"
|
||||
tooltip="The value is inlined in the resource and thus has no special treatment."
|
||||
/>
|
||||
<ToggleButton
|
||||
position="right"
|
||||
value={true}
|
||||
size="sm"
|
||||
label="Secret"
|
||||
tooltip="The value will be stored in a newly created linked secret variable at the same path. That variable can be permissioned differently, will be treated as a secret the UI, operators will not be able to load it and every access will generate a corresponding audit log."
|
||||
/>
|
||||
</ToggleButtonGroup>
|
||||
</div>{/if}</svelte:fragment
|
||||
>
|
||||
</ArgInput>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -161,7 +161,7 @@
|
||||
|
||||
<div>
|
||||
<div class="w-full flex justify-between items-center mb-1">
|
||||
<div class="text-xs text-tertiary font-semibold truncate">
|
||||
<div class="text-xs text-secondary font-semibold truncate">
|
||||
Background runnables
|
||||
<Tooltip
|
||||
class="mb-0.5"
|
||||
|
||||
@@ -26,14 +26,14 @@
|
||||
|
||||
<button
|
||||
class={twMerge(
|
||||
'border-b-2 py-1 px-4 cursor-pointer transition-all z-10 ease-linear font-medium text-tertiary',
|
||||
'border-b-2 py-1 px-4 cursor-pointer transition-all z-10 ease-linear font-medium text-primary',
|
||||
$selected?.startsWith(value)
|
||||
? 'wm-tab-active'
|
||||
: 'border-gray-300 dark:border-gray-600 border-opacity-0 hover:border-opacity-100 ',
|
||||
fontSizeClasses[size],
|
||||
c,
|
||||
$selected?.startsWith(value) ? selectedClass : '',
|
||||
disabled ? 'cursor-not-allowed text-gray-400' : ''
|
||||
disabled ? 'cursor-not-allowed text-tertiary' : ''
|
||||
)}
|
||||
style={`${style} ${$selected?.startsWith(value) ? selectedStyle : ''}`}
|
||||
on:click={() => {
|
||||
|
||||
@@ -9,7 +9,7 @@ const lightTheme = {
|
||||
|
||||
textPrimary: '#2d3748',
|
||||
textSecondary: '#4a5568',
|
||||
textTertiary: '#718096',
|
||||
textTertiary: '#505c70',
|
||||
textDisabled: '#a0aec0',
|
||||
|
||||
border: '#ccc',
|
||||
@@ -27,7 +27,7 @@ const darkTheme = {
|
||||
|
||||
textPrimary: '#f3f6f8',
|
||||
textSecondary: '#e0e7ed',
|
||||
textTertiary: '#d8dee9',
|
||||
textTertiary: '#c7ccd6',
|
||||
textDisabled: '#a0aec0',
|
||||
|
||||
border: '#3e4c60',
|
||||
@@ -478,7 +478,7 @@ const config = {
|
||||
|
||||
'--color-text-primary': lightThemeRgb.textPrimary,
|
||||
'--color-text-secondary': lightThemeRgb.textSecondary,
|
||||
'--color-text-tetiary': lightThemeRgb.textTertiary,
|
||||
'--color-text-tertiary': lightThemeRgb.textTertiary,
|
||||
'--color-text-disabled': lightThemeRgb.textDisabled,
|
||||
|
||||
'--color-surface-inverse': darkThemeRgb.surface,
|
||||
@@ -516,7 +516,7 @@ const config = {
|
||||
|
||||
'--color-text-primary': darkThemeRgb.textPrimary,
|
||||
'--color-text-secondary': darkThemeRgb.textSecondary,
|
||||
'--color-text-tetiary': darkThemeRgb.textTertiary,
|
||||
'--color-text-tertiary': darkThemeRgb.textTertiary,
|
||||
'--color-text-disabled': darkThemeRgb.textDisabled,
|
||||
|
||||
'--color-surface-inverse': lightThemeRgb.surface,
|
||||
|
||||
Reference in New Issue
Block a user