use scroll onschedules, resources and variables

This commit is contained in:
Guilhem
2025-03-31 17:26:23 +01:00
parent 48dbc7cb37
commit 8dda89b513
3 changed files with 710 additions and 718 deletions
@@ -60,7 +60,6 @@
import EditableSchemaWrapper from '$lib/components/schema/EditableSchemaWrapper.svelte'
import ResourceEditorDrawer from '$lib/components/ResourceEditorDrawer.svelte'
import GfmMarkdown from '$lib/components/GfmMarkdown.svelte'
import Scroll from '$lib/components/Scroll.svelte'
type ResourceW = ListableResource & { canWrite: boolean; marked?: string }
type ResourceTypeW = ResourceType & { canWrite: boolean }
@@ -610,423 +609,421 @@
<p>Page not available for operators</p>
</div>
{:else}
<Scroll class="h-full w-full">
<CenteredPage>
<PageHeader
title="Resources"
tooltip="Save and permission rich objects (JSON) including credentials obtained through OAuth."
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
>
<div class="flex flex-row justify-end gap-4">
<Button variant="border" size="md" startIcon={{ icon: Plus }} on:click={startNewType}>
Add resource type
</Button>
<Button size="md" startIcon={{ icon: Link }} on:click={() => appConnect.open?.()}>
Add resource
</Button>
</div>
</PageHeader>
<div class="flex justify-between">
<Tabs
class="w-full"
bind:selected={tab}
on:selected={(e) => {
if (e.detail == 'cache') {
loading.resources = true
loadCache()
} else if (e.detail == 'states') {
loading.resources = true
loadState()
}
}}
>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Workspace
</div>
</Tab>
<Tab size="md" value="types">
<div class="flex gap-2 items-center my-1">
Resource Types
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
>
Every resource has a Resource Type attached to it which contains its schema and make
it easy in scripts and flows to accept only resources of a specific resource type.
</Tooltip>
</div>
</Tab>
<Tab size="md" value="states">
<div class="flex gap-2 items-center my-1">
States
<Tooltip>
States are actually resources (but excluded from the Workspace tab for clarity).
States are used by scripts to keep data persistent between runs of the same script
by the same trigger (schedule or user)
</Tooltip>
</div>
</Tab>
<Tab size="md" value="cache">
<div class="flex gap-2 items-center my-1">
Cache
<Tooltip>
Cached results are actually resources (but excluded from the Workspace tab for
clarity). Cache are used by flows's step to cache result to avoid recomputing
unnecessarily
</Tooltip>
</div>
</Tab>
<Tab size="md" value="theme">
<div class="flex gap-2 items-center my-1">
Theme
<Tooltip>
Theme are actually resources (but excluded from the Workspace tab for clarity).
Theme are used by the apps to customize their look and feel.
</Tooltip>
</div>
</Tab>
</Tabs>
<div class="flex">
<Button
variant="border"
color="light"
on:click={reload}
startIcon={{
icon: RotateCw,
classes: loading.resources || loading.types ? 'animate-spin' : ''
}}
/>
</div>
<CenteredPage useScroll>
<PageHeader
title="Resources"
tooltip="Save and permission rich objects (JSON) including credentials obtained through OAuth."
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
>
<div class="flex flex-row justify-end gap-4">
<Button variant="border" size="md" startIcon={{ icon: Plus }} on:click={startNewType}>
Add resource type
</Button>
<Button size="md" startIcon={{ icon: Link }} on:click={() => appConnect.open?.()}>
Add resource
</Button>
</div>
{#if tab == 'workspace' || tab == 'states' || tab == 'cache' || tab == 'theme'}
<div class="pt-2">
<input placeholder="Search Resource" bind:value={filter} class="input mt-1" />
</div>
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
{#if tab != 'states' && tab != 'cache'}
<ListFilters
queryName="app_filter"
bind:selectedFilter={typeFilter}
filters={types}
resourceType
/>
{:else}
<div class="h-4" />
{/if}
</PageHeader>
<div class="flex justify-between">
<Tabs
class="w-full"
bind:selected={tab}
on:selected={(e) => {
if (e.detail == 'cache') {
loading.resources = true
loadCache()
} else if (e.detail == 'states') {
loading.resources = true
loadState()
}
}}
>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Workspace
</div>
</Tab>
<Tab size="md" value="types">
<div class="flex gap-2 items-center my-1">
Resource Types
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/resources_and_types"
>
Every resource has a Resource Type attached to it which contains its schema and make
it easy in scripts and flows to accept only resources of a specific resource type.
</Tooltip>
</div>
</Tab>
<Tab size="md" value="states">
<div class="flex gap-2 items-center my-1">
States
<Tooltip>
States are actually resources (but excluded from the Workspace tab for clarity).
States are used by scripts to keep data persistent between runs of the same script by
the same trigger (schedule or user)
</Tooltip>
</div>
</Tab>
<Tab size="md" value="cache">
<div class="flex gap-2 items-center my-1">
Cache
<Tooltip>
Cached results are actually resources (but excluded from the Workspace tab for
clarity). Cache are used by flows's step to cache result to avoid recomputing
unnecessarily
</Tooltip>
</div>
</Tab>
<Tab size="md" value="theme">
<div class="flex gap-2 items-center my-1">
Theme
<Tooltip>
Theme are actually resources (but excluded from the Workspace tab for clarity). Theme
are used by the apps to customize their look and feel.
</Tooltip>
</div>
</Tab>
</Tabs>
<div class="flex">
<Button
variant="border"
color="light"
on:click={reload}
startIcon={{
icon: RotateCw,
classes: loading.resources || loading.types ? 'animate-spin' : ''
}}
/>
</div>
</div>
{#if tab == 'workspace' || tab == 'states' || tab == 'cache' || tab == 'theme'}
<div class="pt-2">
<input placeholder="Search Resource" bind:value={filter} class="input mt-1" />
</div>
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
{#if tab != 'states' && tab != 'cache'}
<ListFilters
queryName="app_filter"
bind:selectedFilter={typeFilter}
filters={types}
resourceType
/>
{:else}
<div class="h-4" />
{/if}
<div class="overflow-x-auto pb-40 mt-4">
{#if loading.resources}
<Skeleton layout={[0.5, [2], 1]} />
{#each new Array(6) as _}
<Skeleton layout={[[4], 0.7]} />
{/each}
{:else if filteredItems?.length == 0}
<div class="flex flex-col items-center justify-center h-full">
<div class="text-md font-medium">No resources found</div>
<div class="text-sm text-secondary">
Try changing the filters or creating a new resource
</div>
</div>
{:else}
<DataTable>
<Head>
<Row>
<Cell head first />
<Cell head>Path</Cell>
<Cell head>Resource type</Cell>
<Cell head>Description</Cell>
<Cell head />
<Cell head last />
</Row>
</Head>
<tbody class="divide-y bg-surface">
{#if filteredItems}
{#each filteredItems as { path, description, resource_type, extra_perms, canWrite, is_oauth, is_linked, account, refresh_error, is_expired, marked, is_refreshed }}
<Row>
<Cell first>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</Cell>
<Cell>
<a
class="break-all"
href="#{path}"
on:click={() => resourceEditor?.initEdit?.(path)}
>{#if marked}{@html marked}{:else}{path}{/if}</a
>
</Cell>
<Cell>
<a
href="#{name}"
on:click={() => {
const linkedRt = resourceTypes?.find((rt) => rt.name === resource_type)
if (linkedRt) {
resourceTypeViewerObj = {
rt: linkedRt.name,
//@ts-ignore
schema: linkedRt.schema,
description: linkedRt.description ?? '',
formatExtension: linkedRt.format_extension
}
resourceTypeViewer.openDrawer?.()
} else {
sendUserToast(
`Resource type ${resource_type} not found in workspace.`,
true
)
}
}}
>
<IconedResourceType
name={resource_type}
after={true}
formatExtension={resourceNameToFileExt(resource_type)}
/>
</a>
</Cell>
<Cell>
<span class="text-tertiary text-xs">
{removeMarkdown(truncate(description ?? '', 30))}
</span>
</Cell>
<Cell>
<div class="flex flex-row text-center">
<div class="w-10">
{#if is_linked}
<Popover>
<Link />
<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 is_refreshed}
<Popover>
<RotateCw />
<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 pt-1.5">
{#if refresh_error}
<Popover>
<Circle
class="text-red-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
</Popover>
{:else if is_expired}
<Popover>
<Circle
class="text-yellow-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<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>
<Circle
class="text-green-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<div slot="text">
The resource was connected through OAuth and the token is not
expired.
</div>
</Popover>
{/if}
</div>
{/if}
</div>
</Cell>
<Cell>
<Dropdown
items={[
{
displayName: !canWrite ? 'View Permissions' : 'Share',
icon: Share,
action: () => {
shareModal.openDrawer?.(path, 'resource')
}
},
{
displayName: 'Edit',
icon: Pen,
disabled: !canWrite,
action: () => {
resourceEditor?.initEdit?.(path)
}
},
...(isDeployable('resource', path, deployUiSettings)
? [
{
displayName: 'Deploy to prod/staging',
icon: FileUp,
action: () => {
deploymentDrawer.openDrawer(path, 'resource')
}
}
]
: []),
{
displayName: 'Delete',
disabled: !canWrite,
icon: Trash,
type: 'delete',
action: (event) => {
// TODO
// @ts-ignore
if (event?.shiftKey) {
deleteResource(path, account)
} else {
deleteConfirmedCallback = () => {
deleteResource(path, account)
}
}
}
},
...(account != undefined
? [
{
displayName: 'Refresh token',
icon: RotateCw,
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
id: account ?? 0,
requestBody: {
path
}
})
sendUserToast('Token refreshed')
loadResources()
}
}
]
: [])
]}
/>
</Cell>
</Row>
{/each}
{/if}
</tbody>
</DataTable>
{/if}
</div>
{:else if tab == 'types'}
{#if loading.types}
<div class="overflow-x-auto pb-40 mt-4">
{#if loading.resources}
<Skeleton layout={[0.5, [2], 1]} />
{#each new Array(6) as _}
<Skeleton layout={[[4], 0.7]} />
{/each}
{:else}
<div class="overflow-auto mt-4">
<DataTable>
<Head>
<Row>
<Cell head first>Name</Cell>
<Cell head>Description</Cell>
<Cell head last />
</Row>
</Head>
<tbody class="divide-y bg-surface">
{#if resourceTypes}
{#each resourceTypes as { name, description, schema, canWrite, format_extension }}
<Row>
<Cell first>
<a
href="#{name}"
on:click={() => {
resourceTypeViewerObj = {
rt: name,
//@ts-ignore
schema: schema,
description: description ?? '',
formatExtension: format_extension
}
resourceTypeViewer.openDrawer?.()
}}
>
<IconedResourceType
after={true}
{name}
formatExtension={format_extension}
/>
</a>
</Cell>
<Cell>
<span class="text-tertiary text-xs w-96 flex flex-wrap whitespace-pre-wrap">
{removeMarkdown(truncate(description ?? '', 200))}
</span>
</Cell>
<Cell last>
{#if !canWrite}
<Badge>
Shared globally
<Tooltip>
This resource type is from the 'admins' workspace shared with all
workspaces
</Tooltip>
</Badge>
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
<div class="flex flex-row-reverse gap-2">
<Button
size="xs"
color="red"
variant="border"
btnClasses="border-0"
startIcon={{ icon: Trash }}
on:click={() => handleDeleteResourceType(name)}
>
Delete
</Button>
<Button
size="xs"
color="light"
startIcon={{ icon: Pen }}
on:click={() => startEditResourceType(name)}
>
Edit
</Button>
</div>
{:else}
<Badge>
Non Editable
<Tooltip>
Since resource types are shared with the whole workspace, only admins
can edit/delete them
</Tooltip>
</Badge>
{/if}
</Cell>
</Row>
{/each}
{/if}
</tbody>
</DataTable>
{:else if filteredItems?.length == 0}
<div class="flex flex-col items-center justify-center h-full">
<div class="text-md font-medium">No resources found</div>
<div class="text-sm text-secondary">
Try changing the filters or creating a new resource
</div>
</div>
{:else}
<DataTable>
<Head>
<Row>
<Cell head first />
<Cell head>Path</Cell>
<Cell head>Resource type</Cell>
<Cell head>Description</Cell>
<Cell head />
<Cell head last />
</Row>
</Head>
<tbody class="divide-y bg-surface">
{#if filteredItems}
{#each filteredItems as { path, description, resource_type, extra_perms, canWrite, is_oauth, is_linked, account, refresh_error, is_expired, marked, is_refreshed }}
<Row>
<Cell first>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</Cell>
<Cell>
<a
class="break-all"
href="#{path}"
on:click={() => resourceEditor?.initEdit?.(path)}
>{#if marked}{@html marked}{:else}{path}{/if}</a
>
</Cell>
<Cell>
<a
href="#{name}"
on:click={() => {
const linkedRt = resourceTypes?.find((rt) => rt.name === resource_type)
if (linkedRt) {
resourceTypeViewerObj = {
rt: linkedRt.name,
//@ts-ignore
schema: linkedRt.schema,
description: linkedRt.description ?? '',
formatExtension: linkedRt.format_extension
}
resourceTypeViewer.openDrawer?.()
} else {
sendUserToast(
`Resource type ${resource_type} not found in workspace.`,
true
)
}
}}
>
<IconedResourceType
name={resource_type}
after={true}
formatExtension={resourceNameToFileExt(resource_type)}
/>
</a>
</Cell>
<Cell>
<span class="text-tertiary text-xs">
{removeMarkdown(truncate(description ?? '', 30))}
</span>
</Cell>
<Cell>
<div class="flex flex-row text-center">
<div class="w-10">
{#if is_linked}
<Popover>
<Link />
<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 is_refreshed}
<Popover>
<RotateCw />
<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 pt-1.5">
{#if refresh_error}
<Popover>
<Circle
class="text-red-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
</Popover>
{:else if is_expired}
<Popover>
<Circle
class="text-yellow-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<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>
<Circle
class="text-green-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<div slot="text">
The resource was connected through OAuth and the token is not
expired.
</div>
</Popover>
{/if}
</div>
{/if}
</div>
</Cell>
<Cell>
<Dropdown
items={[
{
displayName: !canWrite ? 'View Permissions' : 'Share',
icon: Share,
action: () => {
shareModal.openDrawer?.(path, 'resource')
}
},
{
displayName: 'Edit',
icon: Pen,
disabled: !canWrite,
action: () => {
resourceEditor?.initEdit?.(path)
}
},
...(isDeployable('resource', path, deployUiSettings)
? [
{
displayName: 'Deploy to prod/staging',
icon: FileUp,
action: () => {
deploymentDrawer.openDrawer(path, 'resource')
}
}
]
: []),
{
displayName: 'Delete',
disabled: !canWrite,
icon: Trash,
type: 'delete',
action: (event) => {
// TODO
// @ts-ignore
if (event?.shiftKey) {
deleteResource(path, account)
} else {
deleteConfirmedCallback = () => {
deleteResource(path, account)
}
}
}
},
...(account != undefined
? [
{
displayName: 'Refresh token',
icon: RotateCw,
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
id: account ?? 0,
requestBody: {
path
}
})
sendUserToast('Token refreshed')
loadResources()
}
}
]
: [])
]}
/>
</Cell>
</Row>
{/each}
{/if}
</tbody>
</DataTable>
{/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}
<div class="overflow-auto mt-4">
<DataTable>
<Head>
<Row>
<Cell head first>Name</Cell>
<Cell head>Description</Cell>
<Cell head last />
</Row>
</Head>
<tbody class="divide-y bg-surface">
{#if resourceTypes}
{#each resourceTypes as { name, description, schema, canWrite, format_extension }}
<Row>
<Cell first>
<a
href="#{name}"
on:click={() => {
resourceTypeViewerObj = {
rt: name,
//@ts-ignore
schema: schema,
description: description ?? '',
formatExtension: format_extension
}
resourceTypeViewer.openDrawer?.()
}}
>
<IconedResourceType
after={true}
{name}
formatExtension={format_extension}
/>
</a>
</Cell>
<Cell>
<span class="text-tertiary text-xs w-96 flex flex-wrap whitespace-pre-wrap">
{removeMarkdown(truncate(description ?? '', 200))}
</span>
</Cell>
<Cell last>
{#if !canWrite}
<Badge>
Shared globally
<Tooltip>
This resource type is from the 'admins' workspace shared with all
workspaces
</Tooltip>
</Badge>
{:else if $userStore?.is_admin || $userStore?.is_super_admin}
<div class="flex flex-row-reverse gap-2">
<Button
size="xs"
color="red"
variant="border"
btnClasses="border-0"
startIcon={{ icon: Trash }}
on:click={() => handleDeleteResourceType(name)}
>
Delete
</Button>
<Button
size="xs"
color="light"
startIcon={{ icon: Pen }}
on:click={() => startEditResourceType(name)}
>
Edit
</Button>
</div>
{:else}
<Badge>
Non Editable
<Tooltip>
Since resource types are shared with the whole workspace, only admins
can edit/delete them
</Tooltip>
</Badge>
{/if}
</Cell>
</Row>
{/each}
{/if}
</tbody>
</DataTable>
</div>
{/if}
</CenteredPage>
</Scroll>
{/if}
</CenteredPage>
{/if}
<SupabaseConnect bind:this={supabaseConnect} on:refresh={loadResources} />
@@ -279,7 +279,7 @@
<p>Page not available for operators</p>
</div>
{:else}
<CenteredPage>
<CenteredPage useScroll>
<PageHeader
title="Schedules"
tooltip="Trigger Scripts and Flows according to a cron schedule"
@@ -37,7 +37,6 @@
EyeOff,
Circle
} from 'lucide-svelte'
import Scroll from '$lib/components/Scroll.svelte'
type ListableVariableW = ListableVariable & { canWrite: boolean }
@@ -151,330 +150,326 @@
<p>Page not available for operators</p>
</div>
{:else}
<Scroll class="h-full w-full">
<CenteredPage>
<PageHeader
title="Variables"
tooltip="Save and permission strings to be reused in Scripts and Flows."
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
>
<div class="flex flex-row justify-end">
{#if tab == 'contextual' && ($userStore?.is_admin || $userStore?.is_super_admin)}
<Button
size="md"
startIcon={{ icon: Plus }}
on:click={() => contextualVariableEditor.initNew()}
>
New&nbsp;contextual&nbsp;variable
</Button>
{:else}
<Button size="md" startIcon={{ icon: Plus }} on:click={() => variableEditor.initNew()}>
New&nbsp;variable
</Button>
{/if}
</div>
</PageHeader>
<CenteredPage useScroll>
<PageHeader
title="Variables"
tooltip="Save and permission strings to be reused in Scripts and Flows."
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
>
<div class="flex flex-row justify-end">
{#if tab == 'contextual' && ($userStore?.is_admin || $userStore?.is_super_admin)}
<Button
size="md"
startIcon={{ icon: Plus }}
on:click={() => contextualVariableEditor.initNew()}
>
New&nbsp;contextual&nbsp;variable
</Button>
{:else}
<Button size="md" startIcon={{ icon: Plus }} on:click={() => variableEditor.initNew()}>
New&nbsp;variable
</Button>
{/if}
</div>
</PageHeader>
<VariableEditor bind:this={variableEditor} on:create={loadVariables} />
<ContextualVariableEditor
bind:this={contextualVariableEditor}
on:update={loadContextualVariables}
/>
<ShareModal
bind:this={shareModal}
on:change={() => {
loadVariables()
}}
/>
<VariableEditor bind:this={variableEditor} on:create={loadVariables} />
<ContextualVariableEditor
bind:this={contextualVariableEditor}
on:update={loadContextualVariables}
/>
<ShareModal
bind:this={shareModal}
on:change={() => {
loadVariables()
}}
/>
<Tabs bind:selected={tab}>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Workspace
</div>
</Tab>
<Tab size="md" value="contextual">
<div class="flex gap-2 items-center my-1">
<DollarSign size={18} />
Contextual
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets#contextual-variables"
>
Contextual variables are passed as environment variables 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" />
<Tabs bind:selected={tab}>
<Tab size="md" value="workspace">
<div class="flex gap-2 items-center my-1">
<Building size={18} />
Workspace
</div>
<div class="min-h-[56px]">
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
</Tab>
<Tab size="md" value="contextual">
<div class="flex gap-2 items-center my-1">
<DollarSign size={18} />
Contextual
<Tooltip
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets#contextual-variables"
>
Contextual variables are passed as environment variables when running a script and
depends on the execution context.
</Tooltip>
</div>
<div class="relative overflow-x-auto pb-40 pr-4">
{#if !filteredItems}
<Skeleton layout={[0.5, [2], 1]} />
{#each new Array(3) as _}
<Skeleton layout={[[3.5], 0.5]} />
{/each}
{:else if filteredItems.length == 0}
<div class="flex flex-col items-center justify-center h-full">
<div class="text-md font-medium">No variables found</div>
<div class="text-sm text-secondary">
Try changing the filters or creating a new variable
</div>
</Tab>
</Tabs>
{#if tab == 'workspace'}
<div class="pt-2">
<input placeholder="Search Variable" bind:value={filter} class="input mt-1" />
</div>
<div class="min-h-[56px]">
<ListFilters bind:selectedFilter={ownerFilter} filters={owners} />
</div>
<div class="relative overflow-x-auto pb-40 pr-4">
{#if !filteredItems}
<Skeleton layout={[0.5, [2], 1]} />
{#each new Array(3) as _}
<Skeleton layout={[[3.5], 0.5]} />
{/each}
{:else if filteredItems.length == 0}
<div class="flex flex-col items-center justify-center h-full">
<div class="text-md font-medium">No variables found</div>
<div class="text-sm text-secondary">
Try changing the filters or creating a new variable
</div>
{:else}
<DataTable size="xs">
<Head>
<tr>
<Cell head first class="!px-0" />
<Cell head>Path</Cell>
<Cell head>Value</Cell>
<Cell head>Description</Cell>
<Cell head />
<Cell head last />
</tr>
</Head>
<tbody class="divide-y">
{#each filteredItems as { path, value, is_secret, description, extra_perms, canWrite, account, is_refreshed, is_expired, refresh_error, is_linked, marked }}
<Row>
<Cell class="!px-0 text-center w-12" first>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</Cell>
<Cell>
<a
class="break-all"
id="edit-{path}"
on:click={() => variableEditor.editVariable(path)}
href="#{path}"
>
{#if marked}
{@html marked}
</div>
{:else}
<DataTable size="xs">
<Head>
<tr>
<Cell head first class="!px-0" />
<Cell head>Path</Cell>
<Cell head>Value</Cell>
<Cell head>Description</Cell>
<Cell head />
<Cell head last />
</tr>
</Head>
<tbody class="divide-y">
{#each filteredItems as { path, value, is_secret, description, extra_perms, canWrite, account, is_refreshed, is_expired, refresh_error, is_linked, marked }}
<Row>
<Cell class="!px-0 text-center w-12" first>
<SharedBadge {canWrite} extraPerms={extra_perms} />
</Cell>
<Cell>
<a
class="break-all"
id="edit-{path}"
on:click={() => variableEditor.editVariable(path)}
href="#{path}"
>
{#if marked}
{@html marked}
{:else}
{path}
{/if}
</a>
</Cell>
<Cell>
<span class="inline-flex flex-row items-center gap-2">
<div class="text-sm break-words">
{#if value}
{truncate(value, 20)}
{:else}
{path}
{/if}
</a>
</Cell>
<Cell>
<span class="inline-flex flex-row items-center gap-2">
<div class="text-sm break-words">
{#if value}
{truncate(value, 20)}
{:else}
&lowast;&lowast;&lowast;&lowast;
{/if}
</div>
{#if is_secret}
<Popover notClickable>
<EyeOff size={12} />
<span slot="text">This item is secret</span>
</Popover>
{/if}
</span>
</Cell>
<Cell class="break-words">
<span class="text-xs text-tertiary">{truncate(description ?? '', 50)} </span>
</Cell>
<Cell class="text-center">
<div class="flex flex-row items-center gap-4">
{#if is_linked}
<Popover notClickable>
<Link size={16} />
<div slot="text">
This variable is linked with a resource of the same path. They are
deleted and renamed together.
</div>
</Popover>
{/if}
{#if account}
<Popover notClickable>
<RefreshCw size={16} />
<div slot="text">
This OAuth token will be kept up-to-date in the background by Windmill
using its refresh token
</div>
</Popover>
{/if}
{#if is_refreshed}
<div class="">
{#if refresh_error}
<Popover notClickable>
<div
class="relative inline-flex justify-center items-center w-4 h-4"
>
<Circle
class="text-red-600 animate-ping absolute z-50 w-4 h-4 fill-current"
size={12}
/>
<Circle
class="text-red-600 relative inline-flex fill-current "
size={12}
/>
</div>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
</Popover>
{:else if is_expired}
<Popover notClickable>
<Circle
class="text-yellow-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<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>
<Circle
class="text-green-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<div slot="text">
The variable was connected through OAuth and the token is not
expired.
</div>
</Popover>
{/if}
</div>
&lowast;&lowast;&lowast;&lowast;
{/if}
</div>
</Cell>
<Cell last shouldStopPropagation>
<Dropdown
items={() => {
let owner = isOwner(path, $userStore, $workspaceStore)
return [
{
displayName: 'Edit',
icon: Pen,
action: () => variableEditor.editVariable(path),
disabled: !canWrite
},
{
displayName: 'Delete',
icon: Trash,
type: 'delete',
action: (event) => {
if (event['shiftKey']) {
{#if is_secret}
<Popover notClickable>
<EyeOff size={12} />
<span slot="text">This item is secret</span>
</Popover>
{/if}
</span>
</Cell>
<Cell class="break-words">
<span class="text-xs text-tertiary">{truncate(description ?? '', 50)} </span>
</Cell>
<Cell class="text-center">
<div class="flex flex-row items-center gap-4">
{#if is_linked}
<Popover notClickable>
<Link size={16} />
<div slot="text">
This variable is linked with a resource of the same path. They are
deleted and renamed together.
</div>
</Popover>
{/if}
{#if account}
<Popover notClickable>
<RefreshCw size={16} />
<div slot="text">
This OAuth token will be kept up-to-date in the background by Windmill
using its refresh token
</div>
</Popover>
{/if}
{#if is_refreshed}
<div class="">
{#if refresh_error}
<Popover notClickable>
<div class="relative inline-flex justify-center items-center w-4 h-4">
<Circle
class="text-red-600 animate-ping absolute z-50 w-4 h-4 fill-current"
size={12}
/>
<Circle
class="text-red-600 relative inline-flex fill-current "
size={12}
/>
</div>
<div slot="text">
Latest exchange of the refresh token did not succeed. Error: {refresh_error}
</div>
</Popover>
{:else if is_expired}
<Popover notClickable>
<Circle
class="text-yellow-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<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>
<Circle
class="text-green-600 animate-[pulse_5s_linear_infinite] fill-current"
size={12}
/>
<div slot="text">
The variable was connected through OAuth and the token is not
expired.
</div>
</Popover>
{/if}
</div>
{/if}
</div>
</Cell>
<Cell last shouldStopPropagation>
<Dropdown
items={() => {
let owner = isOwner(path, $userStore, $workspaceStore)
return [
{
displayName: 'Edit',
icon: Pen,
action: () => variableEditor.editVariable(path),
disabled: !canWrite
},
{
displayName: 'Delete',
icon: Trash,
type: 'delete',
action: (event) => {
if (event['shiftKey']) {
deleteVariable(path, account)
} else {
deleteConfirmedCallback = () => {
deleteVariable(path, account)
} else {
deleteConfirmedCallback = () => {
deleteVariable(path, account)
}
}
},
disabled: !owner
},
...(isDeployable(
is_secret ? 'secret' : 'variable',
path,
deployUiSettings
)
? [
{
displayName: 'Deploy to prod/staging',
icon: FileUp,
action: () => {
deploymentDrawer.openDrawer(path, 'variable')
}
}
},
disabled: !owner
]
: []),
{
displayName: owner ? 'Share' : 'See Permissions',
action: () => {
shareModal.openDrawer(path, 'variable')
},
...(isDeployable(
is_secret ? 'secret' : 'variable',
path,
deployUiSettings
)
? [
{
displayName: 'Deploy to prod/staging',
icon: FileUp,
action: () => {
deploymentDrawer.openDrawer(path, 'variable')
}
icon: Share
},
...(account != undefined
? [
{
displayName: 'Refresh token',
icon: RefreshCw,
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
id: account ?? 0,
requestBody: {
path
}
})
sendUserToast('Token refreshed')
loadVariables()
}
]
: []),
{
displayName: owner ? 'Share' : 'See Permissions',
action: () => {
shareModal.openDrawer(path, 'variable')
},
icon: Share
},
...(account != undefined
? [
{
displayName: 'Refresh token',
icon: RefreshCw,
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
id: account ?? 0,
requestBody: {
path
}
})
sendUserToast('Token refreshed')
loadVariables()
}
}
]
: [])
]
}}
/>
</Cell>
</Row>
{/each}
</tbody>
</DataTable>
{/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}
}
]
: [])
]
}}
/>
</Cell>
</Row>
{/each}
</tbody>
</DataTable>
{/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}
<PageHeader title="Custom contextual variables" primary={false} />
{#if contextualVariables.filter((x) => x.is_custom).length === 0}
<div class="flex flex-col items-center justify-center h-full">
<div class="text-md font-medium">No custom contextual variables found</div>
</div>
{:else}
<PageHeader title="Custom contextual variables" primary={false} />
{#if contextualVariables.filter((x) => x.is_custom).length === 0}
<div class="flex flex-col items-center justify-center h-full">
<div class="text-md font-medium">No custom contextual variables found</div>
</div>
{:else}
<TableSimple
headers={['Name', 'Value']}
data={contextualVariables.filter((x) => x.is_custom)}
keys={['name', 'value']}
getRowActions={$userStore?.is_admin || $userStore?.is_super_admin
? (row) => {
return [
{
displayName: 'Edit',
action: () => contextualVariableEditor.editVariable(row.name, row.value)
},
{
displayName: 'Delete',
type: 'delete',
action: () => {
deleteContextualVariable(row)
}
}
]
}
: undefined}
/>
{/if}
<PageHeader title="Contextual variables" primary={false} />
<TableSimple
headers={['Name', 'Example of value', 'Description']}
data={contextualVariables.filter((x) => !x.is_custom)}
keys={['name', 'value', 'description']}
headers={['Name', 'Value']}
data={contextualVariables.filter((x) => x.is_custom)}
keys={['name', 'value']}
getRowActions={$userStore?.is_admin || $userStore?.is_super_admin
? (row) => {
return [
{
displayName: 'Edit',
action: () => contextualVariableEditor.editVariable(row.name, row.value)
},
{
displayName: 'Delete',
type: 'delete',
action: () => {
deleteContextualVariable(row)
}
}
]
}
: undefined}
/>
{/if}
</div>
{/if}
</CenteredPage>
</Scroll>
<PageHeader title="Contextual variables" primary={false} />
<TableSimple
headers={['Name', 'Example of value', 'Description']}
data={contextualVariables.filter((x) => !x.is_custom)}
keys={['name', 'value', 'description']}
/>
{/if}
</div>
{/if}
</CenteredPage>
{/if}
<ConfirmationModal