Compare commits

...

13 Commits

Author SHA1 Message Date
tristantr
bd1bedf69e nit 2025-12-09 12:38:00 +01:00
tristantr
048368b1f8 Change banner wordings if new tutorial available 2025-12-09 12:37:38 +01:00
tristantr
c92ab37930 Put back banner when new tutorial is available for a user who completed all tutotorials, and never skipped all 2025-12-09 12:10:10 +01:00
tristantr
0bcacc1f10 Create shared Constants for TUTORIAL_DISMISSED_KEY 2025-12-09 11:34:36 +01:00
tristantr
635bb207af Add comments 2025-12-09 11:29:55 +01:00
tristantr
b5029f9a61 Add error handling 2025-12-09 11:27:12 +01:00
tristantr
2033ded4b4 Use derived instead of function for Svelte 5 good practices 2025-12-09 11:24:44 +01:00
tristantr
05099b891a Remove banner if all tutorials marked as completed, by role 2025-12-09 11:09:29 +01:00
tristantr
8ab0ffe2d1 Remove tutorials banner if all tutorials marked as completed 2025-12-09 11:03:31 +01:00
tristantr
3383eaf060 Remove banner if user has skipped all tutorials 2025-12-09 10:50:53 +01:00
tristantr
4be122c6fc Allow new operator so see the tutorial banner 2025-12-09 10:42:34 +01:00
Ruben Fiszel
fb387dbd98 add accel buffering no 2025-12-08 22:55:47 +00:00
Ruben Fiszel
e197e1b064 unsafe set var to rustc latest compat 2025-12-08 19:27:38 +00:00
7 changed files with 128 additions and 28 deletions

View File

