mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-18 16:02:10 +00:00
perf: cache workspace env variables to avoid one query (#5499)
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
-- Add down migration script here
|
||||
DROP TRIGGER workspace_envs_change_trigger ON workspace_env;
|
||||
DROP FUNCTION notify_workspace_envs_change();
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Add up migration script here
|
||||
-- Add up migration script here
|
||||
|
||||
CREATE OR REPLACE FUNCTION notify_workspace_envs_change()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
PERFORM pg_notify('notify_workspace_envs_change', NEW.workspace_id);
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER workspace_envs_change_trigger
|
||||
AFTER INSERT OR UPDATE OF name, value OR DELETE ON workspace_env
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION notify_workspace_envs_change();
|
||||
@@ -702,6 +702,11 @@ Windmill Community Edition {GIT_VERSION}
|
||||
tracing::info!("Webhook change detected, invalidating webhook cache: {}", workspace_id);
|
||||
windmill_api::webhook_util::WEBHOOK_CACHE.remove(workspace_id);
|
||||
},
|
||||
"notify_workspace_envs_change" => {
|
||||
let workspace_id = n.payload();
|
||||
tracing::info!("Workspace envs change detected, invalidating workspace envs cache: {}", workspace_id);
|
||||
windmill_common::variables::CUSTOM_ENVS_CACHE.remove(workspace_id);
|
||||
},
|
||||
"notify_global_setting_change" => {
|
||||
tracing::info!("Global setting change detected: {}", n.payload());
|
||||
match n.payload() {
|
||||
@@ -980,6 +985,7 @@ async fn listen_pg(url: &str) -> Option<PgListener> {
|
||||
"notify_config_change",
|
||||
"notify_global_setting_change",
|
||||
"notify_webhook_change",
|
||||
"notify_workspace_envs_change",
|
||||
])
|
||||
.await
|
||||
{
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::error;
|
||||
use crate::{worker::WORKER_GROUP, BASE_URL, DB};
|
||||
use chrono::{SecondsFormat, Utc};
|
||||
use magic_crypt::{MagicCrypt256, MagicCryptError, MagicCryptTrait};
|
||||
use quick_cache::sync::Cache;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
@@ -160,6 +161,10 @@ pub fn decrypt(mc: &MagicCrypt256, value: String) -> error::Result<String> {
|
||||
|
||||
pub const WM_SCHEDULED_FOR: &str = "WM_SCHEDULED_FOR";
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref CUSTOM_ENVS_CACHE: Cache<String, (i64, Vec<(String, String)>)> = Cache::new(100);
|
||||
}
|
||||
|
||||
pub async fn get_reserved_variables(
|
||||
db: &DB,
|
||||
w_id: &str,
|
||||
@@ -201,6 +206,8 @@ pub async fn get_reserved_variables(
|
||||
}
|
||||
};
|
||||
|
||||
let custom_envs = get_cached_workspace_envs(db, w_id).await;
|
||||
|
||||
let joined_schedule_path = schedule_path
|
||||
.clone()
|
||||
.unwrap_or("manual".to_string())
|
||||
@@ -223,133 +230,157 @@ pub async fn get_reserved_variables(
|
||||
};
|
||||
|
||||
vec![
|
||||
ContextualVariable {
|
||||
name: "WM_WORKSPACE".to_string(),
|
||||
value: w_id.to_string(),
|
||||
description: "Workspace id of the current script".to_string(),
|
||||
ContextualVariable {
|
||||
name: "WM_WORKSPACE".to_string(),
|
||||
value: w_id.to_string(),
|
||||
description: "Workspace id of the current script".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_TOKEN".to_string(),
|
||||
value: token.to_string(),
|
||||
description: "Token ephemeral to the current script with equal permission to the \
|
||||
permission of the run (Usable as a bearer token)"
|
||||
.to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_TOKEN".to_string(),
|
||||
value: token.to_string(),
|
||||
description: "Token ephemeral to the current script with equal permission to the \
|
||||
permission of the run (Usable as a bearer token)"
|
||||
.to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_EMAIL".to_string(),
|
||||
value: email.to_string(),
|
||||
description: "Email of the user that executed the current script".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_USERNAME".to_string(),
|
||||
value: username.to_string(),
|
||||
description: "Username of the user that executed the current script".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_BASE_URL".to_string(),
|
||||
value: BASE_URL.read().await.clone(),
|
||||
description: "base url of this instance".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_JOB_ID".to_string(),
|
||||
value: job_id.to_string(),
|
||||
description: "Job id of the current script".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: WM_SCHEDULED_FOR.to_string(),
|
||||
value: scheduled_for
|
||||
.map(|ts| ts.to_rfc3339_opts(SecondsFormat::Secs, true))
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
description: "date-time in UTC (e.g: 2014-11-28T12:45:59.324310806Z) of when the job was scheduled".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_JOB_PATH".to_string(),
|
||||
value: path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the script or flow being run if any".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_JOB_ID".to_string(),
|
||||
value: flow_id.unwrap_or_else(|| "".to_string()),
|
||||
description: "Job id of the encapsulating flow if the job is a flow step".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_ROOT_FLOW_JOB_ID".to_string(),
|
||||
value: root_flow_id.unwrap_or_else(|| "".to_string()),
|
||||
description: "Job id of the root flow if the job is a flow step".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_PATH".to_string(),
|
||||
value: flow_path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the encapsulating flow if the job is a flow step".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_EMAIL".to_string(),
|
||||
value: email.to_string(),
|
||||
description: "Email of the user that executed the current script".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_USERNAME".to_string(),
|
||||
value: username.to_string(),
|
||||
description: "Username of the user that executed the current script".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_BASE_URL".to_string(),
|
||||
value: BASE_URL.read().await.clone(),
|
||||
description: "base url of this instance".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_JOB_ID".to_string(),
|
||||
value: job_id.to_string(),
|
||||
description: "Job id of the current script".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: WM_SCHEDULED_FOR.to_string(),
|
||||
value: scheduled_for
|
||||
.map(|ts| ts.to_rfc3339_opts(SecondsFormat::Secs, true))
|
||||
.unwrap_or_else(|| "".to_string()),
|
||||
description: "date-time in UTC (e.g: 2014-11-28T12:45:59.324310806Z) of when the job was scheduled".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_JOB_PATH".to_string(),
|
||||
value: path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the script or flow being run if any".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_JOB_ID".to_string(),
|
||||
value: flow_id.unwrap_or_else(|| "".to_string()),
|
||||
description: "Job id of the encapsulating flow if the job is a flow step".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_ROOT_FLOW_JOB_ID".to_string(),
|
||||
value: root_flow_id.unwrap_or_else(|| "".to_string()),
|
||||
description: "Job id of the root flow if the job is a flow step".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_PATH".to_string(),
|
||||
value: flow_path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the encapsulating flow if the job is a flow step".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
|
||||
ContextualVariable {
|
||||
name: "WM_SCHEDULE_PATH".to_string(),
|
||||
value: schedule_path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the schedule if the job of the step or encapsulating step has \
|
||||
been triggered by a schedule"
|
||||
.to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_PERMISSIONED_AS".to_string(),
|
||||
value: permissioned_as.to_string(),
|
||||
description: "Fully Qualified (u/g) owner name of executor of the job".to_string(),
|
||||
ContextualVariable {
|
||||
name: "WM_SCHEDULE_PATH".to_string(),
|
||||
value: schedule_path.unwrap_or_else(|| "".to_string()),
|
||||
description: "Path of the schedule if the job of the step or encapsulating step has \
|
||||
been triggered by a schedule"
|
||||
.to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_STATE_PATH".to_string(),
|
||||
value: state_path.clone(),
|
||||
description: "State resource path unique to a script and its trigger".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_STEP_ID".to_string(),
|
||||
value: step_id.unwrap_or_else(|| "".to_string()),
|
||||
description: "The node id in a flow (like 'a', 'b', or 'f')".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_OBJECT_PATH".to_string(),
|
||||
value: object_path,
|
||||
description: "Script or flow step execution unique path, useful for storing results in an external service".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_OIDC_JWT".to_string(),
|
||||
value: jwt_token.unwrap_or_else(|| "".to_string()),
|
||||
description: "OIDC JWT token (EE only)".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_WORKER_GROUP".to_string(),
|
||||
value: WORKER_GROUP.clone(),
|
||||
description: "name of the worker group the job is running on".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
].into_iter().chain( sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT name, value FROM workspace_env WHERE workspace_id = $1",
|
||||
)
|
||||
.bind(w_id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_iter().map(|(name, value)| ContextualVariable {
|
||||
name,
|
||||
value,
|
||||
description: "Custom workspace environment variable".to_string(),
|
||||
is_custom: true,
|
||||
})).collect()
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_PERMISSIONED_AS".to_string(),
|
||||
value: permissioned_as.to_string(),
|
||||
description: "Fully Qualified (u/g) owner name of executor of the job".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_STATE_PATH".to_string(),
|
||||
value: state_path.clone(),
|
||||
description: "State resource path unique to a script and its trigger".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_FLOW_STEP_ID".to_string(),
|
||||
value: step_id.unwrap_or_else(|| "".to_string()),
|
||||
description: "The node id in a flow (like 'a', 'b', or 'f')".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_OBJECT_PATH".to_string(),
|
||||
value: object_path,
|
||||
description: "Script or flow step execution unique path, useful for storing results in an external service".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_OIDC_JWT".to_string(),
|
||||
value: jwt_token.unwrap_or_else(|| "".to_string()),
|
||||
description: "OIDC JWT token (EE only)".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
ContextualVariable {
|
||||
name: "WM_WORKER_GROUP".to_string(),
|
||||
value: WORKER_GROUP.clone(),
|
||||
description: "name of the worker group the job is running on".to_string(),
|
||||
is_custom: false,
|
||||
},
|
||||
].into_iter().chain(custom_envs.into_iter().map(|(name, value)| ContextualVariable {
|
||||
name,
|
||||
value,
|
||||
description: "Custom workspace environment variable".to_string(),
|
||||
is_custom: true,
|
||||
})
|
||||
).collect()
|
||||
}
|
||||
|
||||
async fn get_cached_workspace_envs(
|
||||
db: &sqlx::Pool<sqlx::Postgres>,
|
||||
w_id: &str,
|
||||
) -> Vec<(String, String)> {
|
||||
let cached_envs_o = CUSTOM_ENVS_CACHE.get(w_id).and_then(|(ts, envs)| {
|
||||
if ts > chrono::Utc::now().timestamp() - (60 * 15) {
|
||||
Some(envs)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
let custom_envs = if let Some(cached_envs) = cached_envs_o {
|
||||
cached_envs
|
||||
} else {
|
||||
let custom_envs = sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT name, value FROM workspace_env WHERE workspace_id = $1",
|
||||
)
|
||||
.bind(w_id)
|
||||
.fetch_all(db)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
CUSTOM_ENVS_CACHE.insert(
|
||||
w_id.to_string(),
|
||||
(chrono::Utc::now().timestamp(), custom_envs.clone()),
|
||||
);
|
||||
custom_envs
|
||||
};
|
||||
custom_envs
|
||||
}
|
||||
|
||||
@@ -40,9 +40,17 @@
|
||||
name: name
|
||||
}
|
||||
})
|
||||
sendUserToast(`${edit ? 'Updated' : 'Created'} contextual variable ${name}`)
|
||||
sendUserToast(
|
||||
`${
|
||||
edit ? 'Updated' : 'Created'
|
||||
} contextual variable ${name}. It may take up to a few minutes to update.`
|
||||
)
|
||||
dispatch('update')
|
||||
|
||||
drawer.closeDrawer()
|
||||
setTimeout(() => {
|
||||
dispatch('update')
|
||||
}, 5000)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -126,7 +126,12 @@
|
||||
}
|
||||
})
|
||||
loadContextualVariables()
|
||||
sendUserToast(`Custom contextual variable ${row.name} was deleted`)
|
||||
sendUserToast(
|
||||
`Custom contextual variable ${row.name} was deleted. It may take up to a few minutes to update.`
|
||||
)
|
||||
setTimeout(() => {
|
||||
loadContextualVariables()
|
||||
}, 5000)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -139,327 +144,332 @@
|
||||
f={(x) => x.path + ' ' + x.description}
|
||||
/>
|
||||
|
||||
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find(_ => _.id === $workspaceStore)?.operator_settings?.variables}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
|
||||
<p class="font-bold">Unauthorized</p>
|
||||
<p>Page not available for operators</p>
|
||||
</div>
|
||||
{#if $userStore?.operator && $workspaceStore && !$userWorkspaces.find((_) => _.id === $workspaceStore)?.operator_settings?.variables}
|
||||
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4 mt-12" role="alert">
|
||||
<p class="font-bold">Unauthorized</p>
|
||||
<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
|
||||
<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>
|
||||
{/if}
|
||||
|
||||
<ConfirmationModal
|
||||
|
||||
Reference in New Issue
Block a user