Compare commits
13 Commits
v1.590.0
...
tl/tutoria
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bd1bedf69e | ||
|
|
048368b1f8 | ||
|
|
c92ab37930 | ||
|
|
0bcacc1f10 | ||
|
|
635bb207af | ||
|
|
b5029f9a61 | ||
|
|
2033ded4b4 | ||
|
|
05099b891a | ||
|
|
8ab0ffe2d1 | ||
|
|
3383eaf060 | ||
|
|
4be122c6fc | ||
|
|
fb387dbd98 | ||
|
|
e197e1b064 |
@@ -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() {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 } })
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user