@@ -335,7 +335,7 @@ async fn windmill_main() -> anyhow::Result<()> {
update_ca_certificates_if_requested();
if std::env::var("RUST_LOG").is_err() {
std::env::set_var("RUST_LOG", "info")
unsafe { std::env::set_var("RUST_LOG", "info") }
}
if let Err(_e) = rustls::crypto::ring::default_provider().install_default() {

View File

@@ -361,24 +361,32 @@ pub async fn load_otel(db: &DB) {
OTEL_TRACING_ENABLED.store(tracing_enabled, Ordering::Relaxed);
let endpoint = if let Some(endpoint) = o.otel_exporter_otlp_endpoint {
std::env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint.clone());
unsafe {
std::env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", endpoint.clone());
}
Some(endpoint.clone())
} else {
std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT").ok()
};
let headers = if let Some(headers) = o.otel_exporter_otlp_headers {
std::env::set_var("OTEL_EXPORTER_OTLP_HEADERS", headers.clone());
unsafe {
std::env::set_var("OTEL_EXPORTER_OTLP_HEADERS", headers.clone());
}
Some(headers.clone())
} else {
std::env::var("OTEL_EXPORTER_OTLP_HEADERS").ok()
};
if let Some(protocol) = o.otel_exporter_otlp_protocol {
std::env::set_var("OTEL_EXPORTER_OTLP_PROTOCOL", protocol);
unsafe {
std::env::set_var("OTEL_EXPORTER_OTLP_PROTOCOL", protocol);
}
}
if let Some(compression) = o.otel_exporter_otlp_compression {
std::env::set_var("OTEL_EXPORTER_OTLP_COMPRESSION", compression);
unsafe {
std::env::set_var("OTEL_EXPORTER_OTLP_COMPRESSION", compression);
}
}
println!("OTEL settings loaded: tracing ({tracing_enabled}), logs ({logs_enabled}), metrics ({metrics_enabled}), endpoint ({:?}), headers defined: ({})",
endpoint, headers.is_some());

View File

@@ -7371,6 +7371,7 @@ async fn get_job_update_sse(
Ok(Response::builder()
.status(200)
.header("X-Accel-Buffering", "no")
.header("Content-Type", "text/event-stream")
.header("Cache-Control", "no-cache")
.header("Connection", "keep-alive")

View File

@@ -58,12 +58,16 @@ pub fn initialize_tracing(
.as_ref()
.is_ok_and(|x| x == "debug" || x == "info")
{
std::env::set_var(
"RUST_LOG",
&format!("windmill={}", rust_log_env.as_ref().unwrap()),
)
unsafe {
std::env::set_var(
"RUST_LOG",
&format!("windmill={}", rust_log_env.as_ref().unwrap()),
)
}
} else if rust_log_env.as_ref().is_ok_and(|x| x == "sqlxdebug") {
std::env::set_var("RUST_LOG", "windmill=debug,sqlx=debug");
unsafe {
std::env::set_var("RUST_LOG", "windmill=debug,sqlx=debug");
}
};
let default_env_filter = if rust_log_env.is_ok_and(|x| x == "debug" || x == "sqlxdebug") {
@@ -110,14 +114,18 @@ pub fn initialize_tracing(
let stdout_env_filter = if rust_log_stdout_env.is_ok() {
// Temporarily set RUST_LOG to RUST_LOG_STDOUT value to parse it
let original_rust_log = std::env::var("RUST_LOG").ok();
std::env::set_var("RUST_LOG", rust_log_stdout_env.unwrap());
unsafe {
std::env::set_var("RUST_LOG", rust_log_stdout_env.unwrap());
}
let filter = EnvFilter::builder()
.with_default_directive(tracing::level_filters::LevelFilter::ERROR.into())
.from_env_lossy();
// Restore original RUST_LOG
match original_rust_log {
Some(val) => std::env::set_var("RUST_LOG", val),
None => std::env::remove_var("RUST_LOG"),
unsafe {
// Restore original RUST_LOG
match original_rust_log {
Some(val) => std::env::set_var("RUST_LOG", val),
None => std::env::remove_var("RUST_LOG"),
}
}
filter
} else {

View File

@@ -5,26 +5,95 @@
import { goto } from '$app/navigation'
import { sendUserToast, type ToastAction } from '$lib/toast'
import { getLocalSetting, storeLocalSetting } from '$lib/utils'
import { skipAllTodos, syncTutorialsTodos } from '$lib/tutorialUtils'
import {
skipAllTodos,
syncTutorialsTodos,
TUTORIAL_BANNER_DISMISSED_KEY
} from '$lib/tutorialUtils'
import { tutorialsToDo, userStore, skippedAll } from '$lib/stores'
import { TUTORIALS_CONFIG } from '$lib/tutorials/config'
import { hasRoleAccess } from '$lib/tutorials/roleUtils'
import { onMount } from 'svelte'
const DISMISSED_KEY = 'tutorial_banner_dismissed'
let isDismissed = $state(false)
let hasCompletedAny = $state(false)
onMount(() => {
// Check if banner has been dismissed
isDismissed = getLocalSetting(DISMISSED_KEY) === 'true'
/**
* Get all tutorial indexes that are accessible to the current user based on their role.
* Automatically recomputes when $userStore changes.
*/
const accessibleTutorialIndexes = $derived.by(() => {
const indexes = new Set<number>()
const user = $userStore
for (const tab of Object.values(TUTORIALS_CONFIG)) {
// Check if user has access to this tab category
if (!hasRoleAccess(user, tab.roles)) {
continue
}
for (const tutorial of tab.tutorials) {
// Check if tutorial has an index and user has access to it
if (tutorial.index !== undefined && hasRoleAccess(user, tutorial.roles)) {
indexes.add(tutorial.index)
}
}
}
return indexes
})
onMount(async () => {
try {
// Sync tutorial progress from backend first
await syncTutorialsTodos()
// Check if banner has been manually dismissed via X button (soft dismiss, per-device)
const manuallyDismissed = getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true'
if (manuallyDismissed) {
isDismissed = true
return
}
// Check if user deliberately skipped all tutorials (permanent dismiss, from backend)
if ($skippedAll) {
isDismissed = true
return
}
// Safe to check tutorialsToDo here since we awaited syncTutorialsTodos() above
// Filter tutorialsToDo to only include tutorials accessible to the user's role
const remainingAccessibleTutorials = $tutorialsToDo.filter((index) =>
accessibleTutorialIndexes.has(index)
)
// Calculate if user has completed at least one tutorial (for banner wording)
hasCompletedAny = remainingAccessibleTutorials.length < accessibleTutorialIndexes.size
// Hide banner if all accessible tutorials are completed (but can reappear with new tutorials)
if (remainingAccessibleTutorials.length === 0) {
isDismissed = true
return
}
// Show banner - user has accessible tutorials to complete
isDismissed = false
} catch (error) {
console.error('Failed to sync tutorial progress:', error)
// Fallback to manual dismissal check only if API call fails
isDismissed = getLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY) === 'true'
}
})
async function handleSkipAllTutorials() {
// Skip all tutorials and set skipped_all flag in backend (permanent)
await skipAllTodos()
await syncTutorialsTodos()
storeLocalSetting(DISMISSED_KEY, 'true')
// No need to set localStorage - backend skipped_all flag is the source of truth
isDismissed = true
}
function dismissBanner() {
storeLocalSetting(DISMISSED_KEY, 'true')
storeLocalSetting(TUTORIAL_BANNER_DISMISSED_KEY, 'true')
isDismissed = true
const actions: ToastAction[] = [
@@ -57,16 +126,24 @@
<GraduationCap size={20} class="text-accent-primary flex-shrink-0" />
<div class="flex-1 min-w-0">
<div class="text-emphasis flex-wrap text-left text-xs font-semibold">
Learn with interactive tutorials
{#if hasCompletedAny}
New tutorial available!
{:else}
Learn with interactive tutorials
{/if}
</div>
<div class="text-hint text-3xs truncate text-left font-normal">
Get started quickly with step-by-step guides on building flows, scripts, and more.
{#if hasCompletedAny}
Continue your learning journey and master new Windmill skills.
{:else}
Get started quickly with step-by-step guides on building flows, scripts, and more.
{/if}
</div>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<Button size="xs" variant="accent" onclick={goToTutorials} startIcon={{ icon: GraduationCap }}>
View tutorials
View tutorials
</Button>
<button
onclick={dismissBanner}

View File

@@ -3,6 +3,12 @@ import { tutorialsToDo, skippedAll } from './stores'
import { UserService } from './gen'
import { TUTORIALS_CONFIG } from './tutorials/config'
/**
* LocalStorage key for tracking if the tutorial banner has been dismissed.
* Shared between tutorialUtils and TutorialBanner component.
*/
export const TUTORIAL_BANNER_DISMISSED_KEY: string = 'tutorial_banner_dismissed'
/**
* Get the maximum tutorial index from the config.
* This ensures we don't hardcode the max ID and it automatically updates when tutorials are added.
@@ -64,6 +70,8 @@ export async function skipAllTodos() {
}
tutorialsToDo.set([])
skippedAll.set(true)
// Set skipped_all flag in backend - this is the permanent source of truth
await UserService.updateTutorialProgress({ requestBody: { progress: bits, skipped_all: true } })
}

View File

@@ -274,9 +274,7 @@
{/if}
</PageHeader>
{#if !$userStore?.operator}
<TutorialBanner />
{/if}
<TutorialBanner />
{#if !$userStore?.operator}
<div class="w-full overflow-auto scrollbar-hidden pb-2">