Skip to content

Commit

Permalink
fix: improve first time setup experience
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenfiszel committed Sep 15, 2024
1 parent 36fc048 commit 2d86dc0
Show file tree
Hide file tree
Showing 9 changed files with 135 additions and 41 deletions.
1 change: 1 addition & 0 deletions backend/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,7 @@ async fn windmill_main() -> anyhow::Result<()> {
// migration code to avoid break
windmill_api::migrate_db(&db).await?;
}

let (killpill_tx, mut killpill_rx) = tokio::sync::broadcast::channel::<()>(2);
let mut monitor_killpill_rx = killpill_tx.subscribe();
let server_killpill_rx = killpill_tx.subscribe();
Expand Down
1 change: 1 addition & 0 deletions backend/windmill-api/src/static_assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ fn serve_path(path: &str) -> Response<Body> {
if path.starts_with("api/") {
return Response::builder().status(404).body(Body::empty()).unwrap();
}

match Asset::get(path) {
Some(content) => {
let body = Body::from(content.data);
Expand Down
49 changes: 46 additions & 3 deletions backend/windmill-api/src/users.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

#![allow(non_snake_case)]

use std::sync::atomic::{AtomicI64, AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
use std::sync::Arc;

use crate::db::ApiAuthed;
Expand Down Expand Up @@ -111,6 +111,7 @@ pub fn global_service() -> Router {
.route("/leave_instance", post(leave_instance))
.route("/export", get(export_global_users))
.route("/overwrite", post(overwrite_global_users))

// .route("/list_invite_codes", get(list_invite_codes))
// .route("/create_invite_code", post(create_invite_code))
// .route("/signup", post(signup))
Expand All @@ -121,8 +122,8 @@ pub fn global_service() -> Router {
pub fn make_unauthed_service() -> Router {
Router::new()
.route("/login", post(login))
.route("/logout", post(logout))
.route("/logout", get(logout))
.route("/logout", post(logout).get(logout))
.route("/is_first_time_setup", get(is_first_time_setup))
}

fn username_override_from_label(label: Option<String>) -> Option<String> {
Expand Down Expand Up @@ -826,6 +827,48 @@ pub struct Login {
pub password: String,
}

lazy_static::lazy_static! {
static ref FIRST_TIME_SETUP: Arc<AtomicBool> = Arc::new(AtomicBool::new(true));
}

pub async fn is_first_time_setup(Extension(db): Extension<DB>) -> JsonResult<bool> {
if !FIRST_TIME_SETUP.load(std::sync::atomic::Ordering::Relaxed) {
return Ok(Json(false));
}
let single_user = sqlx::query_scalar!("SELECT 1 FROM password LIMIT 2")
.fetch_all(&db)
.await
.ok()
.unwrap_or_default()
.len()
== 1;
if single_user {
let user_is_admin_and_password_changeme = sqlx::query_scalar!(
"SELECT 1 FROM password WHERE email = '[email protected]' AND password_hash = '$argon2id$v=19$m=4096,t=3,p=1$oLJo/lPn/gezXCuFOEyaNw$i0T2tCkw3xUFsrBIKZwr8jVNHlIfoxQe+HfDnLtd12I'"
).fetch_all(&db)
.await
.ok()
.unwrap_or_default()
.len() == 1;
if user_is_admin_and_password_changeme {
let base_url_is_not_set =
sqlx::query_scalar!("SELECT COUNT(*) FROM global_settings WHERE name = 'base_url'")
.fetch_optional(&db)
.await
.ok()
.flatten()
.flatten()
.unwrap_or(0)
== 0;
if base_url_is_not_set {
return Ok(Json(true));
}
}
}
FIRST_TIME_SETUP.store(false, std::sync::atomic::Ordering::Relaxed);
Ok(Json(false))
}

#[derive(Deserialize)]
struct WorkspaceUsername {
pub username: String,
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/lib/components/CenteredModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
export let subtitle: string | undefined = undefined
export let title = 'Windmill'
export let disableLogo = false
export let large = false
setLicense()
</script>
Expand All @@ -23,7 +24,9 @@
{/if}

<div
class="border rounded-md shadow-md bg-surface w-full max-w-[640px] p-4 sm:py-8 sm:px-10 mb-6 md:mb-20 z-10"
class="border rounded-md shadow-md bg-surface w-full {large
? 'max-w-5xl'
: 'max-w-[640px]'} p-4 sm:py-8 sm:px-10 mb-6 md:mb-20 z-10"
>
<div class="mb-10">
<h1 class="text-center text-primary">
Expand Down
74 changes: 57 additions & 17 deletions frontend/src/lib/components/InstanceSettings.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import { settings, settingsKeys, type SettingStorage } from './instanceSettings'
import { Button, Tab, TabContent, Tabs } from '$lib/components/common'
import { ConfigService, SettingService } from '$lib/gen'
import { Button, Skeleton, Tab, TabContent, Tabs } from '$lib/components/common'
import { ConfigService, SettingService, SettingsService } from '$lib/gen'
import Toggle from '$lib/components/Toggle.svelte'
import SecondsInput from '$lib/components/common/seconds/SecondsInput.svelte'
import Tooltip from '$lib/components/Tooltip.svelte'
Expand Down Expand Up @@ -37,6 +37,8 @@
import Popover from './Popover.svelte'
import { base } from '$lib/base'
import { createEventDispatcher } from 'svelte'
import { setLicense } from '$lib/enterpriseUtils'
export let tab: string = 'Core'
export let hideTabs: boolean = false
Expand All @@ -54,8 +56,21 @@
let serverConfig = {}
let initialValues: Record<string, any> = {}
let loading = true
let version: string = ''
loadSettings()
loadVersion()
const dispatch = createEventDispatcher()
async function loadVersion() {
version = await SettingsService.backendVersion()
}
async function loadSettings() {
loading = true
try {
serverConfig = (await ConfigService.getConfig({ name: 'server' })) ?? {}
} catch (e) {
Expand Down Expand Up @@ -95,6 +110,8 @@
if (values['base_url'] == undefined) {
values['base_url'] = 'http://localhost'
}
loading = false
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
}
Expand All @@ -113,6 +130,7 @@
})
serverConfig = JSON.parse(JSON.stringify(newServerConfig))
}
let licenseKeySet = false
await Promise.all(
allSettings
.filter((x) => {
Expand All @@ -125,7 +143,13 @@
)
})
.map(async ([_, x]) => {
await SettingService.setGlobal({ key: x.key, requestBody: { value: values?.[x.key] } })
if (x.key == 'license_key') {
licenseKeySet = true
}
return await SettingService.setGlobal({
key: x.key,
requestBody: { value: values?.[x.key] }
})
})
)
initialValues = JSON.parse(JSON.stringify(values))
Expand All @@ -145,10 +169,14 @@
requestBody: { value: requirePreexistingUserForOauth }
})
}
if (licenseKeySet) {
setLicense()
}
} else {
console.error('Values not loaded')
}
sendUserToast('Settings updated')
dispatch('saved')
}
let oauths: Record<string, any> = {}
Expand Down Expand Up @@ -238,6 +266,15 @@
opening = false
}
}
function showSetting(setting: string, values: Record<string, any>) {
if (setting == 'dev_instance') {
if (values['license_key'] == undefined) {
return false
}
}
return true
}
</script>

