feat: unify resources under a single connect API
This commit is contained in:
@@ -1331,7 +1331,7 @@ paths:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ListableResource"
|
||||
$ref: "#/components/schemas/Resource"
|
||||
|
||||
/w/{workspace}/resources/get_value/{path}:
|
||||
get:
|
||||
|
||||
@@ -126,14 +126,14 @@ async fn list_apps(
|
||||
|
||||
let sqlb = SqlBuilder::select_from("app")
|
||||
.fields(&[
|
||||
"id",
|
||||
"app.id",
|
||||
"app.workspace_id",
|
||||
"app.path",
|
||||
"summary",
|
||||
"versions[array_upper(versions, 1)] as version",
|
||||
"policy->>'execution_mode' as execution_mode",
|
||||
"app.summary",
|
||||
"app.versions[array_upper(app.versions, 1)] as version",
|
||||
"app.policy->>'execution_mode' as execution_mode",
|
||||
"app_version.created_at as edited_at",
|
||||
"extra_perms",
|
||||
"app.extra_perms",
|
||||
"favorite.path IS NOT NULL as starred",
|
||||
])
|
||||
.left()
|
||||
|
||||
24
frontend/src/lib/components/ApiConnectForm.svelte
Normal file
24
frontend/src/lib/components/ApiConnectForm.svelte
Normal file
@@ -0,0 +1,24 @@
|
||||
<script lang="ts">
|
||||
import { ResourceService } from '$lib/gen'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { emptySchema } from '$lib/utils'
|
||||
import SchemaForm from './SchemaForm.svelte'
|
||||
|
||||
export let resource_type: string
|
||||
export let args: Record<string, any> = {}
|
||||
export let password: string
|
||||
|
||||
let schema = emptySchema()
|
||||
async function loadSchema() {
|
||||
const rt = await ResourceService.getResourceType({
|
||||
workspace: $workspaceStore!,
|
||||
path: resource_type
|
||||
})
|
||||
schema = rt.schema
|
||||
}
|
||||
$: {
|
||||
$workspaceStore && loadSchema()
|
||||
}
|
||||
</script>
|
||||
|
||||
<SchemaForm {password} isValid {schema} bind:args />
|
||||
@@ -2,19 +2,11 @@
|
||||
const apiTokenApps: Record<string, { img?: string; instructions: string[]; key?: string }> = {
|
||||
airtable: {
|
||||
img: 'airtable_connect.png',
|
||||
instructions: [
|
||||
'Click on the top-right avatar',
|
||||
'Click on Account',
|
||||
'Find "Api"'
|
||||
]
|
||||
instructions: ['Click on the top-right avatar', 'Click on Account', 'Find "Api"']
|
||||
},
|
||||
discord_webhook: {
|
||||
img: 'discord_webhook.png',
|
||||
instructions: [
|
||||
'Click on Server Settings',
|
||||
'Click on Integration',
|
||||
'Find "Webhooks"'
|
||||
],
|
||||
instructions: ['Click on Server Settings', 'Click on Integration', 'Find "Webhooks"'],
|
||||
key: 'webhook_url'
|
||||
},
|
||||
toggl: {
|
||||
@@ -30,7 +22,7 @@
|
||||
'Go to <a href="https://admin.mailchimp.com/account/api" target="_blank" rel=”noopener noreferrer”>https://admin.mailchimp.com/account/api</a>',
|
||||
'Find "Your API Keys"'
|
||||
]
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -43,10 +35,10 @@
|
||||
import { sendUserToast, truncateRev } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import Password from './Password.svelte'
|
||||
import Path from './Path.svelte'
|
||||
import { Alert, Button, Drawer } from './common'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import ApiConnectForm from './ApiConnectForm.svelte'
|
||||
|
||||
let manual = false
|
||||
let value: string = ''
|
||||
@@ -54,6 +46,7 @@
|
||||
let connects: Record<string, { scopes: string[]; extra_params?: Record<string, string> }> = {}
|
||||
let connectsManual: [string, { img?: string; instructions: string[]; key?: string }][] = []
|
||||
let key: string = 'token'
|
||||
let args = {}
|
||||
|
||||
$: key = apiTokenApps[resource_type]?.key ?? 'token'
|
||||
|
||||
@@ -83,7 +76,7 @@
|
||||
scopes = connect.scopes
|
||||
extra_params = Object.entries(connect.extra_params ?? {})
|
||||
}
|
||||
drawer.openDrawer()
|
||||
drawer.openDrawer?.()
|
||||
}
|
||||
|
||||
export function openFromOauth(rt: string) {
|
||||
@@ -94,7 +87,7 @@
|
||||
manual = false
|
||||
step = 3
|
||||
no_back = true
|
||||
drawer.openDrawer()
|
||||
drawer.openDrawer?.()
|
||||
}
|
||||
|
||||
async function loadConnects() {
|
||||
@@ -105,7 +98,16 @@
|
||||
const availableRts = await ResourceService.listResourceTypeNames({
|
||||
workspace: $workspaceStore!
|
||||
})
|
||||
connectsManual = Object.entries(apiTokenApps).filter(([key, _]) => availableRts.includes(key))
|
||||
connectsManual = availableRts
|
||||
.filter((x) => !Object.keys(connects).includes(x))
|
||||
.map((x) => [
|
||||
x,
|
||||
apiTokenApps[x] ?? {
|
||||
instructions: '',
|
||||
img: undefined,
|
||||
key: undefined
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
async function next() {
|
||||
@@ -150,18 +152,19 @@
|
||||
)
|
||||
}
|
||||
const description = `${manual ? 'Token' : 'OAuth token'} for ${resource_type}`
|
||||
|
||||
await VariableService.createVariable({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
path,
|
||||
value,
|
||||
value: manual ? args[key] : value,
|
||||
is_secret: true,
|
||||
description,
|
||||
is_oauth: !manual,
|
||||
account: account
|
||||
}
|
||||
})
|
||||
const resourceValue = {}
|
||||
const resourceValue = args
|
||||
resourceValue[key] = `$var:${path}`
|
||||
await ResourceService.createResource({
|
||||
workspace: $workspaceStore!,
|
||||
@@ -174,7 +177,7 @@
|
||||
})
|
||||
dispatch('refresh')
|
||||
sendUserToast(`App token set at resource and variable path: ${path}`)
|
||||
drawer.closeDrawer()
|
||||
drawer.closeDrawer?.()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +198,11 @@
|
||||
resource_type == 'gsheets')
|
||||
$: disabled =
|
||||
(step == 1 && resource_type == '') ||
|
||||
(step == 2 && value == '') ||
|
||||
(step == 2 &&
|
||||
value == '' &&
|
||||
args['token'] == '' &&
|
||||
args['password'] == '' &&
|
||||
args['api_key'] == '') ||
|
||||
(step == 3 && pathError != '')
|
||||
</script>
|
||||
|
||||
@@ -307,23 +314,26 @@
|
||||
</div>
|
||||
{:else if step == 2}
|
||||
{#if manual}
|
||||
<div class="mb-1 font-semibold text-gray-700 mt-6">Instructions</div>
|
||||
<div class="pl-10">
|
||||
<ol class="list-decimal">
|
||||
{#each apiTokenApps[resource_type].instructions as step}
|
||||
<li>
|
||||
{@html step}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
{#if apiTokenApps[resource_type].img}
|
||||
<div class="mt-4">
|
||||
<img alt="connect" src={apiTokenApps[resource_type].img} />
|
||||
{#if apiTokenApps[resource_type]}
|
||||
<div class="mb-1 font-semibold text-gray-700 mt-6">Instructions</div>
|
||||
<div class="pl-10">
|
||||
<ol class="list-decimal">
|
||||
{#each apiTokenApps[resource_type].instructions as step}
|
||||
<li>
|
||||
{@html step}
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</div>
|
||||
{#if apiTokenApps[resource_type].img}
|
||||
<div class="mt-4">
|
||||
<img alt="connect" src={apiTokenApps[resource_type].img} />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div class="mt-4">
|
||||
<Password bind:password={value} label="Paste token here" />
|
||||
<ApiConnectForm password={key} {resource_type} bind:args />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
@@ -333,27 +343,25 @@
|
||||
initialPath={`u/${$userStore?.username ?? ''}/my_${resource_type}`}
|
||||
kind="resource"
|
||||
/>
|
||||
<ul class="mt-10 bg-white">
|
||||
<li>
|
||||
1. A secret variable containing the token <span class="font-bold"
|
||||
>{truncateRev(value, 5, '*****')}</span
|
||||
>
|
||||
will be stored at
|
||||
<span class="font-mono">{path}</span>. You can refer to this variable anywhere this token
|
||||
is required.
|
||||
</li>
|
||||
<li class="mt-4">
|
||||
2. A resource with a unique token field will be stored at <span class="font-mono"
|
||||
>{path}</span
|
||||
>
|
||||
and refer to the secret variable <span class="font-mono">{path}</span> as its token (using
|
||||
variable templating
|
||||
<span class="font-mono">`$var:${path}`</span>). You can refer to this resource anywhere
|
||||
this token is required. A script can use the resource type
|
||||
<span class="font-mono">{resource_type}</span> as a type parameter to restrict the kind of
|
||||
tokens it accepts to this api.
|
||||
</li>
|
||||
</ul>
|
||||
{#if apiTokenApps[resource_type] || !manual}
|
||||
{manual}
|
||||
{apiTokenApps[resource_type]}
|
||||
<ul class="mt-10 bg-white">
|
||||
<li>
|
||||
1. A secret variable containing the {apiTokenApps[resource_type]?.key ?? 'token'}<span
|
||||
class="font-bold">{truncateRev(value, 5, '*****')}</span
|
||||
>
|
||||
will be stored a
|
||||
<span class="font-mono">{path}</span>.
|
||||
</li>
|
||||
<li class="mt-4">
|
||||
2. The resource containing that token will be stored at the same path<span
|
||||
class="font-mono">{path}</span
|
||||
>. The Variable and Resource will be "linked together", they will be deleted and renamed
|
||||
together.
|
||||
</li></ul
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
<div slot="submission" class="flex items-center gap-4">
|
||||
{#if step > 1 && !no_back}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import autosize from 'svelte-autosize'
|
||||
import Toggle from './Toggle.svelte'
|
||||
import Password from './Password.svelte'
|
||||
|
||||
export let label: string = ''
|
||||
export let value: any
|
||||
@@ -39,6 +40,7 @@
|
||||
export let properties: { [name: string]: SchemaProperty } | undefined = undefined
|
||||
export let autofocus = false
|
||||
export let compact = false
|
||||
export let password = false
|
||||
|
||||
let seeEditable: boolean = enum_ != undefined || pattern != undefined
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -336,24 +338,28 @@
|
||||
: undefined}
|
||||
/>
|
||||
{:else if inputCat == 'string'}
|
||||
<textarea
|
||||
{autofocus}
|
||||
rows="1"
|
||||
bind:this={el}
|
||||
on:focus={() => dispatch('focus')}
|
||||
on:blur={() => dispatch('blur')}
|
||||
use:autosize
|
||||
type="text"
|
||||
{disabled}
|
||||
class="col-span-10 {valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
|
||||
placeholder={defaultValue ?? ''}
|
||||
bind:value
|
||||
on:input={() => {
|
||||
dispatch('input', { rawValue: value, isRaw: false })
|
||||
}}
|
||||
/>
|
||||
{#if password}
|
||||
<Password bind:password={value} />
|
||||
{:else}
|
||||
<textarea
|
||||
{autofocus}
|
||||
rows="1"
|
||||
bind:this={el}
|
||||
on:focus={() => dispatch('focus')}
|
||||
on:blur={() => dispatch('blur')}
|
||||
use:autosize
|
||||
type="text"
|
||||
{disabled}
|
||||
class="col-span-10 {valid
|
||||
? ''
|
||||
: 'border border-red-700 border-opacity-30 focus:border-red-700 focus:border-opacity-30 bg-red-100'}"
|
||||
placeholder={defaultValue ?? ''}
|
||||
bind:value
|
||||
on:input={() => {
|
||||
dispatch('input', { rawValue: value, isRaw: false })
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !required && inputCat != 'resource-object'}
|
||||
<!-- <Tooltip placement="bottom" content="Reset to default value">
|
||||
|
||||
@@ -211,15 +211,6 @@
|
||||
>
|
||||
Connect an API
|
||||
</Button>
|
||||
<Button
|
||||
color="blue"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
resourceEditor.initNew()
|
||||
}}
|
||||
>
|
||||
Add a resource
|
||||
</Button>
|
||||
</div>
|
||||
</ItemPicker>
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// @ts-nocheck
|
||||
import { onMount } from 'svelte'
|
||||
export let password: string
|
||||
export let label = 'password'
|
||||
export let placeholder = '******'
|
||||
|
||||
onMount(() => {
|
||||
@@ -28,7 +27,6 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<label class="block text-gray-700" for="password"> {@html label} </label>
|
||||
<div class="relative w-full">
|
||||
<div class="absolute inset-y-0 right-0 flex items-center px-2">
|
||||
<input class="hidden js-password-toggle" id="toggle" type="checkbox" />
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
export let initialPath: string
|
||||
export let path = ''
|
||||
export let error = ''
|
||||
export let disabled = false
|
||||
|
||||
export let kind: PathKind
|
||||
|
||||
@@ -176,6 +177,7 @@
|
||||
</span>
|
||||
|
||||
<select
|
||||
{disabled}
|
||||
bind:value={meta.ownerKind}
|
||||
on:change={() => {
|
||||
if (meta.ownerKind === 'group') {
|
||||
@@ -202,7 +204,7 @@
|
||||
{:else}
|
||||
<label class="block">
|
||||
<span class="text-gray-700 text-sm">Owner</span>
|
||||
<select bind:value={meta.owner}>
|
||||
<select {disabled} bind:value={meta.owner}>
|
||||
{#each groups as g}
|
||||
<option>{g.name}</option>
|
||||
{/each}
|
||||
@@ -215,6 +217,7 @@
|
||||
<Required required={true} />
|
||||
</span>
|
||||
<input
|
||||
{disabled}
|
||||
type="text"
|
||||
id="path"
|
||||
autofocus
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { type Resource, ResourceService, type ResourceType, VariableService } from '$lib/gen'
|
||||
import { allTrue, emptySchema, sendUserToast } from '$lib/utils'
|
||||
import { allTrue, canWrite, sendUserToast } from '$lib/utils'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
import Path from './Path.svelte'
|
||||
@@ -8,22 +8,18 @@
|
||||
import ItemPicker from './ItemPicker.svelte'
|
||||
import VariableEditor from './VariableEditor.svelte'
|
||||
import Required from './Required.svelte'
|
||||
import { Button, Drawer } from './common'
|
||||
import { Alert, Button, Drawer } from './common'
|
||||
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import ResourceTypePicker from './ResourceTypePicker.svelte'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
import autosize from 'svelte-autosize'
|
||||
import SimpleEditor from './SimpleEditor.svelte'
|
||||
import AppConnect from './AppConnect.svelte'
|
||||
import { faSave } from '@fortawesome/free-solid-svg-icons'
|
||||
|
||||
let path = ''
|
||||
let initialPath = ''
|
||||
let pathError = ''
|
||||
|
||||
let step = 1
|
||||
|
||||
let resourceToEdit: Resource | undefined
|
||||
|
||||
let description: string = ''
|
||||
@@ -32,6 +28,7 @@
|
||||
let resourceType: ResourceType
|
||||
let resourceSchema: Schema | undefined
|
||||
let args: Record<string, any> = {}
|
||||
let can_write = true
|
||||
|
||||
let error: string | undefined
|
||||
|
||||
@@ -44,39 +41,18 @@
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
|
||||
export async function initNew(rt?: string) {
|
||||
step = 1
|
||||
args = {}
|
||||
path = ''
|
||||
description = ''
|
||||
initialPath = ''
|
||||
resourceSchema = emptySchema()
|
||||
resourceToEdit = undefined
|
||||
selectedResourceType = rt
|
||||
drawer.openDrawer()
|
||||
}
|
||||
|
||||
export async function initEdit(p: string): Promise<void> {
|
||||
initialPath = p
|
||||
path = p
|
||||
step = 2
|
||||
resourceToEdit = await ResourceService.getResource({ workspace: $workspaceStore!, path: p })
|
||||
description = resourceToEdit!.description ?? ''
|
||||
selectedResourceType = resourceToEdit!.resource_type
|
||||
args = resourceToEdit!.value
|
||||
can_write =
|
||||
resourceToEdit.workspace_id == $workspaceStore &&
|
||||
canWrite(p, resourceToEdit.extra_perms ?? {}, $userStore)
|
||||
await loadResourceType()
|
||||
drawer.openDrawer()
|
||||
}
|
||||
|
||||
async function createResource(): Promise<void> {
|
||||
await ResourceService.createResource({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: { path, value: args, description, resource_type: resourceType.name }
|
||||
})
|
||||
sendUserToast(`Successfully created resource at ${path}`)
|
||||
|
||||
dispatch('refresh')
|
||||
drawer.closeDrawer()
|
||||
drawer.openDrawer?.()
|
||||
}
|
||||
|
||||
async function editResource(): Promise<void> {
|
||||
@@ -97,7 +73,7 @@
|
||||
})
|
||||
sendUserToast(`Successfully updated resource at ${path}`)
|
||||
dispatch('refresh')
|
||||
drawer.closeDrawer()
|
||||
drawer.closeDrawer?.()
|
||||
} else {
|
||||
throw Error('Cannot edit undefined resourceToEdit')
|
||||
}
|
||||
@@ -126,10 +102,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
function resourceAction() {
|
||||
return resourceToEdit ? editResource() : createResource()
|
||||
}
|
||||
|
||||
let inputCheck: { [id: string]: boolean } = {}
|
||||
|
||||
$: isValid = allTrue(inputCheck) ?? false
|
||||
@@ -141,48 +113,43 @@
|
||||
on:close={drawer.closeDrawer}
|
||||
>
|
||||
<div>
|
||||
<!-- content -->
|
||||
{#if step === 1}
|
||||
<div class="flex flex-col gap-3 py-3 text-gray-700">
|
||||
<div>
|
||||
<span class="text-red-600 text-2xs grow">{error ?? ''}</span>
|
||||
<span class="mb-1 font-semibold text-gray-700">Path</span>
|
||||
<Path
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
namePlaceholder="my_resource"
|
||||
kind="resource"
|
||||
/>
|
||||
</div>
|
||||
<span class=" mt-3 font-semibold text-gray-700 "
|
||||
>Description <Required required={false} />
|
||||
</span>
|
||||
<textarea
|
||||
type="text"
|
||||
use:autosize
|
||||
bind:value={description}
|
||||
placeholder={DESCRIPTION_PLACEHOLDER}
|
||||
/>
|
||||
<div>
|
||||
<div class="mb-2 font-semibold text-gray-700">
|
||||
Resource type<Required required={true} />
|
||||
<div class="flex flex-col gap-3 py-3 text-gray-700">
|
||||
<div>
|
||||
{#if !can_write}
|
||||
<div class="m-2">
|
||||
<Alert type="warning" title="Only read access"
|
||||
>You only have read access to this resource and cannot edit it</Alert
|
||||
>
|
||||
</div>
|
||||
<ResourceTypePicker
|
||||
bind:value={selectedResourceType}
|
||||
notPickable={resourceToEdit != undefined}
|
||||
on:click={() => {
|
||||
args = {}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="text-red-600 text-2xs grow">{error ?? ''}</span>
|
||||
<Path
|
||||
disabled={!can_write}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
namePlaceholder="my_resource"
|
||||
kind="resource"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<h3>Description <Required required={false} /> </h3>
|
||||
<textarea
|
||||
type="text"
|
||||
disabled={!can_write}
|
||||
use:autosize
|
||||
bind:value={description}
|
||||
placeholder={DESCRIPTION_PLACEHOLDER}
|
||||
/>
|
||||
|
||||
<h3 class="mt-4">Value</h3>
|
||||
<div class="text-sm">
|
||||
{#if resourceSchema && resourceSchema?.properties}
|
||||
{#each Object.keys(resourceSchema.properties) as fieldName}
|
||||
<div class="flex flex-row w-full items-end justify-between">
|
||||
<div class="flex flex-row w-full items-center justify-between">
|
||||
<ArgInput
|
||||
compact
|
||||
disabled={!can_write}
|
||||
label={fieldName}
|
||||
description={resourceSchema.properties[fieldName]?.description}
|
||||
bind:value={args[fieldName]}
|
||||
@@ -197,7 +164,7 @@
|
||||
properties={resourceSchema.properties[fieldName]?.properties}
|
||||
format={resourceSchema.properties[fieldName]?.format}
|
||||
/>
|
||||
<div class="pb-6 ml-2 relative">
|
||||
<div class="ml-2 relative">
|
||||
<Button
|
||||
variant="border"
|
||||
color="blue"
|
||||
@@ -205,7 +172,7 @@
|
||||
btnClasses="min-w-min items-center leading-4 py-0"
|
||||
on:click={() => {
|
||||
pickForField = fieldName
|
||||
itemPicker.openDrawer()
|
||||
itemPicker.openDrawer?.()
|
||||
}}>Insert variable</Button
|
||||
>
|
||||
</div>
|
||||
@@ -217,34 +184,12 @@
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<span slot="submission" class="flex gap-4 mr-2">
|
||||
{#if step === 1}
|
||||
<Button
|
||||
target="_blank"
|
||||
color="blue"
|
||||
variant="border"
|
||||
size="sm"
|
||||
href="/resources?connect_app=undefined"
|
||||
>
|
||||
Connect an API
|
||||
</Button>
|
||||
<Button
|
||||
on:click={async () => {
|
||||
await loadResourceType()
|
||||
step = 2
|
||||
}}
|
||||
disabled={selectedResourceType == undefined || pathError != ''}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
{:else}
|
||||
<Button variant="border" on:click={() => (step = 1)}>Back</Button>
|
||||
<Button startIcon={{ icon: faSave }} on:click={resourceAction} disabled={!isValid}
|
||||
>Save</Button
|
||||
>
|
||||
{/if}
|
||||
<Button startIcon={{ icon: faSave }} on:click={editResource} disabled={!can_write || !isValid}
|
||||
>Save</Button
|
||||
>
|
||||
</span>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -273,7 +218,7 @@
|
||||
color="blue"
|
||||
size="sm"
|
||||
on:click={() => {
|
||||
variableEditor.initNew()
|
||||
variableEditor?.initNew?.()
|
||||
}}
|
||||
>
|
||||
Create a new variable
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import Icon from 'svelte-awesome'
|
||||
import { Button } from './common'
|
||||
import ResourceEditor from './ResourceEditor.svelte'
|
||||
import Select from 'svelte-select'
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -15,8 +14,6 @@
|
||||
export let value: string | undefined = initialValue
|
||||
export let resourceType: string | undefined = undefined
|
||||
|
||||
let resourceEditor: ResourceEditor
|
||||
|
||||
async function loadResources(resourceType: string | undefined) {
|
||||
const v = value
|
||||
resources = await ResourceService.listResource({ workspace: $workspaceStore!, resourceType })
|
||||
@@ -35,7 +32,6 @@
|
||||
}))
|
||||
</script>
|
||||
|
||||
<ResourceEditor bind:this={resourceEditor} on:refresh={() => loadResources(resourceType)} />
|
||||
<div class="flex flex-row gap-x-1 w-full">
|
||||
<Select
|
||||
bind:justValue={value}
|
||||
@@ -43,12 +39,8 @@
|
||||
class="grow"
|
||||
placeholder="Pick a {resourceType} resource"
|
||||
/>
|
||||
<Button
|
||||
variant="border"
|
||||
size="xs"
|
||||
on:click={() => {
|
||||
resourceEditor.initNew(resourceType)
|
||||
}}><Icon scale={0.8} data={faPlus} /></Button
|
||||
<Button variant="border" target="_blank" size="xs" href="/resources?connect_app={resourceType}"
|
||||
><Icon scale={0.8} data={faPlus} /></Button
|
||||
>
|
||||
<Button
|
||||
variant="border"
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
export let shouldHideNoInputs: boolean = false
|
||||
export let compact = false
|
||||
export let password: string | undefined = undefined
|
||||
|
||||
let clazz: string = ''
|
||||
export { clazz as class }
|
||||
@@ -74,6 +75,7 @@
|
||||
disabled={disabledArgs.includes(argName)}
|
||||
{editableSchema}
|
||||
{compact}
|
||||
password={argName == password}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import { sendUserToast } from '$lib/utils'
|
||||
import { canWrite, sendUserToast } from '$lib/utils'
|
||||
import { VariableService } from '$lib/gen'
|
||||
import Path from './Path.svelte'
|
||||
import { createEventDispatcher } from 'svelte'
|
||||
import { workspaceStore } from '$lib/stores'
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import Required from './Required.svelte'
|
||||
import Tooltip from './Tooltip.svelte'
|
||||
import { Button, ToggleButton, ToggleButtonGroup } from './common'
|
||||
@@ -34,6 +34,7 @@
|
||||
let edit = false
|
||||
let initialPath: string
|
||||
let pathError = ''
|
||||
let can_write = true
|
||||
|
||||
export function initNew(): void {
|
||||
variable = {
|
||||
@@ -44,6 +45,7 @@
|
||||
edit = false
|
||||
initialPath = ''
|
||||
path = ''
|
||||
can_write = true
|
||||
drawer.openDrawer()
|
||||
}
|
||||
|
||||
@@ -54,6 +56,9 @@
|
||||
path,
|
||||
decryptSecret: false
|
||||
})
|
||||
can_write =
|
||||
getV.workspace_id == $workspaceStore && canWrite(path, getV.extra_perms ?? {}, $userStore)
|
||||
|
||||
variable = {
|
||||
value: getV.value ?? '',
|
||||
is_secret: getV.is_secret,
|
||||
@@ -129,8 +134,16 @@
|
||||
<div class="flex flex-col gap-6">
|
||||
<div>
|
||||
<div>
|
||||
{#if !can_write}
|
||||
<div class="m-2">
|
||||
<Alert type="warning" title="Only read access"
|
||||
>You only have read access to this resource and cannot edit it</Alert
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mb-1 font-semibold text-gray-700">General</div>
|
||||
<Path
|
||||
disabled={!can_write}
|
||||
bind:error={pathError}
|
||||
bind:path
|
||||
{initialPath}
|
||||
@@ -164,6 +177,7 @@
|
||||
<div class="flex flex-row">
|
||||
{#if editorKind == 'plain'}
|
||||
<textarea
|
||||
disabled={!can_write}
|
||||
rows="4"
|
||||
type="text"
|
||||
use:autosize
|
||||
@@ -206,7 +220,7 @@
|
||||
<div slot="submission">
|
||||
<Button
|
||||
on:click={() => (edit ? updateVariable() : createVariable())}
|
||||
disabled={!valid || pathError != ''}
|
||||
disabled={!can_write || !valid || pathError != ''}
|
||||
btnClasses="mr-2"
|
||||
startIcon={{ icon: faSave }}
|
||||
>
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
let filter = ''
|
||||
let appFilter: string | undefined = undefined
|
||||
|
||||
$: prefilteredItems = appFilter ? (items ?? []).filter((i) => i.app == appFilter) : items ?? []
|
||||
|
||||
$: apps = Array.from(new Set(filteredItems?.map((x) => x.app) ?? [])).sort()
|
||||
|
||||
const dispatch = createEventDispatcher()
|
||||
@@ -26,7 +28,7 @@
|
||||
})
|
||||
</script>
|
||||
|
||||
<SearchItems {filter} {items} bind:filteredItems f={(x) => x.summary} />
|
||||
<SearchItems {filter} items={prefilteredItems} bind:filteredItems f={(x) => x.summary} />
|
||||
|
||||
<div class="flex flex-col min-h-0">
|
||||
<div class="w-12/12 pb-2 flex flex-row my-1 gap-1">
|
||||
|
||||
@@ -12,15 +12,13 @@
|
||||
faCog,
|
||||
faStar
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { faAppStore, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import MenuLink from './MenuLink.svelte'
|
||||
import { starStore, userStore, workspaceStore } from '$lib/stores'
|
||||
import { FlowService, ScriptService } from '$lib/gen'
|
||||
import { userStore } from '$lib/stores'
|
||||
|
||||
const mainMenuLinks = [
|
||||
{ label: 'Home', href: '/', icon: faHomeAlt },
|
||||
{ label: 'Runs', href: '/runs', icon: faPlay },
|
||||
{ label: 'Schedules', href: '/schedules', icon: faCalendar },
|
||||
{ label: 'Variables', href: '/variables', icon: faWallet },
|
||||
{ label: 'Resources', href: '/resources', icon: faCubes }
|
||||
]
|
||||
@@ -28,6 +26,7 @@
|
||||
export let favoriteLinks = [] as { label: string; href: string }[]
|
||||
|
||||
$: secondaryMenuLinks = [
|
||||
{ label: 'Schedules', href: '/schedules', icon: faCalendar },
|
||||
{ label: 'Groups', href: '/groups', icon: faUsersCog },
|
||||
{ label: 'Audit Logs', href: '/audit_logs', icon: faEye },
|
||||
{
|
||||
|
||||
@@ -8,20 +8,17 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { canWrite, emptySchema, sendUserToast, truncate } from '$lib/utils'
|
||||
import { OauthService, ResourceService, VariableService, type ListableResource } from '$lib/gen'
|
||||
import type { Resource, ResourceType } from '$lib/gen'
|
||||
import { OauthService, ResourceService, type ListableResource } from '$lib/gen'
|
||||
import type { ResourceType } from '$lib/gen'
|
||||
import PageHeader from '$lib/components/PageHeader.svelte'
|
||||
import ResourceEditor from '$lib/components/ResourceEditor.svelte'
|
||||
import TableCustom from '$lib/components/TableCustom.svelte'
|
||||
import Highlight from 'svelte-highlight'
|
||||
import json from 'svelte-highlight/languages/json'
|
||||
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
|
||||
import ShareModal from '$lib/components/ShareModal.svelte'
|
||||
import SharedBadge from '$lib/components/SharedBadge.svelte'
|
||||
import SvelteMarkdown from 'svelte-markdown'
|
||||
import { userStore, workspaceStore, oauthStore, superadmin } from '$lib/stores'
|
||||
import { userStore, workspaceStore, oauthStore } from '$lib/stores'
|
||||
import SchemaEditor from '$lib/components/SchemaEditor.svelte'
|
||||
import type { Schema } from '$lib/common'
|
||||
import SchemaViewer from '$lib/components/SchemaViewer.svelte'
|
||||
import Dropdown from '$lib/components/Dropdown.svelte'
|
||||
import {
|
||||
@@ -53,14 +50,20 @@
|
||||
|
||||
let resources: ResourceW[] | undefined
|
||||
let resourceTypes: ResourceTypeW[] | undefined
|
||||
let resourceViewer: Drawer
|
||||
let resourceViewerTitle: string = ''
|
||||
let resourceViewerSchema: Schema = emptySchema()
|
||||
let resourceViewerDescription = ''
|
||||
let typeDrawerMode: 'view' | 'view-type' | 'create' = 'view'
|
||||
let newResourceTypeName: string
|
||||
let newResourceTypeSchema: Schema
|
||||
let newResourceTypeDescription: string
|
||||
|
||||
let resourceTypeViewer: Drawer
|
||||
let resourceTypeViewerObj = {
|
||||
rt: '',
|
||||
description: '',
|
||||
schema: emptySchema()
|
||||
}
|
||||
|
||||
let resourceTypeDrawer: Drawer
|
||||
let newResourceType = {
|
||||
name: '',
|
||||
schema: emptySchema(),
|
||||
description: ''
|
||||
}
|
||||
let resourceEditor: ResourceEditor | undefined
|
||||
let shareModal: ShareModal
|
||||
let appConnect: AppConnect
|
||||
@@ -107,12 +110,12 @@
|
||||
await ResourceService.createResourceType({
|
||||
workspace: $workspaceStore!,
|
||||
requestBody: {
|
||||
name: (disableCustomPrefix ? '' : 'c_') + newResourceTypeName,
|
||||
schema: newResourceTypeSchema,
|
||||
description: newResourceTypeDescription
|
||||
name: (disableCustomPrefix ? '' : 'c_') + newResourceType.name,
|
||||
schema: newResourceType.schema,
|
||||
description: newResourceType.description
|
||||
}
|
||||
})
|
||||
resourceViewer.closeDrawer()
|
||||
resourceTypeViewer.closeDrawer?.()
|
||||
loadResourceTypes()
|
||||
}
|
||||
|
||||
@@ -133,12 +136,12 @@
|
||||
}
|
||||
|
||||
const startNewType = () => {
|
||||
resourceViewerTitle = `Create a Resource Type`
|
||||
newResourceTypeName = 'my_resource_type'
|
||||
newResourceTypeSchema = emptySchema()
|
||||
newResourceTypeDescription = 'my description'
|
||||
typeDrawerMode = 'create'
|
||||
resourceViewer.openDrawer()
|
||||
newResourceType = {
|
||||
name: 'my_resource_type',
|
||||
schema: emptySchema(),
|
||||
description: ''
|
||||
}
|
||||
resourceTypeDrawer.openDrawer?.()
|
||||
}
|
||||
|
||||
$: {
|
||||
@@ -151,14 +154,14 @@
|
||||
onMount(() => {
|
||||
let resource_type = $page.url.searchParams.get('resource_type')
|
||||
if ($oauthStore && resource_type) {
|
||||
appConnect.openFromOauth(resource_type)
|
||||
appConnect.openFromOauth?.(resource_type)
|
||||
}
|
||||
if ($page.url.searchParams.get('connect_app')) {
|
||||
const rt = $page.url.searchParams.get('connect_app') ?? undefined
|
||||
if (rt == 'undefined') {
|
||||
appConnect.open()
|
||||
appConnect.open?.()
|
||||
} else {
|
||||
appConnect.open(rt)
|
||||
appConnect.open?.(rt)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -166,64 +169,60 @@
|
||||
let disableCustomPrefix = false
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={resourceViewer} size="800px">
|
||||
<DrawerContent title={resourceViewerTitle} on:close={resourceViewer.closeDrawer}>
|
||||
<Drawer bind:this={resourceTypeViewer} size="800px">
|
||||
<DrawerContent title={resourceTypeViewerObj.rt} on:close={resourceTypeViewer.closeDrawer}>
|
||||
<div>
|
||||
{#if typeDrawerMode === 'create'}
|
||||
<div class="flex flex-col gap-6">
|
||||
<label for="inp">
|
||||
<div class="mb-1 font-semibold text-gray-700">Name<Required required={true} /></div>
|
||||
<div class="flex flex-row items-center gap-x-4">
|
||||
{#if !disableCustomPrefix}
|
||||
<span
|
||||
class="border border-gray-700 rounded p-1 -mr-6 text-sm bg-gray-200 inline-block w-8"
|
||||
>c_</span
|
||||
>
|
||||
{/if}
|
||||
<h1 class="mb-8 mt-4"><IconedResourceType name={resourceTypeViewerObj.rt} /></h1>
|
||||
<div class="py-2 box prose mb-8">
|
||||
<SvelteMarkdown source={resourceTypeViewerObj.description} />
|
||||
</div>
|
||||
<SchemaViewer schema={resourceTypeViewerObj.schema} />
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
|
||||
<div class="inline-block">
|
||||
<input id="inp" type="text" bind:value={newResourceTypeName} />
|
||||
</div>
|
||||
<Drawer bind:this={resourceTypeDrawer} size="800px">
|
||||
<DrawerContent title="Create resource type" on:close={resourceTypeDrawer.closeDrawer}>
|
||||
<div>
|
||||
<div class="flex flex-col gap-6">
|
||||
<label for="inp">
|
||||
<div class="mb-1 font-semibold text-gray-700">Name<Required required={true} /></div>
|
||||
<div class="flex flex-row items-center gap-x-4">
|
||||
{#if !disableCustomPrefix}
|
||||
<span
|
||||
class="border border-gray-700 rounded p-1 -mr-6 text-sm bg-gray-200 inline-block w-8"
|
||||
>c_</span
|
||||
>
|
||||
{/if}
|
||||
|
||||
{#if $userStore?.is_admin}
|
||||
<Toggle
|
||||
bind:checked={disableCustomPrefix}
|
||||
options={{ right: 'disable c_ prefix (admin only)' }}
|
||||
/>
|
||||
<Tooltip
|
||||
>Resource types are synchronized with the official types on the hub regularly. The
|
||||
`c_` prefix is to avoid name clashes with them.</Tooltip
|
||||
>
|
||||
{/if}
|
||||
<div class="inline-block">
|
||||
<input id="inp" type="text" bind:value={newResourceType.name} />
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<div class="mb-1 font-semibold text-gray-700">Description</div>
|
||||
<input type="text" bind:value={newResourceTypeDescription} /></label
|
||||
>
|
||||
<div>
|
||||
<div class="mb-1 font-semibold text-gray-700">Schema</div>
|
||||
<SchemaEditor bind:schema={newResourceTypeSchema} />
|
||||
|
||||
{#if $userStore?.is_admin}
|
||||
<Toggle
|
||||
bind:checked={disableCustomPrefix}
|
||||
options={{ right: 'disable c_ prefix (admin only)' }}
|
||||
/>
|
||||
<Tooltip
|
||||
>Resource types are synchronized with the official types on the hub regularly. The
|
||||
`c_` prefix is to avoid name clashes with them.</Tooltip
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<div class="mb-1 font-semibold text-gray-700">Description</div>
|
||||
<input type="text" bind:value={newResourceType.description} /></label
|
||||
>
|
||||
<div>
|
||||
<div class="mb-1 font-semibold text-gray-700">Schema</div>
|
||||
<SchemaEditor bind:schema={newResourceType.schema} />
|
||||
</div>
|
||||
{:else if typeDrawerMode === 'view'}
|
||||
<div class="py-2 ">
|
||||
<SvelteMarkdown source={resourceViewerDescription} />
|
||||
</div>
|
||||
<div class="border p-2">
|
||||
<Highlight language={json} code={JSON.stringify(resourceViewerSchema, null, 4)} />
|
||||
</div>
|
||||
{:else if typeDrawerMode === 'view-type'}
|
||||
<div class="py-2">
|
||||
<SvelteMarkdown source={resourceViewerDescription} />
|
||||
</div>
|
||||
<SchemaViewer schema={resourceViewerSchema} />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div slot="submission">
|
||||
{#if typeDrawerMode === 'create'}
|
||||
<Button startIcon={{ icon: faSave }} on:click={addResourceType}>Save</Button>
|
||||
{/if}
|
||||
<Button startIcon={{ icon: faSave }} on:click={addResourceType}>Save</Button>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
@@ -234,11 +233,8 @@
|
||||
tooltip="Save and permission rich objects (JSON) including credentials obtained through OAuth."
|
||||
>
|
||||
<div class="flex flex-row space-x-4">
|
||||
<Button size="md" startIcon={{ icon: faChain }} on:click={() => appConnect.open()}>
|
||||
Connect an API
|
||||
</Button>
|
||||
<Button size="md" startIcon={{ icon: faPlus }} on:click={() => resourceEditor?.initNew()}>
|
||||
Add a resource
|
||||
<Button size="md" startIcon={{ icon: faChain }} on:click={() => appConnect.open?.()}>
|
||||
Add a resource/API
|
||||
</Button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
@@ -265,17 +261,7 @@
|
||||
><a
|
||||
class="break-words"
|
||||
href="#{path}"
|
||||
on:click={async () => {
|
||||
resourceViewerTitle = `Resource ${path}`
|
||||
const resource = await ResourceService.getResource({
|
||||
workspace: $workspaceStore ?? 'no_workspace',
|
||||
path
|
||||
})
|
||||
resourceViewerSchema = resource.value
|
||||
resourceViewerDescription = resource.description ?? ''
|
||||
typeDrawerMode = 'view'
|
||||
resourceViewer.openDrawer()
|
||||
}}>{path}</a
|
||||
on:click={() => resourceEditor?.initEdit?.(path)}>{path}</a
|
||||
>
|
||||
<div class="mb-1 -mt-1"><SharedBadge {canWrite} extraPerms={extra_perms} /></div>
|
||||
</td>
|
||||
@@ -303,8 +289,8 @@
|
||||
<Popover>
|
||||
<Icon data={faRefresh} />
|
||||
<div slot="text">
|
||||
The OAuth token will be kept up-to-date in the background by Windmill using
|
||||
its refresh token
|
||||
The OAuth token will be kept up-to-date in the background by Windmill
|
||||
using its refresh token
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
@@ -363,7 +349,7 @@
|
||||
icon: faShare,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
shareModal.openDrawer(path)
|
||||
shareModal.openDrawer?.(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -371,7 +357,7 @@
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
resourceEditor?.initEdit(path)
|
||||
resourceEditor?.initEdit?.(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -428,7 +414,9 @@
|
||||
primary={false}
|
||||
tooltip="Schema and label to filter resources by type"
|
||||
>
|
||||
<Button size="md" startIcon={{ icon: faPlus }} on:click={startNewType}>Add a type</Button>
|
||||
<Button size="md" startIcon={{ icon: faPlus }} on:click={startNewType}
|
||||
>Add a resource type</Button
|
||||
>
|
||||
</PageHeader>
|
||||
|
||||
{#if loading.types}
|
||||
@@ -452,11 +440,13 @@
|
||||
><a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
resourceViewerTitle = `Resource type ${name}`
|
||||
resourceViewerSchema = schema
|
||||
resourceViewerDescription = description ?? ''
|
||||
typeDrawerMode = 'view-type'
|
||||
resourceViewer.openDrawer()
|
||||
resourceTypeViewerObj = {
|
||||
rt: name,
|
||||
schema: schema,
|
||||
description: description ?? ''
|
||||
}
|
||||
|
||||
resourceTypeViewer.openDrawer?.()
|
||||
}}><IconedResourceType after={true} {name} /></a
|
||||
></td
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user