diff --git a/backend/.sqlx/query-04362a55081f7a98bca8fe4db0669939da8944711037957664cc2989b239c9d1.json b/backend/.sqlx/query-04362a55081f7a98bca8fe4db0669939da8944711037957664cc2989b239c9d1.json new file mode 100644 index 0000000000000..0a2f244c1127c --- /dev/null +++ b/backend/.sqlx/query-04362a55081f7a98bca8fe4db0669939da8944711037957664cc2989b239c9d1.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO tutorial_progress (email, progress, skipped_all) VALUES ($2, $1::bigint::bit(64), $3) ON CONFLICT (email) DO UPDATE SET progress = EXCLUDED.progress, skipped_all = EXCLUDED.skipped_all", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Int8", + "Varchar", + "Bool" + ] + }, + "nullable": [] + }, + "hash": "04362a55081f7a98bca8fe4db0669939da8944711037957664cc2989b239c9d1" +} diff --git a/backend/.sqlx/query-f3b7ea04830022f918f9302b4d40f57c9c9c48192bc2293986c8abb54ae94446.json b/backend/.sqlx/query-f3b7ea04830022f918f9302b4d40f57c9c9c48192bc2293986c8abb54ae94446.json new file mode 100644 index 0000000000000..d50fb8f6f2330 --- /dev/null +++ b/backend/.sqlx/query-f3b7ea04830022f918f9302b4d40f57c9c9c48192bc2293986c8abb54ae94446.json @@ -0,0 +1,28 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT progress::bigint as progress, skipped_all FROM tutorial_progress WHERE email = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "progress", + "type_info": "Int8" + }, + { + "ordinal": 1, + "name": "skipped_all", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + null, + false + ] + }, + "hash": "f3b7ea04830022f918f9302b4d40f57c9c9c48192bc2293986c8abb54ae94446" +} diff --git a/backend/migrations/20251203131741_add_skipped_all_to_tutorial_progress.down.sql b/backend/migrations/20251203131741_add_skipped_all_to_tutorial_progress.down.sql new file mode 100644 index 0000000000000..fb6b0b2ffea0a --- /dev/null +++ b/backend/migrations/20251203131741_add_skipped_all_to_tutorial_progress.down.sql @@ -0,0 +1,3 @@ +-- Remove skipped_all column from tutorial_progress table +ALTER TABLE tutorial_progress +DROP COLUMN skipped_all; diff --git a/backend/migrations/20251203131741_add_skipped_all_to_tutorial_progress.up.sql b/backend/migrations/20251203131741_add_skipped_all_to_tutorial_progress.up.sql new file mode 100644 index 0000000000000..55a26221b3446 --- /dev/null +++ b/backend/migrations/20251203131741_add_skipped_all_to_tutorial_progress.up.sql @@ -0,0 +1,5 @@ +-- Add skipped_all column to tutorial_progress table +ALTER TABLE tutorial_progress +ADD COLUMN skipped_all BOOLEAN NOT NULL DEFAULT FALSE; + +COMMENT ON COLUMN tutorial_progress.skipped_all IS 'Indicates if the user has skipped all tutorials (vs completing them all)'; diff --git a/backend/windmill-api/openapi.yaml b/backend/windmill-api/openapi.yaml index 44bdee8d115f1..234e388fabc69 100644 --- a/backend/windmill-api/openapi.yaml +++ b/backend/windmill-api/openapi.yaml @@ -1335,6 +1335,8 @@ paths: properties: progress: type: integer + skipped_all: + type: boolean post: summary: update tutorial progress operationId: updateTutorialProgress @@ -1350,6 +1352,8 @@ paths: properties: progress: type: integer + skipped_all: + type: boolean responses: "200": description: tutorial progress diff --git a/backend/windmill-api/src/users.rs b/backend/windmill-api/src/users.rs index 9021d6fe8e151..7ed3fe13f925e 100644 --- a/backend/windmill-api/src/users.rs +++ b/backend/windmill-api/src/users.rs @@ -562,20 +562,30 @@ async fn list_users_as_super_admin( #[derive(Serialize, Deserialize)] struct Progress { progress: u64, + skipped_all: bool, } async fn get_tutorial_progress( authed: ApiAuthed, Extension(db): Extension, ) -> JsonResult { - let res = sqlx::query_scalar!( - "SELECT progress::bigint FROM tutorial_progress WHERE email = $1", + let row = sqlx::query!( + "SELECT progress::bigint as progress, skipped_all FROM tutorial_progress WHERE email = $1", authed.email ) .fetch_optional(&db) - .await? - .flatten() - .unwrap_or_default() as u64; - Ok(Json(Progress { progress: res })) + .await?; + + if let Some(row) = row { + Ok(Json(Progress { + progress: row.progress.unwrap_or_default() as u64, + skipped_all: row.skipped_all, + })) + } else { + Ok(Json(Progress { + progress: 0, + skipped_all: false, + })) + } } async fn update_tutorial_progress( @@ -583,10 +593,11 @@ async fn update_tutorial_progress( Extension(db): Extension, Json(progress): Json, ) -> Result { - sqlx::query_scalar!( - "INSERT INTO tutorial_progress VALUES ($2, $1::bigint::bit(64)) ON CONFLICT (email) DO UPDATE SET progress = EXCLUDED.progress", + sqlx::query!( + "INSERT INTO tutorial_progress (email, progress, skipped_all) VALUES ($2, $1::bigint::bit(64), $3) ON CONFLICT (email) DO UPDATE SET progress = EXCLUDED.progress, skipped_all = EXCLUDED.skipped_all", progress.progress as i64, - authed.email + authed.email, + progress.skipped_all ) .execute(&db) .await?; diff --git a/frontend/src/lib/components/AppTutorials.svelte b/frontend/src/lib/components/AppTutorials.svelte index f32c613bb33db..25d06e016715f 100644 --- a/frontend/src/lib/components/AppTutorials.svelte +++ b/frontend/src/lib/components/AppTutorials.svelte @@ -1,51 +1,29 @@ - - - - - diff --git a/frontend/src/lib/components/FlowBuilder.svelte b/frontend/src/lib/components/FlowBuilder.svelte index 6367779894fce..f16cd551cc356 100644 --- a/frontend/src/lib/components/FlowBuilder.svelte +++ b/frontend/src/lib/components/FlowBuilder.svelte @@ -13,7 +13,6 @@ import { initHistory, redo, undo } from '$lib/history.svelte' import { enterpriseLicense, - tutorialsToDo, userStore, workspaceStore, usedTriggerKinds @@ -61,11 +60,10 @@ import { getAllModules } from './flows/flowExplorer' import { type FlowCopilotContext } from './copilot/flow' import { loadFlowModuleState } from './flows/flowStateUtils.svelte' - import FlowBuilderTutorials from './FlowBuilderTutorials.svelte' import Dropdown from '$lib/components/DropdownV2.svelte' import FlowTutorials from './FlowTutorials.svelte' - import { ignoredTutorials } from './tutorials/ignoredTutorials' import FlowHistory from './flows/FlowHistory.svelte' + import FlowEditorTutorial from './flows/FlowEditorTutorial.svelte' import Summary from './Summary.svelte' import type { FlowBuilderWhitelabelCustomUi } from './custom_ui' import FlowYamlEditor from './flows/header/FlowYamlEditor.svelte' @@ -805,8 +803,6 @@ if (tutorial) { flowTutorials?.runTutorialById(tutorial) - } else if ($tutorialsToDo.includes(0) && !$ignoredTutorials.includes(0)) { - flowTutorials?.runTutorialById('action') } } @@ -1110,13 +1106,7 @@ {/if} - {#if customUi?.topBar?.tutorials != false} - { - renderCount += 1 - }} - /> - {/if} + {#if customUi?.topBar?.diff != false} + + + +{/if} + diff --git a/frontend/src/lib/components/home/TutorialButton.svelte b/frontend/src/lib/components/home/TutorialButton.svelte new file mode 100644 index 0000000000000..6a31ae9569ba8 --- /dev/null +++ b/frontend/src/lib/components/home/TutorialButton.svelte @@ -0,0 +1,124 @@ + + + + diff --git a/frontend/src/lib/components/scripts/CreateActionsScript.svelte b/frontend/src/lib/components/scripts/CreateActionsScript.svelte index 7d42f4a72b7bb..d77ab9d7acf43 100644 --- a/frontend/src/lib/components/scripts/CreateActionsScript.svelte +++ b/frontend/src/lib/components/scripts/CreateActionsScript.svelte @@ -9,6 +9,7 @@
+ +
+ {/if} + + {#if $userStore?.is_admin} +
+
+ View as an + { + selectedPreviewRole = (v || userEffectiveRole) as Role + }} + noWFull + > + {#snippet children({ item })} + + + + {/snippet} + +
+ + This allows you to see which tutorials your team members can access + +
+ {/if} + + + {#if activeTabs.length > 0} +
+ + {#each activeTabs as [tabId, config]} + {@const badge = getTabBadge(tabId as TabId)} + {#if badge.type === 'progress'} + + {#snippet extra()} + {badge.text} + {/snippet} + + {:else if badge.type === 'check'} + + {#snippet extra()} + + {/snippet} + + {:else if badge.type === 'dot'} + + {#snippet extra()} + + {/snippet} + + {:else} + + {/if} + {/each} + +
+ + {#if tutorials.length > 0} +
+
+ {#if currentTabConfig.progressBar !== false} + + {/if} +
+ + +
+
+ +
+ {#each tutorials as tutorial} + updateSingleTutorial(tutorial.id, false)} + onComplete={() => updateSingleTutorial(tutorial.id, true)} + /> + {/each} +
+
+ {:else if currentTabConfig} +
+
+ No tutorials available for this section yet. +
+
+ {/if} + {:else} +
+
+ No tutorials available for now. Coming soon. +
+
+ {/if} + diff --git a/frontend/static/app.png b/frontend/static/app.png new file mode 100644 index 0000000000000..0cd896135640d Binary files /dev/null and b/frontend/static/app.png differ diff --git a/frontend/static/flow.png b/frontend/static/flow.png new file mode 100644 index 0000000000000..a07e1d0b206d5 Binary files /dev/null and b/frontend/static/flow.png differ diff --git a/frontend/static/languages.png b/frontend/static/languages.png new file mode 100644 index 0000000000000..62987306c22c5 Binary files /dev/null and b/frontend/static/languages.png differ