diff --git a/frontend/src/lib/components/common/emptyState/EmptyState.svelte b/frontend/src/lib/components/common/emptyState/EmptyState.svelte
index 2bed452f4a..8510b2b096 100644
--- a/frontend/src/lib/components/common/emptyState/EmptyState.svelte
+++ b/frontend/src/lib/components/common/emptyState/EmptyState.svelte
@@ -6,6 +6,12 @@
icon: any
title: string
description?: string
+ /**
+ * Accent create button. A page that renders one here should hide the page-header
+ * button for the same action while it shows, so the two accent CTAs don't sit on
+ * screen together — and must derive that from the very conditions gating this
+ * `action`, or a state where the card has no button leaves no way to create at all.
+ */
action?: {
label: string
icon?: any
diff --git a/frontend/src/routes/(root)/(logged)/amqp_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/amqp_triggers/+page.svelte
index 8d08defd12..662395541b 100644
--- a/frontend/src/routes/(root)/(logged)/amqp_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/amqp_triggers/+page.svelte
@@ -53,6 +53,7 @@
let triggers: TriggerM[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -286,14 +287,16 @@
title="AMQP triggers"
tooltip="Windmill can connect to an AMQP broker, subscribe to specific topics, and trigger scripts or flows based on those topics."
>
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if isCloudHosted()}
diff --git a/frontend/src/routes/(root)/(logged)/azure_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/azure_triggers/+page.svelte
index 69963ed803..e2ec500532 100644
--- a/frontend/src/routes/(root)/(logged)/azure_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/azure_triggers/+page.svelte
@@ -66,6 +66,7 @@
let triggers: TriggerD[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
let isDeleting = $state(false)
@@ -353,14 +354,16 @@
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if isCloudHosted()}
diff --git a/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte
index 87c9558dbb..e72a6e38cd 100644
--- a/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/email_triggers/+page.svelte
@@ -67,6 +67,9 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(
+ !loading && !triggers?.length && Boolean($userStore?.is_admin || $userStore?.is_super_admin)
+ )
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
let emailDomain: string | null = $state(null)
@@ -281,14 +284,16 @@
documentationLink="https://www.windmill.dev/docs/advanced/email_triggers"
>
{#if $userStore?.is_admin || $userStore?.is_super_admin}
-
+ {#if !emptyCtaShown}
+
+ {/if}
{/if}
diff --git a/frontend/src/routes/(root)/(logged)/folders/+page.svelte b/frontend/src/routes/(root)/(logged)/folders/+page.svelte
index 38038e9656..97e42c1964 100644
--- a/frontend/src/routes/(root)/(logged)/folders/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/folders/+page.svelte
@@ -30,6 +30,12 @@
let newFolderName: string = $state('')
let folders: FolderW[] | undefined = $state(undefined)
+ // Read through a typed parameter: a $derived reading a same-scope `$state(undefined)`
+ // infers `never` and loses `.length`.
+ function hasNoFolders(list: FolderW[] | undefined): boolean {
+ return list?.length === 0
+ }
+ let emptyCtaShown = $derived(hasNoFolders(folders) && !restricted)
let folderDrawer: Drawer | undefined = $state()
let hubDrawer: Drawer | undefined = $state()
let publishFolderName: string = $state('')
@@ -165,7 +171,7 @@
>
New folder
- {:else}
+ {:else if !emptyCtaShown}
{@render newFolderPopover('New folder', 'bottom-end')}
{/if}
diff --git a/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte
index 22234e0f04..2fe1d9027f 100644
--- a/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/gcp_triggers/+page.svelte
@@ -66,6 +66,7 @@
let triggers: TriggerD[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
let isDeleting = $state(false)
@@ -332,14 +333,16 @@
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if isCloudHosted()}
diff --git a/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte
index 6ee790818a..e0f5e3c215 100644
--- a/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/kafka_triggers/+page.svelte
@@ -58,6 +58,7 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -296,14 +297,16 @@
title="Kafka triggers"
tooltip="Windmill can consume kafka events and trigger scripts or flows based on them."
>
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if isCloudHosted()}
diff --git a/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte
index 8a6d84fe90..b63bdaa623 100644
--- a/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/mqtt_triggers/+page.svelte
@@ -53,6 +53,7 @@
let triggers: TriggerM[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -286,14 +287,16 @@
title="MQTT triggers"
tooltip="Windmill can connect to an MQTT broker, subscribe to specific topics, and trigger scripts or flows based on those topics."
>
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if isCloudHosted()}
diff --git a/frontend/src/routes/(root)/(logged)/native_triggers/[service_name]/+page.svelte b/frontend/src/routes/(root)/(logged)/native_triggers/[service_name]/+page.svelte
index f5e9a30dc5..34755f6f80 100644
--- a/frontend/src/routes/(root)/(logged)/native_triggers/[service_name]/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/native_triggers/[service_name]/+page.svelte
@@ -41,6 +41,9 @@
let loading = $state(true)
let serviceAvailable: boolean | undefined = $state(undefined)
let serviceSupported = $state(true)
+ // The card only renders inside the serviceAvailable branch, so the header button
+ // stays put when the service is not connected — that state shows an alert, not a card.
+ let emptyCtaShown = $derived(Boolean(serviceAvailable) && !loading && !triggers?.length)
let editor: NativeTriggerEditor
let filteredItems: TriggerW[] = $state([])
@@ -215,15 +218,17 @@
tooltip="Native triggers managed externally by {serviceConfig?.serviceDisplayName ||
serviceName}. These are more efficient than regular triggers as they're handled directly by the service provider."
>
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if serviceAvailable === false}
diff --git a/frontend/src/routes/(root)/(logged)/nats_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/nats_triggers/+page.svelte
index 834869891a..3405743a1d 100644
--- a/frontend/src/routes/(root)/(logged)/nats_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/nats_triggers/+page.svelte
@@ -58,6 +58,7 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -295,14 +296,16 @@
title="NATS triggers"
tooltip="Windmill can consume NATS events and trigger scripts or flows based on them."
>
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if isCloudHosted()}
diff --git a/frontend/src/routes/(root)/(logged)/postgres_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/postgres_triggers/+page.svelte
index d1c07a7e75..90221b9d9b 100644
--- a/frontend/src/routes/(root)/(logged)/postgres_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/postgres_triggers/+page.svelte
@@ -65,6 +65,7 @@
let triggers: TriggerD[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -361,14 +362,16 @@
title="Postgres triggers"
tooltip="Windmill enables real-time responsiveness by listening to specific database transactions—such as inserts, updates, and deletes—and automatically triggering scripts or workflows in response."
>
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if isCloudHosted()}
diff --git a/frontend/src/routes/(root)/(logged)/resources/+page.svelte b/frontend/src/routes/(root)/(logged)/resources/+page.svelte
index e3c47085c3..adcf7b636e 100644
--- a/frontend/src/routes/(root)/(logged)/resources/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/resources/+page.svelte
@@ -3,12 +3,11 @@
import { page } from '$app/state'
import AppConnect from '$lib/components/AppConnectDrawer.svelte'
import CenteredPage from '$lib/components/CenteredPage.svelte'
- import { Alert, Badge, Button, EmptyState, Skeleton, Tab } from '$lib/components/common'
+ import { Alert, Badge, Button, EmptyState, Skeleton, Tab, TabFade } 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 Tabs from '$lib/components/common/tabs/Tabs.svelte'
- import TabFade from '$lib/components/common/tabs/TabFade.svelte'
import DeployWorkspaceDrawer from '$lib/components/DeployWorkspaceDrawer.svelte'
import Dropdown from '$lib/components/DropdownV2.svelte'
import IconedResourceType from '$lib/components/IconedResourceType.svelte'
@@ -668,6 +667,14 @@
}
}
let emptyState = $derived(emptyStates[tab] ?? emptyStates.workspace)
+
+ let emptyCtaShown = $derived(
+ tab === 'workspace' &&
+ showCreateButtons &&
+ !loading.resources &&
+ filteredItems?.length === 0 &&
+ !hasActiveFilters
+ )
Add resource type
-
+ {#if !emptyCtaShown}
+
+ {/if}
{/if}
diff --git a/frontend/src/routes/(root)/(logged)/routes/+page.svelte b/frontend/src/routes/(root)/(logged)/routes/+page.svelte
index 53de5457d1..39f6b2ac02 100644
--- a/frontend/src/routes/(root)/(logged)/routes/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/routes/+page.svelte
@@ -71,6 +71,7 @@
let triggers: TriggerW[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let openAPISpecGenerator: OpenApiSpecGenerator | undefined = $state()
let routesGenerator: RoutesGenerator | undefined = $state()
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
@@ -320,14 +321,16 @@
>
To OpenAPI spec
-
+ {#if !emptyCtaShown}
+
+ {/if}
diff --git a/frontend/src/routes/(root)/(logged)/schedules/+page.svelte b/frontend/src/routes/(root)/(logged)/schedules/+page.svelte
index f92490ebb8..63e6160436 100644
--- a/frontend/src/routes/(root)/(logged)/schedules/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/schedules/+page.svelte
@@ -255,6 +255,7 @@
let hasActiveFilters = $derived(
Object.values(filters.val).some((v) => v !== undefined && v !== null && v !== '' && v !== false)
)
+ let emptyCtaShown = $derived(!loading && !schedules?.length && !hasActiveFilters)
let allFolders = $derived(
Array.from(
new Set(
@@ -351,16 +352,18 @@
tooltip="Trigger Scripts and Flows according to a cron schedule"
documentationLink="https://www.windmill.dev/docs/core_concepts/scheduling"
>
-
+ {#if !emptyCtaShown}
+
+ {/if}
diff --git a/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte b/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte
index 1226fc2c0f..e7b7ec2c09 100644
--- a/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/sqs_triggers/+page.svelte
@@ -52,6 +52,7 @@
let triggers: TriggerD[] = $state([])
let shareModal: ShareModal | undefined = $state()
let loading = $state(true)
+ let emptyCtaShown = $derived(!loading && !triggers?.length)
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
let deployUiSettings: WorkspaceDeployUISettings | undefined = $state(undefined)
@@ -280,14 +281,16 @@
-
+ {#if !emptyCtaShown}
+
+ {/if}
{#if isCloudHosted()}
diff --git a/frontend/src/routes/(root)/(logged)/variables/+page.svelte b/frontend/src/routes/(root)/(logged)/variables/+page.svelte
index f2c330da84..1fe505e84d 100644
--- a/frontend/src/routes/(root)/(logged)/variables/+page.svelte
+++ b/frontend/src/routes/(root)/(logged)/variables/+page.svelte
@@ -242,6 +242,22 @@
let hasActiveFilters = $derived(
Object.values(filters.val).some((v) => v !== undefined && v !== null && v !== '' && v !== false)
)
+ // `tab` goes through a typed parameter: a $derived reading a same-scope $state
+ // narrows it to its initial literal, so comparing the other tab fails to compile.
+ function isEmptyCtaShown(currentTab: 'workspace' | 'contextual'): boolean {
+ if (!showCreateButtons) {
+ return false
+ }
+ if (currentTab === 'contextual') {
+ return (
+ !loading.contextual &&
+ contextualVariables.filter((x) => x.is_custom).length === 0 &&
+ Boolean($userStore?.is_admin || $userStore?.is_super_admin)
+ )
+ }
+ return filteredItems?.length === 0 && !hasActiveFilters
+ }
+ let emptyCtaShown = $derived(isEmptyCtaShown(tab))
let deploymentDrawer: DeployWorkspaceDrawer | undefined = $state()
@@ -295,7 +311,7 @@
tooltip="Save and permission strings to be reused in Scripts and Flows."
documentationLink="https://www.windmill.dev/docs/core_concepts/variables_and_secrets"
>
- {#if showCreateButtons}
+ {#if showCreateButtons && !emptyCtaShown}