mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-27 01:02:45 +01:00
save config
This commit is contained in:
@@ -275,7 +275,8 @@
|
||||
"USER_STATUS_PENDING": "Pending",
|
||||
"VIEW": "View",
|
||||
"ADD_CHAT_ID": "Add ChatId...",
|
||||
"ADD_ATTRIBUTE": "Add Attribute..."
|
||||
"ADD_ATTRIBUTE": "Add Attribute...",
|
||||
"ADD_EXTENSION": "Add Extension..."
|
||||
},
|
||||
"TITLE": {
|
||||
"USER_BOUQUET_EDITOR": "User group editor"
|
||||
|
||||
@@ -68,4 +68,5 @@
|
||||
@forward "components/tabset";
|
||||
@forward "components/select";
|
||||
@forward "components/form";
|
||||
@forward "components/api_user/api_user_view";
|
||||
@forward "components/api_user/api_user_view";
|
||||
@forward "components/keyvalue_editor";
|
||||
@@ -0,0 +1,23 @@
|
||||
.tp__keyvalue-editor {
|
||||
display: flex;
|
||||
flex-flow: column;
|
||||
box-sizing: border-box;
|
||||
gap: var(--gap-small);
|
||||
|
||||
&__entries {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
box-sizing: border-box;
|
||||
gap: var(--gap-small);
|
||||
}
|
||||
|
||||
label{
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
&__inputs {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
box-sizing: border-box;
|
||||
gap: var(--gap-small);
|
||||
}
|
||||
}
|
||||
@@ -39,6 +39,70 @@ pub fn ApiConfigView() -> Html {
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let on_form_change = config_view_ctx.on_form_change.clone();
|
||||
let deps = (form_state.form.clone(), form_state.modified);
|
||||
use_effect_with(deps, move |(form, modified)| {
|
||||
on_form_change.emit(ConfigForm::Api(*modified, form.clone()));
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let on_form_change = config_view_ctx.on_form_change.clone();
|
||||
let deps = (form_state.form.clone(), form_state.modified);
|
||||
use_effect_with(deps, move |(form, modified)| {
|
||||
on_form_change.emit(ConfigForm::Api(*modified, form.clone()));
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let on_form_change = config_view_ctx.on_form_change.clone();
|
||||
let deps = (form_state.form.clone(), form_state.modified);
|
||||
use_effect_with(deps, move |(form, modified)| {
|
||||
on_form_change.emit(ConfigForm::Api(*modified, form.clone()));
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let on_form_change = config_view_ctx.on_form_change.clone();
|
||||
let deps = (form_state.form.clone(), form_state.modified);
|
||||
use_effect_with(deps, move |(form, modified)| {
|
||||
on_form_change.emit(ConfigForm::Api(*modified, form.clone()));
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let on_form_change = config_view_ctx.on_form_change.clone();
|
||||
let deps = (form_state.form.clone(), form_state.modified);
|
||||
use_effect_with(deps, move |(form, modified)| {
|
||||
on_form_change.emit(ConfigForm::Api(*modified, form.clone()));
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let on_form_change = config_view_ctx.on_form_change.clone();
|
||||
let deps = (form_state.form.clone(), form_state.modified);
|
||||
use_effect_with(deps, move |(form, modified)| {
|
||||
on_form_change.emit(ConfigForm::Api(*modified, form.clone()));
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let on_form_change = config_view_ctx.on_form_change.clone();
|
||||
let deps = (form_state.form.clone(), form_state.modified);
|
||||
use_effect_with(deps, move |(form, modified)| {
|
||||
on_form_change.emit(ConfigForm::Api(*modified, form.clone()));
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
let form_state = form_state.clone();
|
||||
let api_config = config_ctx
|
||||
@@ -46,7 +110,8 @@ pub fn ApiConfigView() -> Html {
|
||||
.as_ref()
|
||||
.map(|c| c.config.api.clone());
|
||||
|
||||
use_effect_with((api_config, config_view_ctx.edit_mode.clone()), move |(cfg, _mode)| {
|
||||
let deps = (api_config, *config_view_ctx.edit_mode);
|
||||
use_effect_with(deps, move |(cfg, _mode)| {
|
||||
if let Some(api) = cfg {
|
||||
form_state.dispatch(ApiConfigFormAction::SetAll(api.clone()));
|
||||
} else {
|
||||
@@ -54,6 +119,7 @@ pub fn ApiConfigView() -> Html {
|
||||
}
|
||||
|| ()
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
let render_empty = || {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use shared::error::TuliproxError;
|
||||
use shared::info_err;
|
||||
use shared::model::{ConfigApiDto, HdHomeRunConfigDto, IpCheckConfigDto, MainConfigDto, MessagingConfigDto, ProxyConfigDto, ReverseProxyConfigDto, ScheduleConfigDto, WebUiConfigDto};
|
||||
use shared::model::{ConfigApiDto, HdHomeRunConfigDto, IpCheckConfigDto, MainConfigDto, MessagingConfigDto, ProxyConfigDto, ReverseProxyConfigDto, ScheduleConfigDto, VideoConfigDto, WebUiConfigDto};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
@@ -72,7 +72,7 @@ pub enum ConfigForm {
|
||||
Main(bool, MainConfigDto),
|
||||
Api(bool, ConfigApiDto),
|
||||
Schedules(bool, ScheduleConfigDto),
|
||||
Video(bool, MessagingConfigDto),
|
||||
Video(bool, VideoConfigDto),
|
||||
Messaging(bool, MessagingConfigDto),
|
||||
WebUi(bool, WebUiConfigDto),
|
||||
ReverseProxy(bool, ReverseProxyConfigDto),
|
||||
|
||||
@@ -123,15 +123,23 @@ pub fn ReverseProxyConfigView() -> Html {
|
||||
use_effect_with((reverse_proxy_cfg, config_view_ctx.edit_mode.clone()), move |(cfg, _mode)| {
|
||||
if let Some(rp) = cfg {
|
||||
reverse_proxy_state.dispatch(ReverseProxyConfigFormAction::SetAll((*rp).clone()));
|
||||
|
||||
if let Some(cache) = &rp.cache {
|
||||
cache_state.dispatch(CacheConfigFormAction::SetAll(cache.clone()));
|
||||
}
|
||||
if let Some(rl) = &rp.rate_limit {
|
||||
rate_limit_state.dispatch(RateLimitConfigFormAction::SetAll(rl.clone()));
|
||||
}
|
||||
if let Some(stream) = &rp.stream {
|
||||
stream_state.dispatch(StreamConfigFormAction::SetAll(stream.clone()));
|
||||
}
|
||||
cache_state.dispatch(CacheConfigFormAction::SetAll(cache.clone()));
|
||||
} else {
|
||||
cache_state.dispatch(CacheConfigFormAction::SetAll(CacheConfigDto::default()));
|
||||
}
|
||||
if let Some(rl) = &rp.rate_limit {
|
||||
rate_limit_state.dispatch(RateLimitConfigFormAction::SetAll(rl.clone()));
|
||||
} else {
|
||||
rate_limit_state.dispatch(RateLimitConfigFormAction::SetAll(RateLimitConfigDto::default()));
|
||||
}
|
||||
if let Some(stream) = &rp.stream {
|
||||
stream_state.dispatch(StreamConfigFormAction::SetAll(stream.clone()));
|
||||
} else {
|
||||
stream_state.dispatch(StreamConfigFormAction::SetAll(StreamConfigDto::default()));
|
||||
}
|
||||
|
||||
} else {
|
||||
reverse_proxy_state.dispatch(ReverseProxyConfigFormAction::SetAll(ReverseProxyConfigDto::default()));
|
||||
cache_state.dispatch(CacheConfigFormAction::SetAll(CacheConfigDto::default()));
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use crate::app::components::{Card, Chip};
|
||||
use std::collections::HashMap;
|
||||
use crate::app::components::{Card, Chip, KeyValueEditor};
|
||||
use crate::app::context::ConfigContext;
|
||||
use crate::{config_field_bool, config_field_child, config_field_empty, config_field_optional};
|
||||
use shared::model::VideoDownloadConfigDto;
|
||||
use crate::{config_field_bool, config_field_child, config_field_empty, config_field_optional, edit_field_bool, edit_field_text_option, edit_field_list, generate_form_reducer, config_field_bool_empty};
|
||||
use shared::model::{VideoDownloadConfigDto, VideoConfigDto};
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use crate::app::components::config::config_page::ConfigForm;
|
||||
use crate::app::components::config::config_view_context::ConfigViewContext;
|
||||
use crate::app::components::config::macros::HasFormData;
|
||||
|
||||
const LABEL_DOWNLOAD: &str = "LABEL.DOWNLOAD";
|
||||
const LABEL_ORGANIZE_INTO_DIRECTORIES: &str = "LABEL.ORGANIZE_INTO_DIRECTORIES";
|
||||
@@ -12,26 +16,97 @@ const LABEL_EPISODE_PATTERN: &str = "LABEL.EPISODE_PATTERN";
|
||||
const LABEL_HEADERS: &str = "LABEL.HEADERS";
|
||||
const LABEL_EXTENSIONS: &str = "LABEL.EXTENSIONS";
|
||||
const LABEL_WEB_SEARCH: &str = "LABEL.WEB_SEARCH";
|
||||
const LABEL_ADD_EXTENSION: &str = "LABEL.ADD_EXTENSION";
|
||||
|
||||
generate_form_reducer!(
|
||||
state: VideoDownloadConfigFormState { form: VideoDownloadConfigDto },
|
||||
action_name: VideoDownloadConfigFormAction,
|
||||
fields {
|
||||
OrganizeIntoDirectories => organize_into_directories: bool,
|
||||
Directory => directory: Option<String>,
|
||||
EpisodePattern => episode_pattern: Option<String>,
|
||||
Headers => headers: HashMap<String, String>,
|
||||
}
|
||||
);
|
||||
|
||||
generate_form_reducer!(
|
||||
state: VideoConfigFormState { form: VideoConfigDto },
|
||||
action_name: VideoConfigFormAction,
|
||||
fields {
|
||||
WebSearch => web_search: Option<String>,
|
||||
Extensions => extensions: Vec<String>,
|
||||
}
|
||||
);
|
||||
|
||||
#[function_component]
|
||||
pub fn VideoConfigView() -> Html {
|
||||
let translate = use_translation();
|
||||
let config_ctx = use_context::<ConfigContext>().expect("ConfigContext not found");
|
||||
let config_view_ctx = use_context::<ConfigViewContext>().expect("ConfigViewContext not found");
|
||||
|
||||
let download_state: UseReducerHandle<VideoDownloadConfigFormState> = use_reducer(|| {
|
||||
VideoDownloadConfigFormState { form: VideoDownloadConfigDto::default(), modified: false }
|
||||
});
|
||||
let video_state: UseReducerHandle<VideoConfigFormState> = use_reducer(|| {
|
||||
VideoConfigFormState { form: VideoConfigDto::default(), modified: false }
|
||||
});
|
||||
|
||||
let handle_headers = {
|
||||
let download_state = download_state.clone();
|
||||
Callback::from(move |headers: HashMap<String, String>| {
|
||||
download_state.dispatch(VideoDownloadConfigFormAction::Headers(headers));
|
||||
})
|
||||
};
|
||||
|
||||
{
|
||||
let video_state = video_state.clone();
|
||||
let download_state = download_state.clone();
|
||||
let video_cfg = config_ctx.config.as_ref().and_then(|c| c.config.video.clone());
|
||||
use_effect_with(video_cfg, move |video_cfg| {
|
||||
if let Some(video) = video_cfg {
|
||||
video_state.dispatch(VideoConfigFormAction::Extensions(video.extensions.clone()));
|
||||
video_state.dispatch(VideoConfigFormAction::WebSearch(video.web_search.clone()));
|
||||
if let Some(download) = video.download.as_ref() {
|
||||
download_state.dispatch(VideoDownloadConfigFormAction::OrganizeIntoDirectories(download.organize_into_directories));
|
||||
download_state.dispatch(VideoDownloadConfigFormAction::Directory(download.directory.clone()));
|
||||
download_state.dispatch(VideoDownloadConfigFormAction::EpisodePattern(download.episode_pattern.clone()));
|
||||
download_state.dispatch(VideoDownloadConfigFormAction::Headers(download.headers.clone()));
|
||||
} else {
|
||||
download_state.dispatch(VideoDownloadConfigFormAction::SetAll(VideoDownloadConfigDto::default()));
|
||||
}
|
||||
} else {
|
||||
video_state.dispatch(VideoConfigFormAction::SetAll(VideoConfigDto::default()));
|
||||
download_state.dispatch(VideoDownloadConfigFormAction::SetAll(VideoDownloadConfigDto::default()));
|
||||
}
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
// Sync form changes with parent context
|
||||
let on_form_change = config_view_ctx.on_form_change.clone();
|
||||
let download_state = download_state.clone();
|
||||
let video_state = video_state.clone();
|
||||
use_effect_with((video_state, download_state), move |(v, d)| {
|
||||
let mut form = v.form.clone();
|
||||
form.download = Some(d.form.clone());
|
||||
let modified = v.modified || d.modified;
|
||||
on_form_change.emit(ConfigForm::Video(modified, form));
|
||||
});
|
||||
}
|
||||
|
||||
let render_extensions = |extensions: &Vec<String>| html! {
|
||||
<Card>
|
||||
{ config_field_child!(translate.t(LABEL_EXTENSIONS), {
|
||||
html! {
|
||||
<div class="tp__config-view__tags">
|
||||
{
|
||||
html! { for extensions.iter().map(|t| html! { <Chip label={t.clone()} /> }) }
|
||||
}
|
||||
{ html! { for extensions.iter().map(|t| html! { <Chip label={t.clone()} /> }) } }
|
||||
</div>
|
||||
}})}
|
||||
</Card>
|
||||
};
|
||||
|
||||
let render_download = |download: Option<&VideoDownloadConfigDto>| html! {
|
||||
let render_download_view = |download: Option<&VideoDownloadConfigDto>| html! {
|
||||
match download {
|
||||
Some(entry) => html! {
|
||||
<Card class="tp__config-view__card">
|
||||
@@ -43,17 +118,7 @@ pub fn VideoConfigView() -> Html {
|
||||
html! {
|
||||
<div class="tp__config-view__tags">
|
||||
<ul>
|
||||
{
|
||||
if entry.headers.is_empty() {
|
||||
html! {}
|
||||
} else {
|
||||
html! {
|
||||
for entry.headers.iter().map(|(key, value)| html! {
|
||||
<li>{key}{":"} {value}</li>
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
{ for entry.headers.iter().map(|(k,v)| html!{ <li>{k}{":"} {v}</li> }) }
|
||||
</ul>
|
||||
</div>
|
||||
}
|
||||
@@ -63,52 +128,75 @@ pub fn VideoConfigView() -> Html {
|
||||
None => html! {
|
||||
<Card class="tp__config-view__card">
|
||||
<h1>{translate.t(LABEL_DOWNLOAD)}</h1>
|
||||
{ config_field_empty!(translate.t(LABEL_ORGANIZE_INTO_DIRECTORIES)) }
|
||||
{ config_field_bool_empty!(translate.t(LABEL_ORGANIZE_INTO_DIRECTORIES)) }
|
||||
{ config_field_empty!(translate.t(LABEL_DIRECTORY)) }
|
||||
{ config_field_empty!(translate.t(LABEL_EPISODE_PATTERN)) }
|
||||
{ config_field_empty!(translate.t(LABEL_HEADERS)) }
|
||||
</Card>
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let render_empty = || {
|
||||
html! {
|
||||
<>
|
||||
let render_empty = || html! {
|
||||
<>
|
||||
<div class="tp__video-config-config-view__body tp__config-view-page__header">
|
||||
{ config_field_empty!(translate.t(LABEL_WEB_SEARCH)) }
|
||||
</div>
|
||||
<div class="tp__video-config-config-view__body tp__config-view-page__body">
|
||||
{ config_field_empty!(translate.t(LABEL_EXTENSIONS)) }
|
||||
{ render_download(None) }
|
||||
{ render_download_view(None) }
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
};
|
||||
|
||||
let render_view_mode = || {
|
||||
if let Some(config) = &config_ctx.config {
|
||||
if let Some(video) = &config.config.video {
|
||||
html! {
|
||||
<>
|
||||
<div class="tp__video-config-view__body tp__config-view-page__body">
|
||||
{ config_field_optional!(video, translate.t(LABEL_WEB_SEARCH), web_search) }
|
||||
</div>
|
||||
<div class="tp__video-config-view__body tp__config-view-page__body">
|
||||
{ render_extensions(&video.extensions) }
|
||||
{ render_download_view(video.download.as_ref()) }
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} else { render_empty() }
|
||||
} else { render_empty() }
|
||||
};
|
||||
|
||||
let render_edit_mode = || {
|
||||
html! {
|
||||
<>
|
||||
<div class="tp__video-config-view__body tp__config-view-page__body">
|
||||
{ edit_field_text_option!(video_state, translate.t(LABEL_WEB_SEARCH), web_search, VideoConfigFormAction::WebSearch) }
|
||||
</div>
|
||||
<div class="tp__video-config-view__body tp__config-view-page__body">
|
||||
<Card class="tp__config-view__card">
|
||||
{ edit_field_list!(video_state, translate.t(LABEL_EXTENSIONS), extensions, VideoConfigFormAction::Extensions, translate.t(LABEL_ADD_EXTENSION)) }
|
||||
</Card>
|
||||
<Card class="tp__config-view__card">
|
||||
<h1>{translate.t(LABEL_DOWNLOAD)}</h1>
|
||||
{ edit_field_bool!(download_state, translate.t(LABEL_ORGANIZE_INTO_DIRECTORIES), organize_into_directories, VideoDownloadConfigFormAction::OrganizeIntoDirectories) }
|
||||
{ edit_field_text_option!(download_state, translate.t(LABEL_DIRECTORY), directory, VideoDownloadConfigFormAction::Directory) }
|
||||
{ edit_field_text_option!(download_state, translate.t(LABEL_EPISODE_PATTERN), episode_pattern, VideoDownloadConfigFormAction::EpisodePattern) }
|
||||
<KeyValueEditor
|
||||
label={Some(translate.t(LABEL_HEADERS))}
|
||||
entries={download_state.form.headers.clone()}
|
||||
readonly={false}
|
||||
on_change={handle_headers}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__video-config-view tp__config-view-page">
|
||||
{
|
||||
if let Some(config) = &config_ctx.config {
|
||||
if let Some(video) = &config.config.video {
|
||||
html! {
|
||||
<>
|
||||
<div class="tp__video-config-view__body tp__config-view-page__body">
|
||||
{ config_field_optional!(video, translate.t(LABEL_WEB_SEARCH), web_search) }
|
||||
</div>
|
||||
<div class="tp__video-config-view__body tp__config-view-page__body">
|
||||
{ render_extensions(&video.extensions) }
|
||||
{ render_download(video.download.as_ref()) }
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
} else {
|
||||
{ render_empty() }
|
||||
}
|
||||
} else {
|
||||
{ render_empty() }
|
||||
}
|
||||
}
|
||||
{ if *config_view_ctx.edit_mode { render_edit_mode() } else { render_view_mode() } }
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,10 +102,15 @@ pub fn WebUiConfigView() -> Html {
|
||||
webui_state.dispatch(WebUiConfigFormAction::SetAll((*webui).clone()));
|
||||
if let Some(auth) = &webui.auth {
|
||||
auth_state.dispatch(WebUiAuthConfigFormAction::SetAll(auth.clone()));
|
||||
} else {
|
||||
auth_state.dispatch(WebUiAuthConfigFormAction::SetAll(WebAuthConfigDto::default()));
|
||||
}
|
||||
if let Some(csp) = &webui.content_security_policy {
|
||||
csp_state.dispatch(CspConfigFormAction::SetAll(csp.clone()));
|
||||
} else {
|
||||
csp_state.dispatch(CspConfigFormAction::SetAll(ContentSecurityPolicyConfigDto::default()));
|
||||
}
|
||||
|
||||
} else {
|
||||
webui_state.dispatch(WebUiConfigFormAction::SetAll(WebUiConfigDto::default()));
|
||||
auth_state.dispatch(WebUiAuthConfigFormAction::SetAll(WebAuthConfigDto::default()));
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use web_sys::HtmlInputElement;
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::chip::Chip;
|
||||
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub struct KeyValue {
|
||||
pub key: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
pub struct KeyValueEditorProps {
|
||||
#[prop_or_default]
|
||||
pub label: Option<String>,
|
||||
pub entries: HashMap<String, String>,
|
||||
#[prop_or_else(Callback::noop)]
|
||||
pub on_change: Callback<HashMap<String, String>>,
|
||||
#[prop_or(true)]
|
||||
pub readonly: bool,
|
||||
#[prop_or_else(|| "Add key".to_string())]
|
||||
pub key_placeholder: String,
|
||||
#[prop_or_else(|| "Add value".to_string())]
|
||||
pub value_placeholder: String,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn KeyValueEditor(props: &KeyValueEditorProps) -> Html {
|
||||
let KeyValueEditorProps {
|
||||
label,
|
||||
entries,
|
||||
on_change,
|
||||
readonly,
|
||||
key_placeholder,
|
||||
value_placeholder,
|
||||
} = props.clone();
|
||||
|
||||
// local state for editing
|
||||
let entry_state = use_state(|| {
|
||||
entries.iter().map(|(k,v)| Rc::new(KeyValue { key: k.clone(), value: v.clone() })).collect::<Vec<_>>()
|
||||
});
|
||||
let new_key = use_state(String::default);
|
||||
let new_value = use_state(String::default);
|
||||
|
||||
// keep local state in sync when parent updates
|
||||
{
|
||||
let entry_state = entry_state.clone();
|
||||
use_effect_with(entries.clone(), move |entries| {
|
||||
entry_state.set(
|
||||
entries.iter()
|
||||
.map(|(k,v)| Rc::new(KeyValue { key: k.clone(), value: v.clone() }))
|
||||
.collect()
|
||||
);
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
// remove existing entry
|
||||
let on_remove = {
|
||||
let entry_state = entry_state.clone();
|
||||
let on_change = on_change.clone();
|
||||
Callback::from(move |key: String| {
|
||||
let mut updated = (*entry_state).clone();
|
||||
updated.retain(|kv| kv.key != key);
|
||||
// emit new HashMap
|
||||
let map = updated.iter().map(|kv| (kv.key.clone(), kv.value.clone())).collect::<HashMap<_,_>>();
|
||||
on_change.emit(map.clone());
|
||||
entry_state.set(updated);
|
||||
})
|
||||
};
|
||||
|
||||
// input change for new key/value
|
||||
let on_input_key = {
|
||||
let new_key = new_key.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
new_key.set(input.value());
|
||||
})
|
||||
};
|
||||
|
||||
let on_input_value = {
|
||||
let new_value = new_value.clone();
|
||||
Callback::from(move |e: InputEvent| {
|
||||
let input: HtmlInputElement = e.target_unchecked_into();
|
||||
new_value.set(input.value());
|
||||
})
|
||||
};
|
||||
|
||||
// add new entry on enter in value field
|
||||
let on_keydown_value = {
|
||||
let new_key = new_key.clone();
|
||||
let new_value = new_value.clone();
|
||||
let entry_state = entry_state.clone();
|
||||
let on_change = on_change.clone();
|
||||
Callback::from(move |e: KeyboardEvent| {
|
||||
if e.key() == "Enter" {
|
||||
e.prevent_default();
|
||||
let key = (*new_key).trim().to_string();
|
||||
let value = (*new_value).trim().to_string();
|
||||
if !key.is_empty() && !value.is_empty() && !entry_state.iter().any(|kv| kv.key == key) {
|
||||
let mut updated = (*entry_state).clone();
|
||||
updated.push(Rc::new(KeyValue { key: key.clone(), value: value.clone() }));
|
||||
// emit new HashMap
|
||||
let map = updated.iter().map(|kv| (kv.key.clone(), kv.value.clone())).collect::<HashMap<_,_>>();
|
||||
on_change.emit(map.clone());
|
||||
entry_state.set(updated);
|
||||
}
|
||||
new_key.set(String::new());
|
||||
new_value.set(String::new());
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class="tp__keyvalue-editor">
|
||||
{ if let Some(lbl) = &label {
|
||||
html! { <label>{ lbl }</label> }
|
||||
} else { html!{} } }
|
||||
<div class="tp__keyvalue-editor__entries">
|
||||
{ for (*entry_state).iter().map(|kv| {
|
||||
let key_clone = kv.key.clone();
|
||||
html! {
|
||||
<Chip
|
||||
label={format!("{}: {}", kv.key, kv.value)}
|
||||
removable={!readonly}
|
||||
on_remove={if readonly { Callback::noop() } else { on_remove.reform(move |_| key_clone.clone()) }}
|
||||
/>
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
{
|
||||
if readonly {
|
||||
html! {}
|
||||
} else {
|
||||
html! {
|
||||
<div class="tp__keyvalue-editor__inputs">
|
||||
<div class="tp__input">
|
||||
<div class=" tp__input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
value={(*new_key).clone()}
|
||||
oninput={on_input_key}
|
||||
placeholder={key_placeholder.clone()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tp__input">
|
||||
<div class=" tp__input-wrapper">
|
||||
<input
|
||||
type="text"
|
||||
value={(*new_value).clone()}
|
||||
oninput={on_input_value}
|
||||
onkeydown={on_keydown_value}
|
||||
placeholder={value_placeholder.clone()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,7 @@ mod date_input;
|
||||
mod role_based_content;
|
||||
mod api_user;
|
||||
mod cell_value;
|
||||
mod key_value_editor;
|
||||
// pub use self::input::*;
|
||||
// pub use self::menu_item::*;
|
||||
// pub use self::popup_menu::*;
|
||||
@@ -79,5 +80,6 @@ pub use self::csv_table::*;
|
||||
pub use self::tabset::*;
|
||||
pub use self::role_based_content::*;
|
||||
pub use self::cell_value::*;
|
||||
pub use self::key_value_editor::*;
|
||||
//pub use self::number_input::*;
|
||||
//pub use self::date_input::*;
|
||||
@@ -4,7 +4,7 @@ fn default_token_ttl_mins() -> u32 {
|
||||
30
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WebAuthConfigDto {
|
||||
#[serde(default = "default_as_true")]
|
||||
@@ -16,3 +16,15 @@ pub struct WebAuthConfigDto {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub userfile: Option<String>,
|
||||
}
|
||||
|
||||
impl Default for WebAuthConfigDto {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
enabled: default_as_true(),
|
||||
issuer: String::new(),
|
||||
secret: String::new(),
|
||||
token_ttl_mins: default_token_ttl_mins(),
|
||||
userfile: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,13 +30,13 @@ impl FromStr for MsgKind {
|
||||
type Err = TuliproxError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, TuliproxError> {
|
||||
if s.eq("Info") {
|
||||
if s.eq_ignore_ascii_case("info") {
|
||||
Ok(Self::Info)
|
||||
} else if s.eq("Stats") {
|
||||
} else if s.eq_ignore_ascii_case("stats") {
|
||||
Ok(Self::Stats)
|
||||
} else if s.eq("Error") {
|
||||
} else if s.eq_ignore_ascii_case("error") {
|
||||
Ok(Self::Error)
|
||||
} else if s.eq("Watch") {
|
||||
} else if s.eq_ignore_ascii_case("watch") {
|
||||
Ok(Self::Watch)
|
||||
} else {
|
||||
create_tuliprox_error_result!(TuliproxErrorKind::Info, "Unknown MsgKind: {}", s)
|
||||
|
||||
Reference in New Issue
Block a user