<div class="pb-8">
Expand All @@ -249,6 +286,7 @@

<svelte:fragment slot="content">
<div class="pt-4" />

{#each Object.keys(settings) as category}
<TabContent value={category}>
{#if category == 'SMTP'}
Expand Down Expand Up @@ -285,8 +323,7 @@
size="xs">Send usage</Button
>
{/if}
{/if}
{#if category == 'SSO/OAuth'}
{:else if category == 'SSO/OAuth'}
<div>
<Tabs bind:selected={ssoOrOauth} class="mt-2 mb-4">
<Tab value="sso">SSO</Tab>
Expand Down Expand Up @@ -477,7 +514,7 @@
<div>
<div class="flex-col flex gap-2 pb-4">
{#each settings[category] as setting}
{#if !setting.cloudonly || isCloudHosted()}
{#if (!setting.cloudonly || isCloudHosted()) && showSetting(setting.key, values)}
{#if setting.ee_only != undefined && !$enterpriseLicense}
<div class="flex text-xs items-center gap-1 text-yellow-500 whitespace-nowrap">
<AlertTriangle size={16} />
Expand All @@ -494,7 +531,9 @@
{/if}
{#if values}
{@const hasError = setting.isValid && !setting.isValid(values[setting.key])}
{#if setting.fieldType == 'text'}
{#if loading}
<Skeleton layout={[[2.5]]} />
{:else if setting.fieldType == 'text'}
<input
disabled={setting.ee_only != undefined && !$enterpriseLicense}
type="text"
Expand Down Expand Up @@ -559,14 +598,16 @@
{:else if expiration}
<div class="flex flex-row gap-1 items-center">
<AlertCircle size={12} class="text-red-600" />
<span class="text-red-600 text-xs"
<span class="text-red-600 dark:text-red-400 text-xs"
>License key expired on {expiration}</span
>
</div>
{:else}
<div class="flex flex-row gap-1 items-center">
<AlertCircle size={12} class="text-red-600" />
<span class="text-red-600 text-xs">Invalid license key format</span>
<span class="text-red-600 dark:text-red-400 text-xs"
>Invalid license key format</span
>
</div>
{/if}
{/if}
Expand Down Expand Up @@ -617,13 +658,12 @@
</div>
{/if}
{#if licenseKeyChanged && !$enterpriseLicense}
<div class="flex flex-row items-center gap-1">
<AlertCircle size={12} class="text-yellow-600" />
<span class="text-xs text-yellow-600">
Refresh page after setting and saving license key to unlock all
features
</span>
</div>
{#if version.startsWith('CE')}
<div class="text-red-400"
>License key is set but image used is the Community Edition {version}.
Switch image to EE.</div
>
{/if}
{/if}

{#if valid || expiration}
Expand Down Expand Up @@ -810,7 +850,7 @@
{/if}

{#if hasError}
<span class="text-red-500 text-xs">
<span class="text-red-500 dark:text-red-400 text-sm">
{setting.error ?? ''}
</span>
{/if}
Expand Down
19 changes: 12 additions & 7 deletions frontend/src/lib/components/Login.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
export let password: string | undefined = undefined
export let error: string | undefined = undefined
export let popup: boolean = false
export let firstTime: boolean = false
const providers = [
{
Expand Down Expand Up @@ -75,6 +76,11 @@
return
}
if (firstTime) {
goto('/user/first-time')
return
}
// Once logged in, we can fetch the workspaces
$usersWorkspaceStore = await WorkspaceService.listUserWorkspaces()
// trigger a reload of the user
Expand All @@ -88,13 +94,6 @@
}
async function redirectUser() {
const firstTimeCookie =
document.cookie.match('(^|;)\\s*first_time\\s*=\\s*([^;]+)')?.pop() || '0'
if (Number(firstTimeCookie) > 0 && email === '[email protected]') {
goto('/user/first-time')
return
}
if (rd?.startsWith('http')) {
window.location.href = rd
return
Expand Down Expand Up @@ -177,6 +176,7 @@
dispatch('login')
}
}
function storeRedirect(provider: string) {
if (rd) {
try {
Expand Down Expand Up @@ -263,6 +263,11 @@

{#if showPassword}
<div>
{#if firstTime}
<div class="text-lg text-center w-full pb-6"
>First time login: [email protected] / changeme</div
>
{/if}
<div class="space-y-6">
{#if isCloudHosted()}
<p class="text-xs text-tertiary italic pb-6">
Expand Down
3 changes: 1 addition & 2 deletions frontend/src/lib/components/instanceSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,7 @@ export const settings: Record<string, Setting[]> = {
description: 'Whether we should consider the reported usage of this instance as non-prod',
key: 'dev_instance',
fieldType: 'boolean',
storage: 'setting',
ee_only: 'This is only relevant for EE'
storage: 'setting'
},
{
label: 'Retention period in secs',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,20 @@
import CenteredModal from '$lib/components/CenteredModal.svelte'
import InstanceSettings from '$lib/components/InstanceSettings.svelte'
import { Button } from '$lib/components/common'
let saved = false
</script>

<CenteredModal title="Instance Settings">
<InstanceSettings />
<CenteredModal large title="Instance Settings">
<InstanceSettings on:saved={() => (saved = true)} />
<p class="text-secondary text-sm px-2 py-4">
You can change these settings later in the instance settings but finishing setup will leave this
page.
</p>
<Button
disabled={!saved}
on:click={() => {
goto('/apps/get/g/all/setup_app?nomenubar=true')
}}>Finish Setup</Button
}}>Finish Setup {!saved ? '(save settings at least once)' : ''}</Button
>
</CenteredModal>
Loading

0 comments on commit 2d86dc0

Please sign in to comment.