use scroll for variables page
This commit is contained in:
@@ -37,6 +37,7 @@
|
||||
EyeOff,
|
||||
Circle
|
||||
} from 'lucide-svelte'
|
||||
import Scroll from '$lib/components/Scroll.svelte'
|
||||
|
||||
type ListableVariableW = ListableVariable & { canWrite: boolean }
|
||||
|
||||
@@ -150,326 +151,330 @@
|
||||
<p>Page not available for operators</p>
|
||||
</div>
|
||||
{:else}
|
||||
<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 contextual variable
|
||||
</Button>
|
||||
{:else}
|
||||
<Button size="md" startIcon={{ icon: Plus }} on:click={() => variableEditor.initNew()}>
|
||||
New 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()
|
||||
}}
|
||||
/>
|
||||
|
||||
<Tabs bind:selected={tab}>
|
||||
<Tab size="md" value="workspace">
|
||||
<div class="flex gap-2 items-center my-1">
|
||||
<Building size={18} />
|
||||
Workspace
|
||||
<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 contextual variable
|
||||
</Button>
|
||||
{:else}
|
||||
<Button size="md" startIcon={{ icon: Plus }} on:click={() => variableEditor.initNew()}>
|
||||
New variable
|
||||
</Button>
|
||||
{/if}
|
||||
</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" />
|
||||
</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>
|
||||
</PageHeader>
|
||||
|
||||
<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>
|
||||
{: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}
|
||||
∗∗∗∗
|
||||
{/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>
|
||||
{/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)
|
||||
}
|
||||
}
|
||||
},
|
||||
disabled: !owner
|
||||
},
|
||||
...(isDeployable(
|
||||
is_secret ? 'secret' : 'variable',
|
||||
path,
|
||||
deployUiSettings
|
||||
)
|
||||
? [
|
||||
{
|
||||
displayName: 'Deploy to prod/staging',
|
||||
icon: FileUp,
|
||||
action: () => {
|
||||
deploymentDrawer.openDrawer(path, 'variable')
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
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}
|
||||
{:else}
|
||||
<PageHeader title="Custom contextual variables" primary={false} />
|
||||
{#if contextualVariables.filter((x) => x.is_custom).length === 0}
|
||||
</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" />
|
||||
</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 custom contextual variables found</div>
|
||||
<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>
|
||||
</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)
|
||||
<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}
|
||||
∗∗∗∗
|
||||
{/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>
|
||||
{/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)
|
||||
}
|
||||
}
|
||||
},
|
||||
disabled: !owner
|
||||
},
|
||||
...(isDeployable(
|
||||
is_secret ? 'secret' : 'variable',
|
||||
path,
|
||||
deployUiSettings
|
||||
)
|
||||
? [
|
||||
{
|
||||
displayName: 'Deploy to prod/staging',
|
||||
icon: FileUp,
|
||||
action: () => {
|
||||
deploymentDrawer.openDrawer(path, 'variable')
|
||||
}
|
||||
}
|
||||
]
|
||||
: []),
|
||||
{
|
||||
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}
|
||||
{: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}
|
||||
]
|
||||
}
|
||||
: 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']}
|
||||
/>
|
||||
{/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']}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
</div>
|
||||
{/if}
|
||||
</CenteredPage>
|
||||
</Scroll>
|
||||
{/if}
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
Reference in New Issue
Block a user