feat: unify resources under a single connect API

This commit is contained in:
Ruben Fiszel
2022-12-01 19:28:00 +01:00
parent 7d7e862672
commit fed2b2105d
15 changed files with 287 additions and 313 deletions
+1 -1
View File
@@ -1331,7 +1331,7 @@ paths:
content:
application/json:
schema:
$ref: "#/components/schemas/ListableResource"
$ref: "#/components/schemas/Resource"
/w/{workspace}/resources/get_value/{path}:
get:
+5 -5
View File
@@ -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()
@@ -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 />
+62 -54
View File
@@ -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}
+24 -18
View File
@@ -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" />
+4 -1
View File
@@ -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
+47 -102
View File
@@ -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 },
{
+93 -103
View File
@@ -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
>