mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-23 16:00:38 +00:00
feat: display last key renewal attempt (#3839)
* feat: display last key renewal attempt * feat: improve UI + add renew button + dev instance * fix: nits * chore: update ee ref * fix: nit * fix: tests
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, created_at FROM metrics WHERE id = $1 ORDER BY created_at DESC LIMIT 1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "created_at",
|
||||
"type_info": "Timestamptz"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "360d13c0a776e794063c99acad935a424f79017f3769e1150faa87cbb91367a8"
|
||||
}
|
||||
Generated
+3
-2
@@ -9020,9 +9020,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.12"
|
||||
version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b2cb4fbb9995eeb36ac86fadf24031ccd58f99d6b4b2d7b911db70bddb80d90"
|
||||
checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
@@ -10008,6 +10008,7 @@ dependencies = [
|
||||
"swc_ecma_ast",
|
||||
"swc_ecma_parser",
|
||||
"swc_ecma_visit",
|
||||
"triomphe",
|
||||
"wasm-bindgen",
|
||||
"windmill-parser",
|
||||
]
|
||||
|
||||
@@ -254,3 +254,6 @@ sysinfo = "0.30.12"
|
||||
tikv-jemallocator = { version = "0.5" }
|
||||
tikv-jemalloc-sys = { version = "^0.5" }
|
||||
tikv-jemalloc-ctl = { version = "^0.5" }
|
||||
|
||||
# 0.1.12 broken (nested dependency of swc_common)
|
||||
triomphe = "<0.1.12"
|
||||
|
||||
@@ -1 +1 @@
|
||||
553aa1d75498c9c6c07a1495a154478fb0bc064e
|
||||
982774ea62ab95914bd0502aac8c5c9b5a91b5c2
|
||||
@@ -16,6 +16,7 @@ serde-wasm-bindgen.workspace = true
|
||||
[dependencies]
|
||||
windmill-parser.workspace = true
|
||||
swc_common.workspace = true
|
||||
triomphe.workspace = true
|
||||
swc_ecma_parser.workspace = true
|
||||
swc_ecma_ast.workspace = true
|
||||
swc_ecma_visit.workspace = true
|
||||
|
||||
@@ -766,6 +766,45 @@ paths:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
|
||||
/settings/latest_key_renewal_attempt:
|
||||
get:
|
||||
summary: get latest key renewal attempt
|
||||
operationId: getLatestKeyRenewalAttempt
|
||||
tags:
|
||||
- setting
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
result:
|
||||
type: string
|
||||
attempted_at:
|
||||
type: string
|
||||
format: date-time
|
||||
required:
|
||||
- result
|
||||
- attempted_at
|
||||
nullable: true
|
||||
|
||||
/settings/renew_license_key:
|
||||
post:
|
||||
summary: renew license key
|
||||
operationId: renewLicenseKey
|
||||
tags:
|
||||
- setting
|
||||
responses:
|
||||
"200":
|
||||
description: status
|
||||
content:
|
||||
text/plain:
|
||||
schema:
|
||||
type: string
|
||||
|
||||
/saml/test_metadata:
|
||||
post:
|
||||
summary: test metadata
|
||||
|
||||
@@ -42,7 +42,12 @@ pub fn global_service() -> Router {
|
||||
)
|
||||
.route("/test_smtp", post(test_email))
|
||||
.route("/test_license_key", post(test_license_key))
|
||||
.route("/send_stats", post(send_stats));
|
||||
.route("/send_stats", post(send_stats))
|
||||
.route(
|
||||
"/latest_key_renewal_attempt",
|
||||
get(get_latest_key_renewal_attempt),
|
||||
)
|
||||
.route("/renew_license_key", post(renew_license_key));
|
||||
|
||||
#[cfg(feature = "parquet")]
|
||||
{
|
||||
@@ -263,3 +268,60 @@ pub async fn send_stats(Extension(db): Extension<DB>, authed: ApiAuthed) -> Resu
|
||||
|
||||
Ok("Sent stats".to_string())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct KeyRenewalAttempt {
|
||||
result: String,
|
||||
attempted_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
pub async fn get_latest_key_renewal_attempt(
|
||||
Extension(db): Extension<DB>,
|
||||
authed: ApiAuthed,
|
||||
) -> JsonResult<Option<KeyRenewalAttempt>> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
|
||||
let last_attempt = sqlx::query!(
|
||||
"SELECT value, created_at FROM metrics WHERE id = $1 ORDER BY created_at DESC LIMIT 1",
|
||||
"license_key_renewal"
|
||||
)
|
||||
.fetch_optional(&db)
|
||||
.await?;
|
||||
|
||||
match last_attempt {
|
||||
Some(last_attempt) => {
|
||||
let last_attempt_result = serde_json::from_value::<String>(last_attempt.value)
|
||||
.map_err(|e| {
|
||||
error::Error::InternalErr(format!("Failed to parse last attempt: {}", e))
|
||||
})?;
|
||||
Ok(Json(Some(KeyRenewalAttempt {
|
||||
result: last_attempt_result,
|
||||
attempted_at: last_attempt.created_at,
|
||||
})))
|
||||
}
|
||||
None => Ok(Json(None)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "enterprise"))]
|
||||
pub async fn renew_license_key() -> Result<String> {
|
||||
return Err(error::Error::BadRequest(
|
||||
"License key renewal not available on community edition".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn renew_license_key(Extension(db): Extension<DB>, authed: ApiAuthed) -> Result<String> {
|
||||
require_super_admin(&db, &authed.email).await?;
|
||||
windmill_common::stats_ee::send_stats(&"manual".to_string(), &HTTP_CLIENT, &db).await?;
|
||||
let result = windmill_common::ee::renew_license_key(&HTTP_CLIENT, &db).await;
|
||||
|
||||
if result != "success" {
|
||||
return Err(error::Error::BadRequest(format!(
|
||||
"Failed to renew license key: {}",
|
||||
result
|
||||
)));
|
||||
} else {
|
||||
return Ok("Renewed license key".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,12 @@ pub enum CriticalErrorChannel {}
|
||||
|
||||
pub async fn trigger_critical_error_channels(_error_message: String) {}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn renew_license_key(_http_client: &reqwest::Client, _db: &crate::db::DB) -> String {
|
||||
// Implementation is not open source
|
||||
"".to_string()
|
||||
}
|
||||
|
||||
#[cfg(feature = "enterprise")]
|
||||
pub async fn schedule_key_renewal(_http_client: &reqwest::Client, _db: &crate::db::DB) -> () {
|
||||
// Implementation is not open source
|
||||
|
||||
@@ -25,6 +25,7 @@ pub const OBJECT_STORE_CACHE_CONFIG_SETTING: &str = "object_store_cache_config";
|
||||
pub const AUTOMATE_USERNAME_CREATION_SETTING: &str = "automate_username_creation";
|
||||
pub const HUB_BASE_URL_SETTING: &str = "hub_base_url";
|
||||
pub const CRITICAL_ERROR_CHANNELS_SETTING: &str = "critical_error_channels";
|
||||
pub const DEV_INSTANCE_SETTING: &str = "dev_instance";
|
||||
|
||||
pub const ENV_SETTINGS: [&str; 50] = [
|
||||
"DISABLE_NSJAIL",
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
import KeycloakSetting from './KeycloakSetting.svelte'
|
||||
import Alert from './common/alert/Alert.svelte'
|
||||
import { isCloudHosted } from '$lib/cloud'
|
||||
import { capitalize } from '$lib/utils'
|
||||
import { capitalize, classNames } from '$lib/utils'
|
||||
import { enterpriseLicense } from '$lib/stores'
|
||||
import CustomOauth from './CustomOauth.svelte'
|
||||
import { AlertTriangle, Plus, X } from 'lucide-svelte'
|
||||
import { AlertCircle, AlertTriangle, BadgeCheck, Info, Plus, X, BadgeX } from 'lucide-svelte'
|
||||
import CustomSso from './CustomSso.svelte'
|
||||
import AuthentikSetting from '$lib/components/AuthentikSetting.svelte'
|
||||
import AutheliaSetting from '$lib/components/AutheliaSetting.svelte'
|
||||
@@ -25,6 +25,7 @@
|
||||
import Password from './Password.svelte'
|
||||
import ObjectStoreConfigSettings from './ObjectStoreConfigSettings.svelte'
|
||||
import { fade } from 'svelte/transition'
|
||||
import Popover from './Popover.svelte'
|
||||
|
||||
export let tab: string = 'Core'
|
||||
export let hideTabs: boolean = false
|
||||
@@ -35,6 +36,10 @@
|
||||
let initialRequirePreexistingUserForOauth: boolean = false
|
||||
let requirePreexistingUserForOauth: boolean = false
|
||||
let ssoOrOauth: 'sso' | 'oauth' = 'sso'
|
||||
let latestKeyRenewalAttempt: {
|
||||
result: string
|
||||
attempted_at: string
|
||||
} | null = null
|
||||
|
||||
let serverConfig = {}
|
||||
let initialValues: Record<string, any> = {}
|
||||
@@ -79,6 +84,7 @@
|
||||
if (values['base_url'] == undefined) {
|
||||
values['base_url'] = 'http://localhost'
|
||||
}
|
||||
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
|
||||
}
|
||||
|
||||
export async function saveSettings() {
|
||||
@@ -144,7 +150,7 @@
|
||||
try {
|
||||
let i = parseInt(splitted[1])
|
||||
let date = new Date(i * 1000)
|
||||
return date.toDateString()
|
||||
return date.toLocaleDateString()
|
||||
} catch {}
|
||||
}
|
||||
return undefined
|
||||
@@ -180,6 +186,17 @@
|
||||
let clientName = ''
|
||||
|
||||
let licenseKeyChanged = false
|
||||
|
||||
export async function renewLicenseKey() {
|
||||
try {
|
||||
await SettingService.renewLicenseKey()
|
||||
sendUserToast('Key renewal successful')
|
||||
loadSettings()
|
||||
} catch (err) {
|
||||
latestKeyRenewalAttempt = await SettingService.getLatestKeyRenewalAttempt()
|
||||
throw err
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pb-8">
|
||||
@@ -477,22 +494,81 @@
|
||||
requestBody: { license_key: values[setting.key] }
|
||||
})
|
||||
sendUserToast('Valid key')
|
||||
}}>Test Key</Button
|
||||
}}
|
||||
>
|
||||
</div>
|
||||
{#if values[setting.key]?.length > 0}
|
||||
{#if parseDate(values[setting.key])}
|
||||
<span class="text-tertiary text-2xs"
|
||||
>License key expires on {parseDate(values[setting.key])}</span
|
||||
>
|
||||
Test Key
|
||||
</Button>
|
||||
{#if $enterpriseLicense}
|
||||
<Button on:click={renewLicenseKey} size="xs">Renew key</Button>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if licenseKeyChanged}
|
||||
<div class="text-yellow-600"
|
||||
>Refresh page after setting license key and saving to unlock all
|
||||
features</div
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="mt-1 flex flex-col gap-1 items-start">
|
||||
{#if values[setting.key]?.length > 0}
|
||||
{#if parseDate(values[setting.key])}
|
||||
<div class="flex flex-row gap-1 items-center">
|
||||
<Info size={12} class="text-tertiary" />
|
||||
<span class="text-tertiary text-xs"
|
||||
>License key expires on {parseDate(values[setting.key])}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if latestKeyRenewalAttempt}
|
||||
{@const attemptedAt = new Date(
|
||||
latestKeyRenewalAttempt.attempted_at
|
||||
).toLocaleString()}
|
||||
<div class="relative">
|
||||
<Popover notClickable>
|
||||
<div class="flex flex-row items-center gap-1">
|
||||
{#if latestKeyRenewalAttempt.result === 'success'}
|
||||
<BadgeCheck class="text-green-600" size={12} />
|
||||
{:else}
|
||||
<BadgeX class="text-red-600" size={12} />
|
||||
{/if}
|
||||
<span
|
||||
class={classNames(
|
||||
'text-xs',
|
||||
latestKeyRenewalAttempt.result === 'success'
|
||||
? 'text-green-600'
|
||||
: 'text-red-600'
|
||||
)}
|
||||
>
|
||||
{latestKeyRenewalAttempt.result === 'success'
|
||||
? 'Latest key renewal succeeded'
|
||||
: 'Latest key renewal failed'}
|
||||
on {attemptedAt}
|
||||
</span>
|
||||
</div>
|
||||
<div slot="text">
|
||||
{#if latestKeyRenewalAttempt.result === 'success'}
|
||||
<span class="text-green-300">
|
||||
Latest key renewal succeeded on {attemptedAt}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="text-red-300">
|
||||
Latest key renewal failed on {attemptedAt}: {latestKeyRenewalAttempt.result.replace(
|
||||
'error: ',
|
||||
''
|
||||
)}
|
||||
</span>
|
||||
{/if}
|
||||
<br />
|
||||
As long as invoices are paid and usage corresponds to the subscription,
|
||||
the key is renewed daily with a validity of 35 days (grace period).
|
||||
</div>
|
||||
</Popover>
|
||||
</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}
|
||||
</div>
|
||||
{:else if setting.fieldType == 'email'}
|
||||
<input
|
||||
type="email"
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
id="password"
|
||||
type="password"
|
||||
bind:value={password}
|
||||
on:keydown
|
||||
autocomplete="off"
|
||||
{placeholder}
|
||||
{disabled}
|
||||
|
||||
@@ -77,6 +77,14 @@ export const settings: Record<string, Setting[]> = {
|
||||
placeholder: 'only needed to prepare upgrade to EE',
|
||||
storage: 'setting'
|
||||
},
|
||||
{
|
||||
label: 'Non-prod instance',
|
||||
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'
|
||||
},
|
||||
{
|
||||
label: 'Retention Period in secs',
|
||||
key: 'retention_period_secs',
|
||||
|
||||
Reference in New Issue
Block a user