feat: add npm_config_registry support for bun, deno and being settable from UI (#2373)

* foo

* foo

* npm config
This commit is contained in:
Ruben Fiszel
2023-10-02 14:45:29 +02:00
committed by GitHub
parent 3e7827ed09
commit c8543d7ed4
15 changed files with 228 additions and 127 deletions
+5 -76
View File
@@ -226,11 +226,9 @@ From there, you can follow the setup app and create other users.
We publish helm charts at:
<https://github.com/windmill-labs/windmill-helm-charts>.
### Postgres without superuser
### OAuth, SSO & SMTP
If you do not want, or cannot (for instance, in AWS Aurora or Cloud sql) use a
postgres superuser, you can run `./init-db-as-superuser.sql` to init the
required users for Windmill.
Windmill Community Edition allows to configure the OAuth, SSO (including Google Workspace SSO, Microsoft/Azure and Okta) directly from the UI in the superadmin settings. Do note that there is a limit of 50 SSO users on the community edition.
### Commercial license
@@ -249,76 +247,6 @@ your current infrastructure to Windmill, support with tight SLA, and our global
cache sync for high-performance/no dependency cache miss of cluster from 10+
nodes to 200+ nodes.
### OAuth for self-hosting
To get the same oauth integrations as Windmill Cloud, mount `oauth.json` with
the following format:
```json
{
"<client>": {
"id": "<CLIENT_ID>",
"secret": "<CLIENT_SECRET>",
"allowed_domains": ["windmill.dev"] //restrict a client OAuth login to some domains
}
}
```
and mount it at `/usr/src/app/oauth.json`.
The redirect url for the oauth clients is:
`<instance_url>/user/login_callback/<client>`
Even if you setup oauth, you will still want to **login as admin@windmill.dev /
changeme** to setup your instance as a super-admin and give yourself admin
rights.
[The list of all possible "connect an app" oauth clients](https://github.com/windmill-labs/windmill/blob/main/backend/oauth_connect.json)
To add more "connect an app" OAuth clients to the Windmill project, read the
[Contributor's guide](https://www.windmill.dev/docs/misc/contributing). We
welcome contributions!
You may also add your own custom OAuth2 IdP and OAuth2 Resource provider:
```json
{
"<client>": {
"id": "<CLIENT_ID>",
"secret": "<CLIENT_SECRET>",
// To add a new OAuth2 IdP
"login_config": {
"auth_url": "<auth_endpoint>",
"token_url": "<token_endpoint>",
"userinfo_url": "<userinfo endpoint>",
"scopes": ["scope1", "scope2"],
"extra_params": "<if_needed>"
},
// To add a new OAuth2 Resource
"connect_config": {
"auth_url": "<auth_endpoint>",
"token_url": "<token_endpoint>",
"scopes": ["scope1", "scope2"],
"extra_params": "<if_needed>"
}
}
}
```
### smtp for self-hosting
For users to receive emails when you invite them to workspaces or add them to
the instances using their emails, configure the SMTP env variables in the
servers:
```
SMTP_FROM=noreply@windmill.dev
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USERNAME=ruben@windmill.dev
SMTP_PASSWORD=yourpasswordapp
```
### Resource types
You will also want to import all the approved resource types from
@@ -330,7 +258,7 @@ it being synced automatically everyday.
| Environment Variable name | Default | Description | Api Server/Worker/All |
| --------------------------------------------- | ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------- |
| DATABASE_URL | | The Postgres database url. | All |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
| WORKER_GROUP | default | The worker group the worker belongs to and get its configuration pulled from | Worker |
| SERVER_BIND_ADDR | 0.0.0.0 | IP Address on which to bind listening socket | Server |
| PORT | 8000 | Exposed port | Server |
| NUM_WORKERS | 1 | The number of worker per Worker instance (Set to 0 for API/Server instances, Set to 1 for normal workers, and > 1 for workers dedicated to native jobs) | Worker |
@@ -395,12 +323,13 @@ it being synced automatically everyday.
| SMTP_USERNAME | None | username for the smtp server to send invite emails | Server |
| SMTP_PASSWORD | None | password for the smtp server to send invite emails | Server |
| SMTP_TLS_IMPLICIT | false | https://docs.rs/mail-send/latest/mail_send/struct.SmtpClientBuilder.html#method.implicit_tlsemails | Server |
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmin can create workspaces | Server |
| CREATE_WORKSPACE_REQUIRE_SUPERADMIN | true | If true, only superadmin can create workspaces | Server |
| GLOBAL_ERROR_HANDLER_PATH_IN_ADMINS_WORKSPACE | None | Path to a script to run when a root job fails. The script will be run in and from the admins workspace | Server |
| WHITELIST_ENVS | None | List of envs variables, separated by a ',' that are whitelisted as being safe to passthrough the workers | Worker |
| SAML_METADATA | None | SAML Metadata URL to enable SAML SSO (EE only) | Server |
| SECRET_SALT | None | Secret Salt used for encryption and decryption of secrets. If defined, the secrets will not be decryptable unless the right salt is passed in, which is the case for the workers and the server | Server + Worker |
| OPENAI_AZURE_BASE_PATH | None | Azure OpenAI API base path (no trailing slash) | Server |
| DISABLE_NSJAIL | true | Disable Nsjail Sandboxing | Worker |
## Run a local dev setup
@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM global_settings WHERE name = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "025e5bf6aef56e9832239489daf21b4970d84529cf7ed7928ce2dcfec8b007f0"
}
@@ -0,0 +1 @@
-- Add down migration script here
@@ -0,0 +1,15 @@
-- Add up migration script here
CREATE FUNCTION "notify_global_setting_delete" ()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('notify_global_setting_change', OLD.name::text);
RETURN OLD;
END;
$$ LANGUAGE PLPGSQL;
CREATE OR REPLACE TRIGGER "notify_global_setting_delete"
AFTER DELETE ON "global_settings"
FOR EACH ROW
EXECUTE FUNCTION "notify_global_setting_delete" ();
+14 -13
View File
@@ -21,7 +21,8 @@ use tokio::{
};
use windmill_common::{
global_settings::{
BASE_URL_SETTING, CUSTOM_TAGS_SETTING, ENV_SETTINGS, LICENSE_KEY_SETTING, OAUTH_SETTING,
BASE_URL_SETTING, CUSTOM_TAGS_SETTING, ENV_SETTINGS, EXTRA_PIP_INDEX_URL_SETTING,
LICENSE_KEY_SETTING, NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING,
REQUEST_SIZE_LIMIT_SETTING, RETENTION_PERIOD_SECS_SETTING,
},
utils::rd_string,
@@ -36,8 +37,9 @@ use windmill_worker::{
};
use crate::monitor::{
initial_load, monitor_db, reload_base_url_setting, reload_license_key,
reload_retention_period_setting, reload_server_config, reload_worker_config,
initial_load, monitor_db, reload_base_url_setting, reload_extra_pip_index_url_setting,
reload_license_key, reload_npm_config_registry_setting, reload_retention_period_setting,
reload_server_config, reload_worker_config,
};
const GIT_VERSION: &str = git_version!(args = ["--tag", "--always"], fallback = "unknown-version");
@@ -220,52 +222,51 @@ Windmill Community Edition {GIT_VERSION}
tracing::info!("Received new pg notification: {n:?}");
match n.channel() {
"notify_config_change" => {
tracing::info!("Config change detected");
tracing::info!("Config change detected: {}", n.payload());
match n.payload() {
"server" if server_mode => {
tracing::info!("Server config change detected");
reload_server_config(&db).await;
},
a@ _ if worker_mode && a == format!("worker__{}", *WORKER_GROUP) => {
tracing::info!("Worker config change detected");
reload_worker_config(&db, tx.clone(), true).await;
},
_ => {
()
tracing::error!("config target neither a server or a worker");
}
}
},
"notify_global_setting_change" => {
tracing::info!("Global setting change detected");
tracing::info!("Global setting change detected: {}", n.payload());
match n.payload() {
BASE_URL_SETTING => {
tracing::info!("Base URL setting change detected");
if let Err(e) = reload_base_url_setting(&db).await {
tracing::error!(error = %e, "Could not reload base url setting");
}
},
OAUTH_SETTING => {
tracing::info!("OAuth setting change detected");
if let Err(e) = reload_base_url_setting(&db).await {
tracing::error!(error = %e, "Could not reload oauth setting");
}
},
CUSTOM_TAGS_SETTING => {
tracing::info!("Custom tags setting change detected");
if let Err(e) = reload_custom_tags_setting(&db).await {
tracing::error!(error = %e, "Could not reload custom tags setting");
}
},
LICENSE_KEY_SETTING => {
tracing::info!("License Key setting change detected");
if let Err(e) = reload_license_key(&db).await {
tracing::error!(error = %e, "Could not reload license key setting");
}
},
RETENTION_PERIOD_SECS_SETTING => {
tracing::info!("Retention period setting change detected");
reload_retention_period_setting(&db).await
},
EXTRA_PIP_INDEX_URL_SETTING => {
reload_extra_pip_index_url_setting(&db).await
},
NPM_CONFIG_REGISTRY_SETTING => {
reload_npm_config_registry_setting(&db).await
},
REQUEST_SIZE_LIMIT_SETTING => {
tracing::info!("Request limit size change detected, killing server expecting to be restarted");
// we wait a bit randomly to avoid having all servers shutdown at same time
+83 -3
View File
@@ -15,7 +15,8 @@ use windmill_api::{
use windmill_common::{
error,
global_settings::{
BASE_URL_SETTING, LICENSE_KEY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING,
BASE_URL_SETTING, EXTRA_PIP_INDEX_URL_SETTING, LICENSE_KEY_SETTING,
NPM_CONFIG_REGISTRY_SETTING, OAUTH_SETTING, REQUEST_SIZE_LIMIT_SETTING,
RETENTION_PERIOD_SECS_SETTING,
},
jobs::{JobKind, QueuedJob},
@@ -25,7 +26,8 @@ use windmill_common::{
BASE_URL, DB, METRICS_ENABLED,
};
use windmill_worker::{
create_token_for_owner, handle_job_error, AuthedClient, SCRIPT_TOKEN_EXPIRY,
create_token_for_owner, handle_job_error, AuthedClient, NPM_CONFIG_REGISTRY,
PIP_EXTRA_INDEX_URL, SCRIPT_TOKEN_EXPIRY,
};
#[cfg(feature = "enterprise")]
@@ -118,6 +120,18 @@ pub async fn initial_load(
}
};
let reload_extra_pip_index_url_f = async {
if worker_mode {
reload_extra_pip_index_url_setting(&db).await;
}
};
let reload_npm_config_registry_f = async {
if worker_mode {
reload_npm_config_registry_setting(&db).await;
}
};
join!(
reload_worker_config_f,
reload_server_config_f,
@@ -125,7 +139,9 @@ pub async fn initial_load(
reload_request_size_f,
reload_base_url_f,
reload_retention_period_f,
reload_license_key_f
reload_license_key_f,
reload_extra_pip_index_url_f,
reload_npm_config_registry_f
);
}
@@ -201,6 +217,32 @@ pub async fn delete_expired_items(db: &DB) -> () {
}
}
pub async fn reload_extra_pip_index_url_setting(db: &DB) {
if let Err(e) = reload_option_string_setting(
db,
EXTRA_PIP_INDEX_URL_SETTING,
"PIP_EXTRA_INDEX_URL",
PIP_EXTRA_INDEX_URL.clone(),
)
.await
{
tracing::error!("Error reloading extra_pip_index_url period: {:?}", e)
}
}
pub async fn reload_npm_config_registry_setting(db: &DB) {
if let Err(e) = reload_option_string_setting(
db,
NPM_CONFIG_REGISTRY_SETTING,
"NPM_CONFIG_REGISTRY",
NPM_CONFIG_REGISTRY.clone(),
)
.await
{
tracing::error!("Error reloading npm_config_registry period: {:?}", e)
}
}
pub async fn reload_retention_period_setting(db: &DB) {
if let Err(e) = reload_setting(
db,
@@ -261,6 +303,44 @@ pub async fn reload_license_key(db: &DB) -> error::Result<()> {
Ok(())
}
pub async fn reload_option_string_setting(
db: &DB,
setting_name: &str,
std_env_var: &str,
lock: Arc<RwLock<Option<String>>>,
) -> error::Result<()> {
let q = sqlx::query!(
"SELECT value FROM global_settings WHERE name = $1",
setting_name
)
.fetch_optional(db)
.await?;
let mut value = std::env::var(std_env_var).ok();
if let Some(q) = q {
if let Ok(v) = serde_json::from_value::<String>(q.value.clone()) {
tracing::info!(
"Loaded setting {setting_name} from db config: {:#?}",
&q.value
);
value = Some(v)
} else {
tracing::error!("Could not parse {setting_name} found: {:#?}", &q.value);
}
};
{
if value.is_none() {
tracing::info!("Loaded {setting_name} setting to None");
}
let mut l = lock.write().await;
*l = value;
}
Ok(())
}
pub async fn reload_setting<T: FromStr + DeserializeOwned + Display>(
db: &DB,
setting_name: &str,
+27 -9
View File
@@ -79,7 +79,6 @@ pub struct TestKey {
pub license_key: String,
}
pub async fn test_license_key(
Extension(db): Extension<DB>,
authed: ApiAuthed,
@@ -109,6 +108,14 @@ pub async fn get_local_settings(
pub struct Value {
pub value: serde_json::Value,
}
pub async fn delete_global_setting(db: &DB, key: &str) -> error::Result<()> {
sqlx::query!("DELETE FROM global_settings WHERE name = $1", key,)
.execute(db)
.await?;
tracing::info!("Unset global setting {}", key);
Ok(())
}
pub async fn set_global_setting(
Extension(db): Extension<DB>,
authed: ApiAuthed,
@@ -116,14 +123,25 @@ pub async fn set_global_setting(
Json(value): Json<Value>,
) -> error::Result<()> {
require_super_admin(&db, &authed.email).await?;
sqlx::query!(
"INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2, updated_at = now()",
key,
value.value
)
.execute(&db)
.await?;
tracing::info!("Set global setting {} to {}", key, value.value);
match value.value {
serde_json::Value::Null => {
delete_global_setting(&db, &key).await?;
}
serde_json::Value::String(x) if x.is_empty() => {
delete_global_setting(&db, &key).await?;
}
v => {
sqlx::query!(
"INSERT INTO global_settings (name, value) VALUES ($1, $2) ON CONFLICT (name) DO UPDATE SET value = $2, updated_at = now()",
key,
v
)
.execute(&db)
.await?;
tracing::info!("Set global setting {} to {}", key, v);
}
};
Ok(())
}
@@ -5,6 +5,8 @@ pub const OAUTH_SETTING: &str = "oauths";
pub const RETENTION_PERIOD_SECS_SETTING: &str = "retention_period_secs";
pub const REQUEST_SIZE_LIMIT_SETTING: &str = "request_size_limit_mb";
pub const LICENSE_KEY_SETTING: &str = "license_key";
pub const NPM_CONFIG_REGISTRY_SETTING: &str = "npm_config_registry";
pub const EXTRA_PIP_INDEX_URL_SETTING: &str = "pip_extra_index_url";
pub const ENV_SETTINGS: [&str; 54] = [
"DISABLE_NSJAIL",
+11 -10
View File
@@ -93,7 +93,7 @@ pub async fn gen_lockfile(
.await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url);
get_common_bun_proc_envs(&base_internal_url).await;
let child = Command::new(&*BUN_PATH)
.current_dir(job_dir)
@@ -252,7 +252,7 @@ pub async fn handle_bun_job(
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url);
get_common_bun_proc_envs(&base_internal_url).await;
if let Some(reqs) = requirements_o {
let splitted = reqs.split(BUN_LOCKB_SPLIT).collect::<Vec<&str>>();
@@ -297,7 +297,8 @@ pub async fn handle_bun_job(
// TODO: remove once bun implement a reasonable set of trusted deps
let trusted_deps = get_trusted_deps(inner_content);
let empty_trusted_deps = trusted_deps.len() == 0;
if !*DISABLE_NSJAIL || !empty_trusted_deps {
let has_custom_config_registry = common_bun_proc_envs.contains_key("NPM_CONFIG_REGISTRY");
if !*DISABLE_NSJAIL || !empty_trusted_deps || has_custom_config_registry {
logs.push_str("\n\n--- BUN INSTALL ---\n");
set_logs(&logs, &job.id, &db).await;
let _ = gen_lockfile(
@@ -316,7 +317,7 @@ pub async fn handle_bun_job(
.await?;
}
if empty_trusted_deps {
if empty_trusted_deps && !has_custom_config_registry {
let node_modules_path = format!("{}/node_modules", job_dir);
let node_modules_exists = tokio::fs::metadata(&node_modules_path).await.is_ok();
if node_modules_exists {
@@ -498,8 +499,8 @@ plugin(p)
read_result(job_dir).await
}
pub fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String, String> {
let mut deno_envs: HashMap<String, String> = HashMap::from([
pub async fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String, String> {
let mut bun_envs: HashMap<String, String> = HashMap::from([
(String::from("PATH"), PATH_ENV.clone()),
(String::from("HOME"), HOME_ENV.clone()),
(String::from("TZ"), TZ_ENV.clone()),
@@ -517,10 +518,10 @@ pub fn get_common_bun_proc_envs(base_internal_url: &str) -> HashMap<String, Stri
),
]);
if let Some(ref s) = *NPM_CONFIG_REGISTRY {
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
bun_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
}
return deno_envs;
return bun_envs;
}
#[cfg(feature = "enterprise")]
@@ -546,7 +547,7 @@ pub async fn start_worker(
let mut logs = "".to_string();
let _ = write_file(job_dir, "main.ts", inner_content).await?;
let common_bun_proc_envs: HashMap<String, String> =
get_common_bun_proc_envs(&base_internal_url);
get_common_bun_proc_envs(&base_internal_url).await;
let context = variables::get_reserved_variables(
w_id,
&token,
+6 -1
View File
@@ -63,7 +63,7 @@ async fn get_common_deno_proc_envs(
),
]);
if let Some(ref s) = *NPM_CONFIG_REGISTRY {
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
}
return deno_envs;
@@ -92,6 +92,10 @@ pub async fn generate_deno_lock(
write_file(job_dir, "import_map.json", &import_map).await?;
write_file(job_dir, "empty.ts", "").await?;
let mut deno_envs = HashMap::new();
if let Some(ref s) = NPM_CONFIG_REGISTRY.read().await.clone() {
deno_envs.insert(String::from("NPM_CONFIG_REGISTRY"), s.clone());
}
let child = Command::new(DENO_PATH.as_str())
.current_dir(job_dir)
.args(vec![
@@ -103,6 +107,7 @@ pub async fn generate_deno_lock(
&import_map_path,
"main.ts",
])
.envs(deno_envs)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()?;
@@ -25,7 +25,6 @@ lazy_static::lazy_static! {
static ref PIP_INDEX_URL: Option<String> = std::env::var("PIP_INDEX_URL").ok();
static ref PIP_EXTRA_INDEX_URL: Option<String> = std::env::var("PIP_EXTRA_INDEX_URL").ok();
static ref PIP_TRUSTED_HOST: Option<String> = std::env::var("PIP_TRUSTED_HOST").ok();
static ref PIP_LOCAL_DEPENDENCIES: Option<Vec<String>> = {
let pip_local_dependencies = std::env::var("PIP_LOCAL_DEPENDENCIES")
@@ -62,7 +61,7 @@ use crate::{
write_file,
},
AuthedClientBackgroundTask, DISABLE_NSJAIL, DISABLE_NUSER, HTTPS_PROXY, HTTP_PROXY,
LOCK_CACHE_DIR, NO_PROXY, NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, TZ_ENV,
LOCK_CACHE_DIR, NO_PROXY, NSJAIL_PATH, PATH_ENV, PIP_CACHE_DIR, PIP_EXTRA_INDEX_URL, TZ_ENV,
};
pub async fn create_dependencies_dir(job_dir: &str) {
@@ -117,7 +116,8 @@ pub async fn pip_compile(
write_file(job_dir, file, &requirements).await?;
let mut args = vec!["-q", "--no-header", file, "--resolver=backtracking"];
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
let pip_extra_index_url = PIP_EXTRA_INDEX_URL.read().await.clone();
if let Some(url) = pip_extra_index_url.as_ref() {
args.extend(["--extra-index-url", url]);
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
@@ -497,8 +497,11 @@ pub async fn handle_python_reqs(
) -> error::Result<Vec<String>> {
let mut req_paths: Vec<String> = vec![];
let mut vars = vec![("PATH", PATH_ENV.as_str())];
let pip_extra_index_url;
if !*DISABLE_NSJAIL {
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
pip_extra_index_url = PIP_EXTRA_INDEX_URL.read().await.clone();
if let Some(url) = pip_extra_index_url.as_ref() {
vars.push(("EXTRA_INDEX_URL", url));
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
@@ -595,7 +598,8 @@ pub async fn handle_python_reqs(
"-t",
venv_p.as_str(),
];
if let Some(url) = PIP_EXTRA_INDEX_URL.as_ref() {
let pip_extra_index_url = PIP_EXTRA_INDEX_URL.read().await.clone();
if let Some(url) = pip_extra_index_url.as_ref() {
command_args.extend(["--extra-index-url", url]);
}
if let Some(url) = PIP_INDEX_URL.as_ref() {
+2 -1
View File
@@ -223,7 +223,8 @@ lazy_static::lazy_static! {
pub static ref NETRC: Option<String> = std::env::var("NETRC").ok();
pub static ref NPM_CONFIG_REGISTRY: Option<String> = std::env::var("NPM_CONFIG_REGISTRY").ok();
pub static ref NPM_CONFIG_REGISTRY: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
pub static ref PIP_EXTRA_INDEX_URL: Arc<RwLock<Option<String>>> = Arc::new(RwLock::new(None));
@@ -17,6 +17,7 @@
import { capitalize } from '$lib/utils'
import { enterpriseLicense } from '$lib/stores'
import CustomOauth from './CustomOauth.svelte'
import { AlertTriangle } from 'lucide-svelte'
export const settings: Record<string, Setting[]> = {
Core: [
@@ -61,6 +62,26 @@
fieldType: 'license_key',
placeholder: 'only needed to prepare upgrade to EE',
storage: 'setting'
},
{
label: 'Pip Extra Index Url',
description: 'Add private PIP registry',
key: 'pip_extra_index_url',
fieldType: 'text',
placeholder: 'https://username:password@pypi.company.com/simple',
storage: 'setting',
ee_only:
'You can still set this setting by using PIP_EXTRA_INDEX_URL as env variable to the worker containers'
},
{
label: 'Npm Config Registry',
description: 'Add private NPM registry',
key: 'npm_config_registry',
fieldType: 'text',
placeholder: 'https://yourregistry',
storage: 'setting',
ee_only:
'You can still set this setting by using NPM_CONFIG_REGISTRY as env variable to the worker containers'
}
],
SMTP: [
@@ -153,7 +174,7 @@
const allSettings = Object.values(settings).flatMap((x) => Object.entries(x))
const newServerConfig = Object.fromEntries(
allSettings
.filter((x) => x[1].storage == 'config')
.filter((x) => x[1].storage == 'config' && values?.[x[1].key] && values?.[x[1].key] != '')
.map((x) => [x[1].key, values?.[x[1].key]])
)
if (!deepEqual(newServerConfig, serverConfig)) {
@@ -165,19 +186,20 @@
}
await Promise.all(
allSettings
.filter(
(x) =>
.filter((x) => {
return (
x[1].storage == 'setting' &&
!deepEqual(initialValues?.[x[1].key], values?.[x[1].key]) &&
values?.[x[1].key] != undefined &&
values?.[x[1].key] != null &&
values?.[x[1].key] != ''
)
(values?.[x[1].key] != '' ||
initialValues?.[x[1].key] != undefined ||
initialValues?.[x[1].key] != null)
)
})
.map(async ([_, x]) => {
await SettingService.setGlobal({ key: x.key, requestBody: { value: values?.[x.key] } })
})
)
initialValues = JSON.parse(JSON.stringify(initialValues))
initialValues = JSON.parse(JSON.stringify(values))
if (!deepEqual(initialOauths, oauths)) {
await SettingService.setGlobal({
@@ -251,6 +273,12 @@
<div class="flex-col flex gap-2 pb-4">
{#each settings[category] as setting}
{#if !setting.cloudonly || isCloudHosted()}
{#if setting.ee_only != undefined && !$enterpriseLicense}
<div class="flex text-xs items-center gap-1 text-yellow-500 whitespace-nowrap">
<AlertTriangle size={16} />
EE only <Tooltip>{setting.ee_only}</Tooltip>
</div>
{/if}
<label class="block pb-2">
<span class="text-primary font-semibold text-sm">{setting.label}</span>
{#if setting.description}
@@ -262,6 +290,7 @@
{#if values}
{#if setting.fieldType == 'text'}
<input
disabled={setting.ee_only != undefined && !$enterpriseLicense}
type="text"
placeholder={setting.placeholder}
bind:value={values[setting.key]}
@@ -51,7 +51,7 @@
{#if $enterpriseLicense === undefined}
<div bind:clientHeight={alertHeight} class="p-2 flex flex-row gap-2">
<div class="flex flex-row items-center text-yellow-500 text-xs">
<div class="flex whitespace-nowrap">
<div class="flex items-center whitespace-nowrap">
<AlertTriangle size={16} />
EE only
</div>
@@ -3,6 +3,7 @@ export interface Setting {
description?: string
placeholder?: string
cloudonly?: boolean
ee_only?: string
tooltip?: string
key: string
fieldType: