766 lines
31 KiB
Rust
766 lines
31 KiB
Rust
use leptos::prelude::*;
|
|
use leptos::task::spawn_local;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct User {
|
|
pub id: String,
|
|
pub email: String,
|
|
pub name: String,
|
|
pub status: UserStatus,
|
|
pub roles: Vec<String>,
|
|
pub groups: Vec<String>,
|
|
pub created_at: String,
|
|
pub last_login: Option<String>,
|
|
pub mfa_enabled: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub enum UserStatus {
|
|
Active,
|
|
Inactive,
|
|
Suspended,
|
|
PendingVerification,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Role {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub permissions: Vec<String>,
|
|
pub parent_role: Option<String>,
|
|
pub children: Vec<String>,
|
|
pub level: u32,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Group {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub members: Vec<String>,
|
|
pub parent_group: Option<String>,
|
|
pub children: Vec<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Permission {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub resource: String,
|
|
pub action: String,
|
|
pub category: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AccessReviewCampaign {
|
|
pub id: String,
|
|
pub name: String,
|
|
pub description: String,
|
|
pub schedule: String,
|
|
pub reviewers: Vec<String>,
|
|
pub status: String,
|
|
pub due_date: String,
|
|
}
|
|
|
|
#[component]
|
|
pub fn UsersPage() -> impl IntoView {
|
|
let (active_tab, set_active_tab) = signal("users".to_string());
|
|
let (users, set_users) = signal(Vec::<User>::new());
|
|
let (roles, set_roles) = signal(Vec::<Role>::new());
|
|
let (groups, set_groups) = signal(Vec::<Group>::new());
|
|
let (permissions, set_permissions) = signal(Vec::<Permission>::new());
|
|
let (selected_user, set_selected_user) = signal(None::<User>);
|
|
let (show_user_modal, set_show_user_modal) = signal(false);
|
|
let (show_role_hierarchy, set_show_role_hierarchy) = signal(false);
|
|
let (show_bulk_operations, set_show_bulk_operations) = signal(false);
|
|
|
|
// Load initial data
|
|
Effect::new(move |_| {
|
|
spawn_local(async move {
|
|
// Mock data - in real app, fetch from API
|
|
let mock_users = vec![
|
|
User {
|
|
id: "u1".to_string(),
|
|
email: "admin@example.com".to_string(),
|
|
name: "System Admin".to_string(),
|
|
status: UserStatus::Active,
|
|
roles: vec!["admin".to_string()],
|
|
groups: vec!["admins".to_string()],
|
|
created_at: "2024-01-01".to_string(),
|
|
last_login: Some("2024-01-15".to_string()),
|
|
mfa_enabled: true,
|
|
},
|
|
User {
|
|
id: "u2".to_string(),
|
|
email: "user@example.com".to_string(),
|
|
name: "Regular User".to_string(),
|
|
status: UserStatus::Active,
|
|
roles: vec!["user".to_string()],
|
|
groups: vec!["users".to_string()],
|
|
created_at: "2024-01-10".to_string(),
|
|
last_login: Some("2024-01-14".to_string()),
|
|
mfa_enabled: false,
|
|
},
|
|
];
|
|
|
|
let mock_roles = vec![
|
|
Role {
|
|
id: "admin".to_string(),
|
|
name: "Administrator".to_string(),
|
|
description: "Full system access".to_string(),
|
|
permissions: vec!["*".to_string()],
|
|
parent_role: None,
|
|
children: vec!["operator".to_string()],
|
|
level: 0,
|
|
},
|
|
Role {
|
|
id: "operator".to_string(),
|
|
name: "Operator".to_string(),
|
|
description: "Infrastructure management".to_string(),
|
|
permissions: vec!["infra:*".to_string()],
|
|
parent_role: Some("admin".to_string()),
|
|
children: vec!["user".to_string()],
|
|
level: 1,
|
|
},
|
|
Role {
|
|
id: "user".to_string(),
|
|
name: "User".to_string(),
|
|
description: "Basic access".to_string(),
|
|
permissions: vec!["read:*".to_string()],
|
|
parent_role: Some("operator".to_string()),
|
|
children: vec![],
|
|
level: 2,
|
|
},
|
|
];
|
|
|
|
set_users.set(mock_users);
|
|
set_roles.set(mock_roles);
|
|
});
|
|
});
|
|
|
|
view! {
|
|
<div class="users-page h-full flex flex-col">
|
|
// Header
|
|
<div class="flex-none border-b border-base-300 p-4">
|
|
<div class="flex items-center justify-between">
|
|
<h1 class="text-3xl font-bold">"User Management"</h1>
|
|
<div class="flex gap-2">
|
|
<button
|
|
class="btn btn-outline btn-sm"
|
|
on:click=move |_| set_show_bulk_operations.set(true)
|
|
>
|
|
"Bulk Operations"
|
|
</button>
|
|
<button
|
|
class="btn btn-outline btn-sm"
|
|
on:click=move |_| set_show_role_hierarchy.set(true)
|
|
>
|
|
"Role Hierarchy"
|
|
</button>
|
|
<button
|
|
class="btn btn-primary btn-sm"
|
|
on:click=move |_| {
|
|
set_selected_user.set(None);
|
|
set_show_user_modal.set(true);
|
|
}
|
|
>
|
|
"Add User"
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
// Tab Navigation
|
|
<div class="tabs tabs-boxed mt-4">
|
|
<a
|
|
class={move || if active_tab.get() == "users" { "tab tab-active" } else { "tab" }}
|
|
on:click=move |_| set_active_tab.set("users".to_string())
|
|
>
|
|
"Users"
|
|
</a>
|
|
<a
|
|
class={move || if active_tab.get() == "roles" { "tab tab-active" } else { "tab" }}
|
|
on:click=move |_| set_active_tab.set("roles".to_string())
|
|
>
|
|
"Roles"
|
|
</a>
|
|
<a
|
|
class={move || if active_tab.get() == "groups" { "tab tab-active" } else { "tab" }}
|
|
on:click=move |_| set_active_tab.set("groups".to_string())
|
|
>
|
|
"Groups"
|
|
</a>
|
|
<a
|
|
class={move || if active_tab.get() == "permissions" { "tab tab-active" } else { "tab" }}
|
|
on:click=move |_| set_active_tab.set("permissions".to_string())
|
|
>
|
|
"Permissions"
|
|
</a>
|
|
<a
|
|
class={move || if active_tab.get() == "reviews" { "tab tab-active" } else { "tab" }}
|
|
on:click=move |_| set_active_tab.set("reviews".to_string())
|
|
>
|
|
"Access Reviews"
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
// Content Area
|
|
<div class="flex-1 overflow-hidden">
|
|
{move || match active_tab.get().as_str() {
|
|
"users" => view! { <UsersTab users=users set_selected_user=set_selected_user set_show_user_modal=set_show_user_modal /> }.into(),
|
|
"roles" => view! { <RolesTab roles=roles /> }.into(),
|
|
"groups" => view! { <GroupsTab groups=groups /> }.into(),
|
|
"permissions" => view! { <PermissionsTab permissions=permissions /> }.into(),
|
|
"reviews" => view! { <AccessReviewsTab /> }.into(),
|
|
_ => view! { <div>"Unknown tab"</div> }.into(),
|
|
}}
|
|
</div>
|
|
|
|
// User Modal
|
|
{move || if show_user_modal.get() {
|
|
view! {
|
|
<UserModal
|
|
user=selected_user
|
|
show=show_user_modal
|
|
set_show=set_show_user_modal
|
|
roles=roles.get()
|
|
groups=groups.get()
|
|
/>
|
|
}.into()
|
|
} else {
|
|
view! { <div></div> }.into()
|
|
}}
|
|
|
|
// Role Hierarchy Modal
|
|
{move || if show_role_hierarchy.get() {
|
|
view! {
|
|
<RoleHierarchyModal
|
|
roles=roles.get()
|
|
show=show_role_hierarchy
|
|
set_show=set_show_role_hierarchy
|
|
/>
|
|
}.into()
|
|
} else {
|
|
view! { <div></div> }.into()
|
|
}}
|
|
|
|
// Bulk Operations Modal
|
|
{move || if show_bulk_operations.get() {
|
|
view! {
|
|
<BulkOperationsModal
|
|
show=show_bulk_operations
|
|
set_show=set_show_bulk_operations
|
|
/>
|
|
}.into()
|
|
} else {
|
|
view! { <div></div> }.into()
|
|
}}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn UsersTab(
|
|
users: ReadSignal<Vec<User>>,
|
|
set_selected_user: WriteSignal<Option<User>>,
|
|
set_show_user_modal: WriteSignal<bool>,
|
|
) -> impl IntoView {
|
|
let (search_term, set_search_term) = signal(String::new());
|
|
let (status_filter, set_status_filter) = signal("all".to_string());
|
|
|
|
let filtered_users = Memo::new(move |_| {
|
|
let search = search_term.get().to_lowercase();
|
|
let status = status_filter.get();
|
|
|
|
users.get().into_iter().filter(|user| {
|
|
let matches_search = search.is_empty() ||
|
|
user.name.to_lowercase().contains(&search) ||
|
|
user.email.to_lowercase().contains(&search);
|
|
|
|
let matches_status = status == "all" ||
|
|
format!("{:?}", user.status).to_lowercase() == status;
|
|
|
|
matches_search && matches_status
|
|
}).collect::<Vec<_>>()
|
|
});
|
|
|
|
view! {
|
|
<div class="p-4 h-full flex flex-col">
|
|
// Search and Filters
|
|
<div class="flex-none mb-4 flex gap-4 items-center">
|
|
<div class="flex-1">
|
|
<input
|
|
type="text"
|
|
placeholder="Search users..."
|
|
class="input input-bordered w-full"
|
|
prop:value=move || search_term.get()
|
|
on:input=move |ev| set_search_term.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
<select
|
|
class="select select-bordered"
|
|
on:change=move |ev| set_status_filter.set(event_target_value(&ev))
|
|
>
|
|
<option value="all">"All Status"</option>
|
|
<option value="active">"Active"</option>
|
|
<option value="inactive">"Inactive"</option>
|
|
<option value="suspended">"Suspended"</option>
|
|
<option value="pendingverification">"Pending"</option>
|
|
</select>
|
|
</div>
|
|
|
|
// Users Table
|
|
<div class="flex-1 overflow-auto">
|
|
<table class="table table-zebra w-full">
|
|
<thead>
|
|
<tr>
|
|
<th>"Name"</th>
|
|
<th>"Email"</th>
|
|
<th>"Status"</th>
|
|
<th>"Roles"</th>
|
|
<th>"MFA"</th>
|
|
<th>"Last Login"</th>
|
|
<th>"Actions"</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{move || {
|
|
filtered_users.get().into_iter().map(|user| {
|
|
let user_clone = user.clone();
|
|
let user_clone2 = user.clone();
|
|
view! {
|
|
<tr>
|
|
<td class="font-medium">{&user.name}</td>
|
|
<td>{&user.email}</td>
|
|
<td>
|
|
<div class={format!("badge {}",
|
|
match user.status {
|
|
UserStatus::Active => "badge-success",
|
|
UserStatus::Inactive => "badge-warning",
|
|
UserStatus::Suspended => "badge-error",
|
|
UserStatus::PendingVerification => "badge-info",
|
|
}
|
|
)}>
|
|
{format!("{:?}", user.status)}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<div class="flex gap-1 flex-wrap">
|
|
{user.roles.into_iter().map(|role| {
|
|
view! {
|
|
<div class="badge badge-outline badge-sm">{role}</div>
|
|
}
|
|
}).collect::<Vec<_>>()}
|
|
</div>
|
|
</td>
|
|
<td>
|
|
{if user.mfa_enabled {
|
|
view! { <div class="badge badge-success">"Enabled"</div> }
|
|
} else {
|
|
view! { <div class="badge badge-warning">"Disabled"</div> }
|
|
}}
|
|
</td>
|
|
<td class="text-sm text-base-content/70">
|
|
{user.last_login.unwrap_or_else(|| "Never".to_string())}
|
|
</td>
|
|
<td>
|
|
<div class="flex gap-2">
|
|
<button
|
|
class="btn btn-ghost btn-xs"
|
|
on:click=move |_| {
|
|
set_selected_user.set(Some(user_clone.clone()));
|
|
set_show_user_modal.set(true);
|
|
}
|
|
>
|
|
"Edit"
|
|
</button>
|
|
<button class="btn btn-ghost btn-xs text-error">
|
|
"Delete"
|
|
</button>
|
|
</div>
|
|
</td>
|
|
</tr>
|
|
}
|
|
}).collect::<Vec<_>>()
|
|
}}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn RolesTab(roles: ReadSignal<Vec<Role>>) -> impl IntoView {
|
|
view! {
|
|
<div class="p-4 h-full">
|
|
<div class="mb-4">
|
|
<button class="btn btn-primary btn-sm">"Add Role"</button>
|
|
</div>
|
|
|
|
<div class="grid gap-4">
|
|
{move || {
|
|
roles.get().into_iter().map(|role| {
|
|
view! {
|
|
<div class="card bg-base-200 p-4">
|
|
<div class="flex items-start justify-between">
|
|
<div>
|
|
<h3 class="font-semibold">{&role.name}</h3>
|
|
<p class="text-sm text-base-content/70 mt-1">{&role.description}</p>
|
|
<div class="mt-2">
|
|
<span class="text-xs font-medium">"Permissions: "</span>
|
|
<div class="flex gap-1 flex-wrap mt-1">
|
|
{role.permissions.into_iter().map(|perm| {
|
|
view! {
|
|
<div class="badge badge-outline badge-xs">{perm}</div>
|
|
}
|
|
}).collect::<Vec<_>>()}
|
|
</div>
|
|
</div>
|
|
{role.parent_role.as_ref().map(|parent| {
|
|
view! {
|
|
<div class="mt-2 text-xs">
|
|
"Inherits from: "
|
|
<span class="badge badge-ghost badge-xs">{parent}</span>
|
|
</div>
|
|
}
|
|
})}
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="btn btn-ghost btn-xs">"Edit"</button>
|
|
<button class="btn btn-ghost btn-xs text-error">"Delete"</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}).collect::<Vec<_>>()
|
|
}}
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn GroupsTab(groups: ReadSignal<Vec<Group>>) -> impl IntoView {
|
|
view! {
|
|
<div class="p-4">
|
|
<div class="text-center text-base-content/50 mt-8">
|
|
"Group management coming soon..."
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn PermissionsTab(permissions: ReadSignal<Vec<Permission>>) -> impl IntoView {
|
|
view! {
|
|
<div class="p-4">
|
|
<PermissionMatrix />
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn AccessReviewsTab() -> impl IntoView {
|
|
view! {
|
|
<div class="p-4">
|
|
<div class="mb-4">
|
|
<button class="btn btn-primary btn-sm">"Create Campaign"</button>
|
|
</div>
|
|
<div class="text-center text-base-content/50 mt-8">
|
|
"Access review campaigns coming soon..."
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn UserModal(
|
|
user: ReadSignal<Option<User>>,
|
|
show: ReadSignal<bool>,
|
|
set_show: WriteSignal<bool>,
|
|
roles: Vec<Role>,
|
|
groups: Vec<Group>,
|
|
) -> impl IntoView {
|
|
let (name, set_name) = signal(String::new());
|
|
let (email, set_email) = signal(String::new());
|
|
let (selected_roles, set_selected_roles) = signal(Vec::<String>::new());
|
|
|
|
Effect::new(move |_| {
|
|
if let Some(u) = user.get() {
|
|
set_name.set(u.name);
|
|
set_email.set(u.email);
|
|
set_selected_roles.set(u.roles);
|
|
} else {
|
|
set_name.set(String::new());
|
|
set_email.set(String::new());
|
|
set_selected_roles.set(Vec::new());
|
|
}
|
|
});
|
|
|
|
view! {
|
|
<div class={move || if show.get() { "modal modal-open" } else { "modal" }}>
|
|
<div class="modal-box w-11/12 max-w-2xl">
|
|
<h3 class="font-bold text-lg">
|
|
{move || if user.get().is_some() { "Edit User" } else { "Add User" }}
|
|
</h3>
|
|
|
|
<div class="py-4 space-y-4">
|
|
<div>
|
|
<label class="label">"Name"</label>
|
|
<input
|
|
type="text"
|
|
class="input input-bordered w-full"
|
|
prop:value=move || name.get()
|
|
on:input=move |ev| set_name.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="label">"Email"</label>
|
|
<input
|
|
type="email"
|
|
class="input input-bordered w-full"
|
|
prop:value=move || email.get()
|
|
on:input=move |ev| set_email.set(event_target_value(&ev))
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label class="label">"Roles"</label>
|
|
<div class="space-y-2">
|
|
{roles.into_iter().map(|role| {
|
|
let role_id = role.id.clone();
|
|
view! {
|
|
<label class="label cursor-pointer justify-start gap-2">
|
|
<input
|
|
type="checkbox"
|
|
class="checkbox"
|
|
prop:checked=move || selected_roles.get().contains(&role_id)
|
|
on:change=move |_| {
|
|
let mut current = selected_roles.get();
|
|
if current.contains(&role_id) {
|
|
current.retain(|r| r != &role_id);
|
|
} else {
|
|
current.push(role_id.clone());
|
|
}
|
|
set_selected_roles.set(current);
|
|
}
|
|
/>
|
|
<span class="label-text">{&role.name}</span>
|
|
<span class="text-xs text-base-content/50">{"("}{&role.description}{")"}</span>
|
|
</label>
|
|
}
|
|
}).collect::<Vec<_>>()}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button
|
|
class="btn"
|
|
on:click=move |_| set_show.set(false)
|
|
>
|
|
"Cancel"
|
|
</button>
|
|
<button class="btn btn-primary">"Save"</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn RoleHierarchyModal(
|
|
roles: Vec<Role>,
|
|
show: ReadSignal<bool>,
|
|
set_show: WriteSignal<bool>,
|
|
) -> impl IntoView {
|
|
view! {
|
|
<div class={move || if show.get() { "modal modal-open" } else { "modal" }}>
|
|
<div class="modal-box w-11/12 max-w-4xl">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="font-bold text-lg">"Role Hierarchy"</h3>
|
|
<button
|
|
class="btn btn-ghost btn-sm"
|
|
on:click=move |_| set_show.set(false)
|
|
>
|
|
"✕"
|
|
</button>
|
|
</div>
|
|
|
|
<div class="py-4">
|
|
<RoleHierarchyViewer roles=roles />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn RoleHierarchyViewer(roles: Vec<Role>) -> impl IntoView {
|
|
// Sort roles by level for hierarchy display
|
|
let mut sorted_roles = roles;
|
|
sorted_roles.sort_by_key(|r| r.level);
|
|
|
|
view! {
|
|
<div class="space-y-4">
|
|
{sorted_roles.into_iter().map(|role| {
|
|
let indent = role.level * 24; // 24px per level
|
|
view! {
|
|
<div class="flex items-center" style={format!("margin-left: {}px", indent)}>
|
|
<div class="w-4 h-4 border-2 border-primary rounded mr-3"></div>
|
|
<div class="flex-1">
|
|
<div class="font-medium">{&role.name}</div>
|
|
<div class="text-sm text-base-content/70">{&role.description}</div>
|
|
<div class="flex gap-1 mt-1">
|
|
{role.permissions.into_iter().take(3).map(|perm| {
|
|
view! {
|
|
<div class="badge badge-outline badge-xs">{perm}</div>
|
|
}
|
|
}).collect::<Vec<_>>()}
|
|
{if role.permissions.len() > 3 {
|
|
view! {
|
|
<div class="badge badge-ghost badge-xs">
|
|
{"+"}{role.permissions.len() - 3}{" more"}
|
|
</div>
|
|
}.into()
|
|
} else {
|
|
view! { <div></div> }.into()
|
|
}}
|
|
</div>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<button class="btn btn-ghost btn-xs">"Edit"</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
}).collect::<Vec<_>>()}
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn PermissionMatrix() -> impl IntoView {
|
|
let resources = vec!["Users", "Roles", "Infrastructure", "Workflows", "Policies"];
|
|
let actions = vec!["Read", "Write", "Delete", "Execute", "Admin"];
|
|
let roles = vec!["Admin", "Operator", "User"];
|
|
|
|
view! {
|
|
<div class="overflow-auto">
|
|
<table class="table table-zebra table-compact w-full">
|
|
<thead>
|
|
<tr>
|
|
<th>"Resource"</th>
|
|
<th>"Action"</th>
|
|
{roles.iter().map(|role| {
|
|
let role = role.clone();
|
|
view! { <th>{role}</th> }
|
|
}).collect::<Vec<_>>()}
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{resources.into_iter().flat_map(|resource| {
|
|
actions.iter().map({
|
|
let resource = resource.clone();
|
|
move |action| {
|
|
view! {
|
|
<tr>
|
|
<td>{&resource}</td>
|
|
<td>{action}</td>
|
|
{roles.iter().map(|role| {
|
|
let role = role.clone();
|
|
view! {
|
|
<td>
|
|
<input type="checkbox" class="checkbox checkbox-xs" />
|
|
</td>
|
|
}
|
|
}).collect::<Vec<_>>()}
|
|
</tr>
|
|
}
|
|
}
|
|
}).collect::<Vec<_>>()
|
|
}).collect::<Vec<_>>()}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
#[component]
|
|
fn BulkOperationsModal(
|
|
show: ReadSignal<bool>,
|
|
set_show: WriteSignal<bool>,
|
|
) -> impl IntoView {
|
|
let (operation_type, set_operation_type) = signal("import".to_string());
|
|
|
|
view! {
|
|
<div class={move || if show.get() { "modal modal-open" } else { "modal" }}>
|
|
<div class="modal-box">
|
|
<div class="flex justify-between items-center mb-4">
|
|
<h3 class="font-bold text-lg">"Bulk Operations"</h3>
|
|
<button
|
|
class="btn btn-ghost btn-sm"
|
|
on:click=move |_| set_show.set(false)
|
|
>
|
|
"✕"
|
|
</button>
|
|
</div>
|
|
|
|
<div class="py-4 space-y-4">
|
|
<div>
|
|
<label class="label">"Operation Type"</label>
|
|
<select
|
|
class="select select-bordered w-full"
|
|
on:change=move |ev| set_operation_type.set(event_target_value(&ev))
|
|
>
|
|
<option value="import">"Import Users"</option>
|
|
<option value="export">"Export Users"</option>
|
|
<option value="bulk_update">"Bulk Update"</option>
|
|
<option value="bulk_delete">"Bulk Delete"</option>
|
|
</select>
|
|
</div>
|
|
|
|
{move || match operation_type.get().as_str() {
|
|
"import" => view! {
|
|
<div>
|
|
<label class="label">"CSV File"</label>
|
|
<input type="file" class="file-input file-input-bordered w-full" accept=".csv" />
|
|
<div class="text-sm text-base-content/70 mt-2">
|
|
"Expected format: name,email,roles (comma-separated)"
|
|
</div>
|
|
</div>
|
|
}.into(),
|
|
"export" => view! {
|
|
<div>
|
|
<label class="label cursor-pointer">
|
|
<input type="checkbox" class="checkbox" />
|
|
<span class="label-text">"Include sensitive data"</span>
|
|
</label>
|
|
<label class="label cursor-pointer">
|
|
<input type="checkbox" class="checkbox" />
|
|
<span class="label-text">"Include inactive users"</span>
|
|
</label>
|
|
</div>
|
|
}.into(),
|
|
_ => view! { <div>"Operation settings..."</div> }.into(),
|
|
}}
|
|
</div>
|
|
|
|
<div class="modal-action">
|
|
<button
|
|
class="btn"
|
|
on:click=move |_| set_show.set(false)
|
|
>
|
|
"Cancel"
|
|
</button>
|
|
<button class="btn btn-primary">"Execute"</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|