mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-20 23:02:34 +02:00
@@ -119,6 +119,24 @@
|
||||
],
|
||||
"path": "m7 14 5-5 5 5z"
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"SortAsc"
|
||||
],
|
||||
"path": "m20 12-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8z"
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"SortDesc"
|
||||
],
|
||||
"path": "M 20,12 18.59,13.41 13,7.83 V 20 H 11 V 7.83 L 5.42,13.42 4,12 12,4 Z"
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"Sort"
|
||||
],
|
||||
"path": "M 16.222224,17.566656 V 9.7777638 H 14 V 17.566656 H 10.666667 L 15.11111,22 19.555557,17.566656 Z M 8.444443,2 4,6.4333018 H 7.3333333 V 14.222194 H 9.555557 V 6.4333018 h 3.333333 z"
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
"CloudDownload"
|
||||
|
||||
@@ -34,6 +34,17 @@
|
||||
background-color: var(--table-header-background-color);
|
||||
box-shadow: inset 0 -1px 0 var(--border-color);
|
||||
}
|
||||
.tp__table-header {
|
||||
display: flex;
|
||||
flex-flow: row;
|
||||
gap: var(--gap-small);
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
.svg-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ use yew::platform::spawn_local;
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind};
|
||||
use shared::model::{ConfigInputAliasDto, ConfigInputDto};
|
||||
use shared::model::{ConfigInputAliasDto, ConfigInputDto, SortOrder};
|
||||
use crate::app::components::menu_item::MenuItem;
|
||||
use crate::model::DialogResult;
|
||||
use crate::services::{DialogService};
|
||||
@@ -182,15 +182,26 @@ pub fn InputTable(props: &InputTableProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let is_sortable = Callback::<usize, bool>::from(move |_col| {
|
||||
false
|
||||
});
|
||||
|
||||
let on_sort = Callback::<Option<(usize, SortOrder)>, ()>::from(move |_args| {
|
||||
});
|
||||
|
||||
let table_definition = {
|
||||
let render_header_cell_cb = render_header_cell.clone();
|
||||
let render_data_cell_cb = render_data_cell.clone();
|
||||
let is_sortable = is_sortable.clone();
|
||||
let on_sort = on_sort.clone();
|
||||
let num_cols = HEADERS.len();
|
||||
use_memo(props.inputs.clone(), |inputs|
|
||||
inputs.as_ref().map(|list|
|
||||
Rc::new(TableDefinition::<InputRow> {
|
||||
items: if list.is_empty() {None} else {Some(Rc::new(list.clone()))},
|
||||
num_cols,
|
||||
is_sortable,
|
||||
on_sort,
|
||||
render_header_cell: render_header_cell_cb,
|
||||
render_data_cell: render_data_cell_cb,
|
||||
}))
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
use crate::app::components::menu_item::MenuItem;
|
||||
use crate::app::components::popup_menu::PopupMenu;
|
||||
use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, FilterView, PlaylistMappings, PlaylistProcessing, RevealContent, Table, TableDefinition, TargetOptions, TargetOutput, TargetRename, TargetSort, TargetWatch};
|
||||
use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, FilterView, PlaylistMappings,
|
||||
PlaylistProcessing, RevealContent, Table, TableDefinition, TargetOptions, TargetOutput, TargetRename, TargetSort, TargetWatch};
|
||||
use crate::hooks::use_service_context;
|
||||
use crate::model::DialogResult;
|
||||
use crate::services::DialogService;
|
||||
use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind};
|
||||
use shared::model::ConfigTargetDto;
|
||||
use shared::model::{ConfigTargetDto, SortOrder};
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
@@ -109,16 +110,27 @@ pub fn TargetTable(props: &TargetTableProps) -> Html {
|
||||
})
|
||||
};
|
||||
|
||||
let is_sortable = Callback::<usize, bool>::from(move |_col| {
|
||||
false
|
||||
});
|
||||
|
||||
let on_sort = Callback::<Option<(usize, SortOrder)>, ()>::from(move |_args| {
|
||||
});
|
||||
|
||||
let table_definition = {
|
||||
// first register for config update
|
||||
let render_header_cell_cb = render_header_cell.clone();
|
||||
let render_data_cell_cb = render_data_cell.clone();
|
||||
let is_sortable = is_sortable.clone();
|
||||
let on_sort = on_sort.clone();
|
||||
let num_cols = HEADERS.len();
|
||||
use_memo(props.targets.clone(), move |targets| {
|
||||
targets.as_ref().map(|list|
|
||||
Rc::new(TableDefinition::<ConfigTargetDto> {
|
||||
items: if list.is_empty() {None} else {Some(Rc::new(list.clone()))},
|
||||
num_cols,
|
||||
is_sortable,
|
||||
on_sort,
|
||||
render_header_cell: render_header_cell_cb,
|
||||
render_data_cell: render_data_cell_cb,
|
||||
}))
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
use std::rc::Rc;
|
||||
use yew::prelude::*;
|
||||
use crate::app::components::{NoContent};
|
||||
use crate::app::components::{NoContent, AppIcon};
|
||||
use shared::model::SortOrder;
|
||||
|
||||
#[derive(Properties, PartialEq)]
|
||||
pub struct TableDefinition<T: PartialEq + Clone> {
|
||||
pub items: Option<Rc<Vec<Rc<T>>>>,
|
||||
pub num_cols: usize,
|
||||
// Return true if a given column is sortable
|
||||
pub is_sortable: Callback<usize, bool>,
|
||||
pub render_header_cell: Callback<usize, Html>,
|
||||
pub render_data_cell: Callback<(usize, usize, Rc<T>), Html>,
|
||||
#[prop_or_else(Callback::noop)]
|
||||
pub on_sort: Callback<Option<(usize, SortOrder)>, ()>,
|
||||
}
|
||||
|
||||
#[derive(Properties, Clone, PartialEq)]
|
||||
@@ -20,10 +25,34 @@ pub fn Table<T: PartialEq + Clone>(props: &TableProps<T>) -> Html {
|
||||
let TableDefinition {
|
||||
items,
|
||||
num_cols,
|
||||
is_sortable,
|
||||
on_sort,
|
||||
render_header_cell,
|
||||
render_data_cell,
|
||||
} = &*props.definition;
|
||||
|
||||
// Local sort state: None = neutral; Some((col, order)) = sorted column and order
|
||||
let sort_state = use_state::<Option<(usize, SortOrder)>, _>(|| None);
|
||||
|
||||
let on_header_click = {
|
||||
let sort_state = sort_state.clone();
|
||||
let is_sortable = is_sortable.clone();
|
||||
let on_sort = on_sort.clone();
|
||||
Callback::from(move |col_index: usize| {
|
||||
if !is_sortable.emit(col_index) {
|
||||
return;
|
||||
}
|
||||
let state = match *sort_state {
|
||||
Some((c, SortOrder::Asc)) if c == col_index => Some((col_index, SortOrder::Desc)),
|
||||
Some((c, SortOrder::Desc)) if c == col_index => None,
|
||||
_ => Some((col_index, SortOrder::Asc)),
|
||||
};
|
||||
|
||||
sort_state.set(state);
|
||||
on_sort.emit(state);
|
||||
})
|
||||
};
|
||||
|
||||
html! {
|
||||
<div class={"tp__table"}>
|
||||
<div class={"tp__table__container"}>
|
||||
@@ -32,8 +61,49 @@ pub fn Table<T: PartialEq + Clone>(props: &TableProps<T>) -> Html {
|
||||
<tr>
|
||||
{
|
||||
for (0..*num_cols).map(|col_index| {
|
||||
// Determine if this column is sortable
|
||||
let sortable = is_sortable.emit(col_index);
|
||||
|
||||
// Decide which icon to show for this column
|
||||
let icon_html = if sortable {
|
||||
match *sort_state {
|
||||
Some((c, SortOrder::Asc)) if c == col_index => html!{ <AppIcon name="SortAsc"/> },
|
||||
Some((c, SortOrder::Desc)) if c == col_index => html!{ <AppIcon name="SortDesc"/> },
|
||||
_ => html!{ <AppIcon name="Sort"/> }, // neutral
|
||||
}
|
||||
} else {
|
||||
html!{}
|
||||
};
|
||||
|
||||
// Click handler per column
|
||||
let on_click_col = {
|
||||
let on_header_click = on_header_click.clone();
|
||||
Callback::from(move |_| on_header_click.emit(col_index))
|
||||
};
|
||||
|
||||
html!{
|
||||
<th> {render_header_cell.emit(col_index)}</th>
|
||||
<th
|
||||
class={classes!(
|
||||
if sortable { Some("tp__table__th--sortable") } else { None }
|
||||
)}
|
||||
onclick={if sortable { Some(on_click_col) } else { None }}
|
||||
role={if sortable { Some("button") } else { None }}
|
||||
aria-sort={
|
||||
if let Some((c, order)) = &*sort_state {
|
||||
if *c == col_index {
|
||||
Some(match order {
|
||||
SortOrder::Asc => "ascending",
|
||||
SortOrder::Desc => "descending",
|
||||
}.to_string())
|
||||
} else { Some("none".to_string()) }
|
||||
} else { Some("none".to_string()) }
|
||||
}
|
||||
>
|
||||
<span class="tp__table-header">
|
||||
{render_header_cell.emit(col_index)}
|
||||
{icon_html}
|
||||
</span>
|
||||
</th>
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ pub fn UserlistList() -> Html {
|
||||
let userlist_body = if let Some(data) = userlist_ctx.get_users().as_ref() {
|
||||
html! {
|
||||
<div class="tp__userlist-list__user">
|
||||
<UserTable targets={if data.is_empty() {None} else {Some(data.clone())}} />
|
||||
<UserTable users={if data.is_empty() {None} else {Some(data.clone())}} />
|
||||
</div>
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
use std::borrow::Cow;
|
||||
use crate::app::components::menu_item::MenuItem;
|
||||
use crate::app::components::popup_menu::PopupMenu;
|
||||
use crate::app::components::{convert_bool_to_chip_style, AppIcon, Chip, HideContent, MaxConnections,
|
||||
ProxyTypeView, RevealContent, Table, TableDefinition, UserStatus,
|
||||
UserlistContext, UserlistPage};
|
||||
use crate::app::context::TargetUser;
|
||||
use crate::model::DialogResult;
|
||||
use crate::services::DialogService;
|
||||
use shared::error::{create_tuliprox_error_result, TuliproxError, TuliproxErrorKind};
|
||||
use shared::model::SortOrder;
|
||||
use shared::utils::{unix_ts_to_str, Substring};
|
||||
use std::fmt::Display;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
use yew::platform::spawn_local;
|
||||
use yew::prelude::*;
|
||||
use yew_i18n::use_translation;
|
||||
use shared::utils::{unix_ts_to_str, Substring};
|
||||
use crate::app::context::TargetUser;
|
||||
use crate::services::DialogService;
|
||||
|
||||
const HEADERS: [&str; 15] = [
|
||||
"TABLE.EMPTY",
|
||||
@@ -33,9 +35,26 @@ const HEADERS: [&str; 15] = [
|
||||
"TABLE.COMMENT",
|
||||
];
|
||||
|
||||
fn get_cell_value(user: &TargetUser, col: usize) -> Cow<'_, str> {
|
||||
match col {
|
||||
1 => Cow::Owned(user.credentials.is_active().to_string()),
|
||||
2 => Cow::Owned(user.credentials.status.as_ref().map_or_else(String::new, ToString::to_string)),
|
||||
3 => Cow::Borrowed(user.target.as_str()),
|
||||
4 => Cow::Borrowed(user.credentials.username.as_str()),
|
||||
7 => Cow::Owned(user.credentials.proxy.to_string()),
|
||||
8 => Cow::Owned(user.credentials.server.as_ref().map_or_else(String::new, Clone::clone)),
|
||||
_ => Cow::Owned(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn is_col_sortable(col: usize) -> bool {
|
||||
matches!(col, 1 | 2 | 3 | 4 | 7 | 8)
|
||||
}
|
||||
|
||||
|
||||
#[derive(Properties, PartialEq, Clone)]
|
||||
pub struct UserTableProps {
|
||||
pub targets: Option<Rc<Vec<Rc<TargetUser>>>>,
|
||||
pub users: Option<Rc<Vec<Rc<TargetUser>>>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
@@ -46,6 +65,16 @@ pub fn UserTable(props: &UserTableProps) -> Html {
|
||||
let popup_anchor_ref = use_state(|| None::<web_sys::Element>);
|
||||
let popup_is_open = use_state(|| false);
|
||||
let selected_dto = use_state(|| None::<Rc<TargetUser>>);
|
||||
let user_list = use_state(|| props.users.clone());
|
||||
|
||||
{
|
||||
let user_list = user_list.clone();
|
||||
let users = props.users.clone();
|
||||
use_effect_with(users, move |users| {
|
||||
user_list.set(users.clone());
|
||||
|| ()
|
||||
});
|
||||
}
|
||||
|
||||
let handle_popup_close = {
|
||||
let set_is_open = popup_is_open.clone();
|
||||
@@ -105,37 +134,69 @@ pub fn UserTable(props: &UserTableProps) -> Html {
|
||||
2 => html! { <UserStatus status={ dto.credentials.status } /> },
|
||||
3 => html! { dto.target.as_str() },
|
||||
4 => html! { dto.credentials.username.as_str() },
|
||||
5 => html! { <HideContent content={&dto.credentials.password.to_string()}></HideContent> },
|
||||
5 => html! { <HideContent content={dto.credentials.password.to_string()}></HideContent> },
|
||||
6 => html! { dto.credentials.token.as_ref().map_or_else(|| html!{}, |token| html! { <HideContent content={token.to_string()}></HideContent>}) },
|
||||
7 => html! {<ProxyTypeView value={dto.credentials.proxy} /> },
|
||||
8 => dto.credentials.server.as_ref().map_or_else(|| html! {}, |s| html! { s } ),
|
||||
8 => dto.credentials.server.as_ref().map_or_else(|| html! {}, |s| html! { s }),
|
||||
9 => html! { <MaxConnections value={dto.credentials.max_connections} /> },
|
||||
10 => html! { <Chip class={ convert_bool_to_chip_style(dto.credentials.ui_enabled ) }
|
||||
label={if dto.credentials.ui_enabled {translator.t("LABEL.ENABLED")} else { translator.t("LABEL.DISABLED")} }
|
||||
/> },
|
||||
11 => dto.credentials.epg_timeshift.as_ref().map_or_else(|| html! {}, |s| html! { s } ),
|
||||
11 => dto.credentials.epg_timeshift.as_ref().map_or_else(|| html! {}, |s| html! { s }),
|
||||
12 => dto.credentials.created_at.as_ref().and_then(|ts| unix_ts_to_str(*ts))
|
||||
.map(|s| html! { { s } }).unwrap_or_else(|| html! { <AppIcon name="Unlimited" /> }),
|
||||
.map(|s| html! { { s } }).unwrap_or_else(|| html! { <AppIcon name="Unlimited" /> }),
|
||||
13 => dto.credentials.exp_date.as_ref().and_then(|ts| unix_ts_to_str(*ts))
|
||||
.map(|s| html! { { s } }).unwrap_or_else(|| html! { <AppIcon name="Unlimited" /> }),
|
||||
.map(|s| html! { { s } }).unwrap_or_else(|| html! { <AppIcon name="Unlimited" /> }),
|
||||
14 => dto.credentials.comment.as_ref()
|
||||
.map_or_else(|| html!{},
|
||||
|comment|html! { <RevealContent preview={Some(html! {comment.substring(0, 50)})}>{comment}</RevealContent> }),
|
||||
.map_or_else(|| html! {},
|
||||
|comment| html! { <RevealContent preview={Some(html! {comment.substring(0, 50)})}>{comment}</RevealContent> }),
|
||||
_ => html! {""},
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let is_sortable = Callback::<usize, bool>::from(move |col| {
|
||||
is_col_sortable(col)
|
||||
});
|
||||
|
||||
let on_sort = {
|
||||
let users = props.users.clone();
|
||||
let user_list = user_list.clone();
|
||||
Callback::<Option<(usize, SortOrder)>, ()>::from(move |args| {
|
||||
if let Some((col, order)) = args {
|
||||
if let Some(new_user_list)= users.as_ref() {
|
||||
let mut new_user_list = new_user_list.as_ref().clone();
|
||||
new_user_list.sort_by(|a, b| {
|
||||
let a_value = get_cell_value(a, col);
|
||||
let b_value = get_cell_value(b, col);
|
||||
match order {
|
||||
SortOrder::Asc => a_value.cmp(&b_value),
|
||||
SortOrder::Desc => b_value.cmp(&a_value),
|
||||
}
|
||||
});
|
||||
user_list.set(Some(Rc::new(new_user_list)));
|
||||
}
|
||||
} else {
|
||||
user_list.set(users.clone());
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
let table_definition = {
|
||||
// first register for config update
|
||||
let render_header_cell_cb = render_header_cell.clone();
|
||||
let render_data_cell_cb = render_data_cell.clone();
|
||||
let on_sort = on_sort.clone();
|
||||
let is_sortable = is_sortable.clone();
|
||||
let num_cols = HEADERS.len();
|
||||
use_memo(props.targets.clone(), move |targets| {
|
||||
let items = targets.clone();
|
||||
TableDefinition::<TargetUser> {
|
||||
let user_list_clone = user_list.clone();
|
||||
use_memo(user_list_clone.clone(), move |targets| {
|
||||
let items = if (*targets).as_ref().is_none_or(|l| l.is_empty()) {None} else {(**targets).clone()};
|
||||
TableDefinition::<TargetUser> {
|
||||
items,
|
||||
num_cols,
|
||||
is_sortable,
|
||||
on_sort,
|
||||
render_header_cell: render_header_cell_cb,
|
||||
render_data_cell: render_data_cell_cb,
|
||||
}
|
||||
@@ -158,8 +219,7 @@ pub fn UserTable(props: &UserTableProps) -> Html {
|
||||
ul_context.active_page.set(UserlistPage::Edit);
|
||||
}
|
||||
}
|
||||
TableAction::Refresh => {
|
||||
}
|
||||
TableAction::Refresh => {}
|
||||
TableAction::Delete => {
|
||||
let confirm = confirm.clone();
|
||||
let translator = translate.clone();
|
||||
|
||||
Reference in New Issue
Block a user