mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 08:01:38 +00:00
fix(frontend): variables and resources uses tab navigation
This commit is contained in:
@@ -378,9 +378,8 @@
|
||||
|
||||
<input type="text" bind:value={description} />
|
||||
|
||||
<h2 class="mt-4">Value</h2>
|
||||
{#if apiTokenApps[resource_type]}
|
||||
<div class="mb-1 font-semibold text-gray-700 mt-6">Instructions</div>
|
||||
<h2 class="mt-4 mb-2">Instructions</h2>
|
||||
<div class="pl-10">
|
||||
<ol class="list-decimal">
|
||||
{#each apiTokenApps[resource_type].instructions as step}
|
||||
@@ -397,6 +396,7 @@
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<h2 class="mt-4">Value</h2>
|
||||
<div class="mt-4">
|
||||
<ApiConnectForm password={key ?? ''} {resource_type} bind:args />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<script lang="ts">
|
||||
import { createPopperActions } from 'svelte-popperjs'
|
||||
|
||||
export let notClickable = false
|
||||
|
||||
const [popperRef, popperContent] = createPopperActions({
|
||||
placement: 'auto'
|
||||
})
|
||||
@@ -37,7 +40,12 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<button use:popperRef on:mouseenter={open} on:mouseleave={close}>
|
||||
<button
|
||||
class:cursor-default={notClickable}
|
||||
use:popperRef
|
||||
on:mouseenter={open}
|
||||
on:mouseleave={close}
|
||||
>
|
||||
<slot />
|
||||
</button>
|
||||
{#if showTooltip}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import type { Schema } from '$lib/common'
|
||||
import Path from './Path.svelte'
|
||||
import Required from './Required.svelte'
|
||||
import { Alert, Button, Drawer } from './common'
|
||||
import { Alert, Button, Drawer, Skeleton } from './common'
|
||||
|
||||
import { userStore, workspaceStore } from '$lib/stores'
|
||||
import DrawerContent from './common/drawer/DrawerContent.svelte'
|
||||
@@ -23,10 +23,10 @@
|
||||
let description: string = ''
|
||||
let DESCRIPTION_PLACEHOLDER = `You can use markdown to style your description`
|
||||
let selectedResourceType: string | undefined
|
||||
let resourceType: ResourceType
|
||||
let resourceSchema: Schema | undefined
|
||||
let args: Record<string, any> = {}
|
||||
let can_write = true
|
||||
let loadingSchema = false
|
||||
|
||||
let error: string | undefined
|
||||
|
||||
@@ -39,15 +39,18 @@
|
||||
export async function initEdit(p: string): Promise<void> {
|
||||
initialPath = p
|
||||
path = p
|
||||
resourceToEdit = undefined
|
||||
resourceSchema = undefined
|
||||
loadingSchema = true
|
||||
drawer.openDrawer?.()
|
||||
resourceToEdit = await ResourceService.getResource({ workspace: $workspaceStore!, path: p })
|
||||
description = resourceToEdit!.description ?? ''
|
||||
selectedResourceType = resourceToEdit!.resource_type
|
||||
loadResourceType()
|
||||
args = resourceToEdit!.value
|
||||
can_write =
|
||||
resourceToEdit.workspace_id == $workspaceStore &&
|
||||
canWrite(p, resourceToEdit.extra_perms ?? {}, $userStore)
|
||||
await loadResourceType()
|
||||
drawer.openDrawer?.()
|
||||
}
|
||||
|
||||
async function editResource(): Promise<void> {
|
||||
@@ -80,7 +83,7 @@
|
||||
async function loadResourceType(): Promise<void> {
|
||||
if (selectedResourceType) {
|
||||
try {
|
||||
resourceType = await ResourceService.getResourceType({
|
||||
const resourceType = await ResourceService.getResourceType({
|
||||
workspace: $workspaceStore!,
|
||||
path: selectedResourceType
|
||||
})
|
||||
@@ -90,11 +93,13 @@
|
||||
}
|
||||
} catch (err) {
|
||||
resourceSchema = undefined
|
||||
loadingSchema = false
|
||||
rawCode = JSON.stringify(args, null, 2)
|
||||
}
|
||||
} else {
|
||||
sendUserToast(`ResourceType cannot be undefined.`, true)
|
||||
}
|
||||
loadingSchema = false
|
||||
}
|
||||
|
||||
let isValid = true
|
||||
@@ -137,7 +142,9 @@
|
||||
|
||||
<h3 class="mt-4">Value</h3>
|
||||
<div class="text-sm">
|
||||
{#if resourceSchema && resourceSchema?.properties}
|
||||
{#if loadingSchema}
|
||||
<Skeleton layout={[[4]]} />
|
||||
{:else if resourceSchema && resourceSchema?.properties}
|
||||
<SchemaForm
|
||||
disabled={!can_write}
|
||||
compact
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
const dispatch = createEventDispatcher()
|
||||
</script>
|
||||
|
||||
<div class="mt-8 flex flex-col {$$props.class}">
|
||||
<div class="mt-2 flex flex-col {$$props.class}">
|
||||
<div class="inline-block min-w-full align-middle">
|
||||
<table class="min-w-full divide-y divide-gray-300 table-auto">
|
||||
<thead>
|
||||
|
||||
@@ -37,13 +37,15 @@
|
||||
import AppConnect from '$lib/components/AppConnect.svelte'
|
||||
import { page } from '$app/stores'
|
||||
import { onMount } from 'svelte'
|
||||
import { Button, Alert, Badge, Skeleton } from '$lib/components/common'
|
||||
import { Button, Alert, Badge, Skeleton, Tab } from '$lib/components/common'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
import Drawer from '$lib/components/common/drawer/Drawer.svelte'
|
||||
import DrawerContent from '$lib/components/common/drawer/DrawerContent.svelte'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import Toggle from '$lib/components/Toggle.svelte'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import Tabs from '$lib/components/common/tabs/Tabs.svelte'
|
||||
import { Building } from 'svelte-lucide'
|
||||
|
||||
type ResourceW = ListableResource & { canWrite: boolean }
|
||||
type ResourceTypeW = ResourceType & { canWrite: boolean }
|
||||
@@ -168,6 +170,7 @@
|
||||
})
|
||||
|
||||
let disableCustomPrefix = false
|
||||
let tab: 'workspace' | 'types' = 'workspace'
|
||||
</script>
|
||||
|
||||
<Drawer bind:this={resourceTypeViewer} size="800px">
|
||||
@@ -234,286 +237,299 @@
|
||||
tooltip="Save and permission rich objects (JSON) including credentials obtained through OAuth."
|
||||
>
|
||||
<div class="flex flex-row space-x-4">
|
||||
<Button variant="border" size="md" startIcon={{ icon: faPlus }} on:click={startNewType}
|
||||
>Add a resource type</Button
|
||||
>
|
||||
<Button size="md" startIcon={{ icon: faChain }} on:click={() => appConnect.open?.()}>
|
||||
Add a resource/API
|
||||
</Button>
|
||||
</div>
|
||||
</PageHeader>
|
||||
<div class="overflow-x-auto pb-40">
|
||||
{#if loading.resources}
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab size="md" value="workspace">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Building size="18px" />
|
||||
Workspace
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="md" value="types">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
Resource Types <Tooltip
|
||||
>Every resources have Resource Types attached to them which contains its schema and make
|
||||
it easy in scripts and flows to accept only resources of a specific resource type</Tooltip
|
||||
>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{#if tab == 'workspace'}
|
||||
<div class="overflow-x-auto pb-40">
|
||||
{#if loading.resources}
|
||||
<Skeleton layout={[0.5, [2], 1]} />
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[4], 0.7]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>path</th>
|
||||
<th />
|
||||
<th>resource type</th>
|
||||
<th>description</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if resources}
|
||||
{#each resources as { path, description, resource_type, extra_perms, canWrite, is_oauth, is_linked, account, refresh_error, is_expired }}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
class="break-words"
|
||||
href="#{path}"
|
||||
on:click={() => resourceEditor?.initEdit?.(path)}>{path}</a
|
||||
>
|
||||
</td>
|
||||
<td><SharedBadge {canWrite} extraPerms={extra_perms} /></td>
|
||||
<td class="px-2"
|
||||
><a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
const linkedRt = resourceTypes?.find((rt) => rt.name === resource_type)
|
||||
if (linkedRt) {
|
||||
resourceTypeViewerObj = {
|
||||
rt: linkedRt.name,
|
||||
schema: linkedRt.schema,
|
||||
description: linkedRt.description ?? ''
|
||||
}
|
||||
resourceTypeViewer.openDrawer?.()
|
||||
} else {
|
||||
sendUserToast(
|
||||
`Resource type ${resource_type} not found in workspace.`,
|
||||
true
|
||||
)
|
||||
}
|
||||
}}><IconedResourceType name={resource_type} after={true} /></a
|
||||
></td
|
||||
>
|
||||
<td
|
||||
><span class="text-gray-500 text-xs"
|
||||
><SvelteMarkdown source={truncate(description ?? '', 30)} /></span
|
||||
></td
|
||||
>
|
||||
<td class="text-center">
|
||||
<div class="flex flex-row">
|
||||
<div class="w-10">
|
||||
{#if is_linked}
|
||||
<Popover>
|
||||
<Icon data={faChain} />
|
||||
<div slot="text">
|
||||
This resource is linked with a variable of the same path. They are
|
||||
deleted and renamed together.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-10">
|
||||
{#if account}
|
||||
<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
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if is_oauth}
|
||||
<div class="w-10">
|
||||
{#if refresh_error}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-red-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during exchange of the refresh token"
|
||||
/>
|
||||
<div slot="text">
|
||||
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if is_expired}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is expired"
|
||||
/>
|
||||
<div slot="text">
|
||||
The access_token is expired, it will get renewed the next time this
|
||||
variable is fetched or you can request is to be refreshed in the
|
||||
dropdown on the right.
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-green-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is tied to an OAuth app"
|
||||
/>
|
||||
<div slot="text">
|
||||
The resource was connected through OAuth and the token is not
|
||||
expired.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<Dropdown
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Share',
|
||||
icon: faShare,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
shareModal.openDrawer?.(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
resourceEditor?.initEdit?.(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
disabled: !canWrite,
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
action: (event) => {
|
||||
if (event?.shiftKey) {
|
||||
deleteResource(path, account)
|
||||
} else {
|
||||
deleteConfirmedCallback = () => {
|
||||
deleteResource(path, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...(account != undefined
|
||||
? [
|
||||
{
|
||||
displayName: 'Refresh token',
|
||||
icon: faRefresh,
|
||||
action: async () => {
|
||||
await OauthService.refreshToken({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: account ?? 0,
|
||||
requestBody: {
|
||||
path
|
||||
}
|
||||
})
|
||||
sendUserToast('Token refreshed')
|
||||
loadResources()
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
relative={true}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if resources}
|
||||
<tr> No resources to display</tr>
|
||||
{:else}
|
||||
<tr>Loading...</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if tab == 'types'}
|
||||
{#if loading.types}
|
||||
<Skeleton layout={[0.5, [2], 1]} />
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[4], 0.7]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>path</th>
|
||||
<th />
|
||||
<th>resource type</th>
|
||||
<th>description</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if resources}
|
||||
{#each resources as { path, description, resource_type, extra_perms, canWrite, is_oauth, is_linked, account, refresh_error, is_expired }}
|
||||
<tr>
|
||||
<td>
|
||||
<a
|
||||
class="break-words"
|
||||
href="#{path}"
|
||||
on:click={() => resourceEditor?.initEdit?.(path)}>{path}</a
|
||||
>
|
||||
</td>
|
||||
<td><SharedBadge {canWrite} extraPerms={extra_perms} /></td>
|
||||
<td class="px-2"
|
||||
><a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
const linkedRt = resourceTypes?.find((rt) => rt.name === resource_type)
|
||||
if (linkedRt) {
|
||||
<div class="overflow-auto">
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if resourceTypes}
|
||||
{#each resourceTypes as { name, description, schema, canWrite }}
|
||||
<tr>
|
||||
<td
|
||||
><a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
resourceTypeViewerObj = {
|
||||
rt: linkedRt.name,
|
||||
schema: linkedRt.schema,
|
||||
description: linkedRt.description ?? ''
|
||||
rt: name,
|
||||
schema: schema,
|
||||
description: description ?? ''
|
||||
}
|
||||
|
||||
resourceTypeViewer.openDrawer?.()
|
||||
} else {
|
||||
sendUserToast(
|
||||
`Resource type ${resource_type} not found in workspace.`,
|
||||
true
|
||||
)
|
||||
}
|
||||
}}><IconedResourceType name={resource_type} after={true} /></a
|
||||
></td
|
||||
>
|
||||
<td
|
||||
><span class="text-gray-500 text-xs"
|
||||
><SvelteMarkdown source={truncate(description ?? '', 30)} /></span
|
||||
></td
|
||||
>
|
||||
<td class="text-center">
|
||||
<div class="flex flex-row">
|
||||
<div class="w-10">
|
||||
{#if is_linked}
|
||||
<Popover>
|
||||
<Icon data={faChain} />
|
||||
<div slot="text">
|
||||
This resource is linked with a variable of the same path. They are
|
||||
deleted and renamed together.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-10">
|
||||
{#if account}
|
||||
<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
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if is_oauth}
|
||||
<div class="w-10">
|
||||
{#if refresh_error}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-red-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during exchange of the refresh token"
|
||||
/>
|
||||
<div slot="text">
|
||||
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if is_expired}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is expired"
|
||||
/>
|
||||
<div slot="text">
|
||||
The access_token is expired, it will get renewed the next time this
|
||||
variable is fetched or you can request is to be refreshed in the
|
||||
dropdown on the right.
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-green-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is tied to an OAuth app"
|
||||
/>
|
||||
<div slot="text">
|
||||
The resource was connected through OAuth and the token is not expired.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
}}><IconedResourceType after={true} {name} /></a
|
||||
></td
|
||||
>
|
||||
<td
|
||||
><span class="text-gray-500 text-xs"
|
||||
><SvelteMarkdown source={truncate(description ?? '', 200)} /></span
|
||||
></td
|
||||
>
|
||||
<td>
|
||||
{#if !canWrite}
|
||||
<Badge
|
||||
>Shared globally<Tooltip
|
||||
>This resource type is from the 'starter' workspace shared with all
|
||||
workspaces</Tooltip
|
||||
></Badge
|
||||
>
|
||||
{:else if $userStore?.is_admin}
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => handleDeleteResourceType(name)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{:else}
|
||||
<Badge
|
||||
>Non Editable <Tooltip
|
||||
>Since resource types are shared with the whole workspace, only admins can
|
||||
edit/delete them</Tooltip
|
||||
></Badge
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<Dropdown
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Share',
|
||||
icon: faShare,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
shareModal.openDrawer?.(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
disabled: !canWrite,
|
||||
action: () => {
|
||||
resourceEditor?.initEdit?.(path)
|
||||
}
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
disabled: !canWrite,
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
action: (event) => {
|
||||
if (event?.shiftKey) {
|
||||
deleteResource(path, account)
|
||||
} else {
|
||||
deleteConfirmedCallback = () => {
|
||||
deleteResource(path, account)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
...(account != undefined
|
||||
? [
|
||||
{
|
||||
displayName: 'Refresh token',
|
||||
icon: faRefresh,
|
||||
action: async () => {
|
||||
await OauthService.refreshToken({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: account ?? 0,
|
||||
requestBody: {
|
||||
path
|
||||
}
|
||||
})
|
||||
sendUserToast('Token refreshed')
|
||||
loadResources()
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
relative={true}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if resources}
|
||||
<tr> No resources to display</tr>
|
||||
{:else}
|
||||
<tr>Loading...</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if resources}
|
||||
<tr> No resources types to display</tr>
|
||||
{:else}
|
||||
<tr>Loading...</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<PageHeader
|
||||
title="Resources types"
|
||||
primary={false}
|
||||
tooltip="Schema and label to filter resources by type"
|
||||
>
|
||||
<Button size="md" startIcon={{ icon: faPlus }} on:click={startNewType}
|
||||
>Add a resource type</Button
|
||||
>
|
||||
</PageHeader>
|
||||
|
||||
{#if loading.types}
|
||||
<Skeleton layout={[0.5, [2], 1]} />
|
||||
{#each new Array(6) as _}
|
||||
<Skeleton layout={[[4], 0.7]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<div class="overflow-auto">
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>name</th>
|
||||
<th>description</th>
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#if resourceTypes}
|
||||
{#each resourceTypes as { name, description, schema, canWrite }}
|
||||
<tr>
|
||||
<td
|
||||
><a
|
||||
href="#{name}"
|
||||
on:click={() => {
|
||||
resourceTypeViewerObj = {
|
||||
rt: name,
|
||||
schema: schema,
|
||||
description: description ?? ''
|
||||
}
|
||||
|
||||
resourceTypeViewer.openDrawer?.()
|
||||
}}><IconedResourceType after={true} {name} /></a
|
||||
></td
|
||||
>
|
||||
<td
|
||||
><span class="text-gray-500 text-xs"
|
||||
><SvelteMarkdown source={truncate(description ?? '', 200)} /></span
|
||||
></td
|
||||
>
|
||||
<td>
|
||||
{#if !canWrite}
|
||||
<Badge
|
||||
>Shared globally<Tooltip
|
||||
>This resource type is from the 'starter' workspace shared with all
|
||||
workspaces</Tooltip
|
||||
></Badge
|
||||
>
|
||||
{:else if $userStore?.is_admin}
|
||||
<Button
|
||||
size="sm"
|
||||
color="red"
|
||||
variant="border"
|
||||
startIcon={{ icon: faTrash }}
|
||||
on:click={() => handleDeleteResourceType(name)}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
{:else}
|
||||
<Badge
|
||||
>Non Editable <Tooltip
|
||||
>Since resource types are shared with the whole workspace, only admins can
|
||||
edit/delete them</Tooltip
|
||||
></Badge
|
||||
>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if resources}
|
||||
<tr> No resources types to display</tr>
|
||||
{:else}
|
||||
<tr>Loading...</tr>
|
||||
{/if}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
|
||||
@@ -30,14 +30,19 @@
|
||||
faEdit,
|
||||
faShare
|
||||
} from '@fortawesome/free-solid-svg-icons'
|
||||
import { Button } from '$lib/components/common'
|
||||
import { Button, Tab, Tabs } from '$lib/components/common'
|
||||
import ConfirmationModal from '$lib/components/common/confirmationModal/ConfirmationModal.svelte'
|
||||
import { Alert, Badge, Skeleton } from '$lib/components/common'
|
||||
import Popover from '$lib/components/Popover.svelte'
|
||||
import { Building, DollarSign } from 'svelte-lucide'
|
||||
import Tooltip from '$lib/components/Tooltip.svelte'
|
||||
import SearchItems from '$lib/components/SearchItems.svelte'
|
||||
|
||||
type ListableVariableW = ListableVariable & { canWrite: boolean }
|
||||
|
||||
let filter = ''
|
||||
let variables: ListableVariableW[] = []
|
||||
let filteredItems: ListableVariableW[] = []
|
||||
let contextualVariables: ContextualVariable[] = []
|
||||
let shareModal: ShareModal
|
||||
let variableEditor: VariableEditor
|
||||
@@ -81,8 +86,16 @@
|
||||
loadContextualVariables()
|
||||
}
|
||||
}
|
||||
let tab: 'workspace' | 'contextual' = 'workspace'
|
||||
</script>
|
||||
|
||||
<SearchItems
|
||||
{filter}
|
||||
items={variables}
|
||||
bind:filteredItems
|
||||
f={(x) => x.path + ' ' + x.description}
|
||||
/>
|
||||
|
||||
<CenteredPage>
|
||||
<PageHeader
|
||||
title="Variables"
|
||||
@@ -94,188 +107,6 @@
|
||||
</PageHeader>
|
||||
|
||||
<VariableEditor bind:this={variableEditor} on:create={loadVariables} />
|
||||
<div class="relative overflow-x-auto pb-40 pr-4">
|
||||
{#if loading.variables}
|
||||
<Skeleton layout={[0.5, [2], 1]} />
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[3.5], 0.5]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>path</th>
|
||||
|
||||
<th>value</th>
|
||||
<th>description</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each variables as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth, is_expired, refresh_error, is_linked }}
|
||||
<tr>
|
||||
<td
|
||||
><a
|
||||
class="break-words"
|
||||
id="edit-{path}"
|
||||
on:click={() => variableEditor.editVariable(path)}
|
||||
href="#{path}">{path}</a
|
||||
>
|
||||
<div><SharedBadge {canWrite} extraPerms={extra_perms} /></div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="inline-flex flex-row">
|
||||
<span class="text-sm break-words">
|
||||
{truncate(value ?? '****', 20)}
|
||||
</span>
|
||||
{#if is_secret}
|
||||
<Popover>
|
||||
<Icon
|
||||
label="Secret"
|
||||
class="text-gray-700 mb-2 ml-2"
|
||||
data={faEyeSlash}
|
||||
scale={0.8}
|
||||
/>
|
||||
<span slot="text">This item is secret</span>
|
||||
</Popover>
|
||||
{/if}
|
||||
</span>
|
||||
</td>
|
||||
<td class="break-words"
|
||||
><span class="text-xs text-gray-500">{truncate(description ?? '', 50)}</span></td
|
||||
>
|
||||
|
||||
<td class="text-center">
|
||||
<div class="flex flex-row">
|
||||
<div class="w-10">
|
||||
{#if is_linked}
|
||||
<Popover>
|
||||
<Icon data={faChain} />
|
||||
<div slot="text">
|
||||
This variable is linked with a resource of the same path. They are deleted
|
||||
and renamed together.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-10">
|
||||
{#if account}
|
||||
<Popover>
|
||||
<Icon data={faRefresh} />
|
||||
<div slot="text">
|
||||
This OAuth token will be kept up-to-date in the background by Windmill
|
||||
using its refresh token
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if is_oauth}
|
||||
<div class="w-10">
|
||||
{#if refresh_error}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-red-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during exchange of the refresh token"
|
||||
/>
|
||||
<div slot="text">
|
||||
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if is_expired}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is expired"
|
||||
/>
|
||||
<div slot="text">
|
||||
The access_token is expired, it will get renewed the next time this
|
||||
variable is fetched or you can request is to be refreshed in the
|
||||
dropdown on the right.
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Popover>
|
||||
<Icon
|
||||
class="text-green-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is tied to an OAuth app"
|
||||
/>
|
||||
<div slot="text">
|
||||
The variable was connected through OAuth and the token is not expired.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
><Dropdown
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
action: () => variableEditor.editVariable(path),
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
action: (event) => {
|
||||
if (event?.shiftKey) {
|
||||
deleteVariable(path, account)
|
||||
} else {
|
||||
deleteConfirmedCallback = () => {
|
||||
deleteVariable(path, account)
|
||||
}
|
||||
}
|
||||
},
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Share',
|
||||
action: () => {
|
||||
shareModal.openDrawer(path)
|
||||
},
|
||||
icon: faShare,
|
||||
disabled: !canWrite
|
||||
},
|
||||
...(account != undefined
|
||||
? [
|
||||
{
|
||||
displayName: 'Refresh token',
|
||||
icon: faRefresh,
|
||||
action: async () => {
|
||||
await OauthService.refreshToken({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: account ?? 0,
|
||||
requestBody: {
|
||||
path
|
||||
}
|
||||
})
|
||||
sendUserToast('Token refreshed')
|
||||
loadVariables()
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
relative={true}
|
||||
/></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<ShareModal
|
||||
bind:this={shareModal}
|
||||
kind="variable"
|
||||
@@ -284,30 +115,225 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<PageHeader
|
||||
title="Contextual Variables"
|
||||
tooltip="
|
||||
Contextual variables are utility variables passed to your environment when running a script
|
||||
and depends on the execution context. Variables cannot use a reseved variable name."
|
||||
primary={false}
|
||||
/>
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab size="md" value="workspace">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Building size="18px" />
|
||||
Workspace
|
||||
</div>
|
||||
</Tab>
|
||||
<Tab size="md" value="contextual">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<DollarSign size="18px" />
|
||||
Contextual <Tooltip>
|
||||
Contextual variables are utility variables passed to your environment when running a
|
||||
script and depends on the execution context.</Tooltip
|
||||
>
|
||||
</div>
|
||||
</Tab>
|
||||
</Tabs>
|
||||
{#if tab == 'workspace'}
|
||||
<div class="pt-2">
|
||||
<input placeholder="Search Variable" bind:value={filter} class="input mt-1" />
|
||||
</div>
|
||||
<div class="relative overflow-x-auto pb-40 pr-4">
|
||||
{#if loading.variables}
|
||||
<Skeleton layout={[0.5, [2], 1]} />
|
||||
{#each new Array(3) as _}
|
||||
<Skeleton layout={[[3.5], 0.5]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<TableCustom>
|
||||
<tr slot="header-row">
|
||||
<th>path</th>
|
||||
|
||||
<div class="overflow-auto">
|
||||
<div class="my-5" />
|
||||
{#if loading.contextual}
|
||||
<Skeleton layout={[0.5, [2], 1]} />
|
||||
{#each new Array(8) as _}
|
||||
<Skeleton layout={[[2.8], 0.5]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<TableSimple
|
||||
headers={['name', 'example of value', 'description']}
|
||||
data={contextualVariables}
|
||||
keys={['name', 'value', 'description']}
|
||||
twTextSize="text-sm"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
<th>value</th>
|
||||
<th>description</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
<tbody slot="body">
|
||||
{#each filteredItems as { path, value, is_secret, description, extra_perms, canWrite, account, is_oauth, is_expired, refresh_error, is_linked }}
|
||||
<tr>
|
||||
<td
|
||||
><a
|
||||
class="break-words"
|
||||
id="edit-{path}"
|
||||
on:click={() => variableEditor.editVariable(path)}
|
||||
href="#{path}">{path}</a
|
||||
>
|
||||
<div><SharedBadge {canWrite} extraPerms={extra_perms} /></div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="inline-flex flex-row">
|
||||
<span class="text-sm break-words">
|
||||
{truncate(value ?? '****', 20)}
|
||||
</span>
|
||||
{#if is_secret}
|
||||
<Popover notClickable>
|
||||
<Icon
|
||||
label="Secret"
|
||||
class="text-gray-700 mb-2 ml-2"
|
||||
data={faEyeSlash}
|
||||
scale={0.8}
|
||||
/>
|
||||
<span slot="text">This item is secret</span>
|
||||
</Popover>
|
||||
{/if}
|
||||
</span>
|
||||
</td>
|
||||
<td class="break-words"
|
||||
><span class="text-xs text-gray-500">{truncate(description ?? '', 50)}</span></td
|
||||
>
|
||||
|
||||
<td class="text-center">
|
||||
<div class="flex flex-row">
|
||||
<div class="w-10">
|
||||
{#if is_linked}
|
||||
<Popover notClickable>
|
||||
<Icon data={faChain} />
|
||||
<div slot="text">
|
||||
This variable is linked with a resource of the same path. They are
|
||||
deleted and renamed together.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="w-10">
|
||||
{#if account}
|
||||
<Popover notClickable>
|
||||
<Icon data={faRefresh} />
|
||||
<div slot="text">
|
||||
This OAuth token will be kept up-to-date in the background by Windmill
|
||||
using its refresh token
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if is_oauth}
|
||||
<div class="w-10">
|
||||
{#if refresh_error}
|
||||
<Popover notClickable>
|
||||
<Icon
|
||||
class="text-red-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Error during exchange of the refresh token"
|
||||
/>
|
||||
<div slot="text">
|
||||
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
|
||||
</div>
|
||||
</Popover>
|
||||
{:else if is_expired}
|
||||
<Popover notClickable>
|
||||
<Icon
|
||||
class="text-yellow-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is expired"
|
||||
/>
|
||||
<div slot="text">
|
||||
The access_token is expired, it will get renewed the next time this
|
||||
variable is fetched or you can request is to be refreshed in the
|
||||
dropdown on the right.
|
||||
</div>
|
||||
</Popover>
|
||||
{:else}
|
||||
<Popover notClickable>
|
||||
<Icon
|
||||
class="text-green-600 animate-[pulse_5s_linear_infinite]"
|
||||
data={faCircle}
|
||||
scale={0.7}
|
||||
label="Variable is tied to an OAuth app"
|
||||
/>
|
||||
<div slot="text">
|
||||
The variable was connected through OAuth and the token is not expired.
|
||||
</div>
|
||||
</Popover>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
><Dropdown
|
||||
dropdownItems={[
|
||||
{
|
||||
displayName: 'Edit',
|
||||
icon: faEdit,
|
||||
action: () => variableEditor.editVariable(path),
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Delete',
|
||||
icon: faTrash,
|
||||
type: 'delete',
|
||||
action: (event) => {
|
||||
if (event?.shiftKey) {
|
||||
deleteVariable(path, account)
|
||||
} else {
|
||||
deleteConfirmedCallback = () => {
|
||||
deleteVariable(path, account)
|
||||
}
|
||||
}
|
||||
},
|
||||
disabled: !canWrite
|
||||
},
|
||||
{
|
||||
displayName: 'Share',
|
||||
action: () => {
|
||||
shareModal.openDrawer(path)
|
||||
},
|
||||
icon: faShare,
|
||||
disabled: !canWrite
|
||||
},
|
||||
...(account != undefined
|
||||
? [
|
||||
{
|
||||
displayName: 'Refresh token',
|
||||
icon: faRefresh,
|
||||
action: async () => {
|
||||
await OauthService.refreshToken({
|
||||
workspace: $workspaceStore ?? '',
|
||||
id: account ?? 0,
|
||||
requestBody: {
|
||||
path
|
||||
}
|
||||
})
|
||||
sendUserToast('Token refreshed')
|
||||
loadVariables()
|
||||
}
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]}
|
||||
relative={true}
|
||||
/></td
|
||||
>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</TableCustom>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if tab == 'contextual'}
|
||||
<div class="overflow-auto">
|
||||
{#if loading.contextual}
|
||||
<Skeleton layout={[0.5, [2], 1]} />
|
||||
{#each new Array(8) as _}
|
||||
<Skeleton layout={[[2.8], 0.5]} />
|
||||
{/each}
|
||||
{:else}
|
||||
<TableSimple
|
||||
headers={['name', 'example of value', 'description']}
|
||||
data={contextualVariables}
|
||||
keys={['name', 'value', 'description']}
|
||||
twTextSize="text-sm"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
Reference in New Issue
Block a user