307 lines
13 KiB
Python
307 lines
13 KiB
Python
"""
|
||
manajemen_user.py
|
||
==================
|
||
Halaman khusus ADMIN untuk mengelola akun user.
|
||
|
||
Layout:
|
||
"Manajemen User"
|
||
"Daftar User" [+ Tambah User]
|
||
|
||
Tabel (st.dataframe asli -- sama seperti tabel di halaman lain):
|
||
No | Username | Nama | Role
|
||
|
||
Klik salah satu baris untuk memilihnya -> muncul tombol
|
||
"✏️ Edit User Ini" dan "🗑️ Hapus User Ini" di bawah tabel.
|
||
Kalau baris yang dipilih adalah akun sendiri yang sedang login,
|
||
tombol hapus tetap tampil tapi dinonaktifkan (disabled).
|
||
|
||
Semua aksi (tambah / edit / hapus) tampil sebagai POPUP (st.dialog),
|
||
mengikuti pola yang sama seperti panel "Perbarui Model (.pkl)" di
|
||
Perbandingan Konfigurasi: aksi dikonfirmasi di dalam popup -> pesan
|
||
sukses "dititipkan" ke session_state -> st.rerun() -> popup sukses
|
||
tampil sebentar -> halaman kembali ke tampilan awal (tabel ter-refresh,
|
||
pilihan baris ikut ter-reset).
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import streamlit as st
|
||
|
||
from utils import auth, ui_components
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Popup sukses (dipakai untuk semua jenis aksi: tambah/edit/hapus)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@st.dialog("✅ Berhasil")
|
||
def _dialog_sukses(pesan: str) -> None:
|
||
st.write(pesan)
|
||
if st.button("Oke", type="primary", width="stretch", key="btn_um_sukses_ok"):
|
||
st.rerun()
|
||
|
||
|
||
def _tutup_dan_refresh(pesan_sukses: str | None = None) -> None:
|
||
"""Bersihkan semua state popup yang sedang terbuka, titipkan pesan
|
||
sukses (kalau ada), lalu rerun -- persis pola di panel upload model."""
|
||
st.session_state.pop("_um_action", None)
|
||
if pesan_sukses:
|
||
st.session_state["_um_success_msg"] = pesan_sukses
|
||
# >>> FIX: naikkan versi key tabel supaya widget dataframe jadi
|
||
# "baru" (pilihan baris ter-reset) -- mencegah index nyasar kalau
|
||
# user yang tadi dipilih ternyata baru saja dihapus/berubah posisi.
|
||
st.session_state["_um_table_version"] = st.session_state.get("_um_table_version", 0) + 1
|
||
st.rerun()
|
||
|
||
|
||
def _batalkan_aksi() -> None:
|
||
"""
|
||
Callback untuk on_dismiss di st.dialog -- Streamlit otomatis
|
||
menjalankan ini kapan pun popup ditutup DENGAN CARA APAPUN, termasuk
|
||
klik tombol "x" atau klik di luar area popup (bukan cuma lewat
|
||
tombol "Batal" buatan kita).
|
||
|
||
>>> FIX BUG: sebelumnya, kalau popup ditutup lewat "x", session_state
|
||
["_um_action"] tidak ikut kebersihkan -- akibatnya popup yang sama
|
||
muncul lagi tiba-tiba di rerun berikutnya (misal cuma karena klik
|
||
baris tabel), bahkan masih nyangkut walau sudah pindah halaman lalu
|
||
balik lagi. Dengan on_dismiss, pembersihan ini sekarang terjadi
|
||
OTOMATIS untuk semua cara menutup popup, tidak cuma tombol Batal.
|
||
"""
|
||
st.session_state.pop("_um_action", None)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Popup: Tambah User
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@st.dialog("➕ Tambah User Baru", on_dismiss=_batalkan_aksi)
|
||
def _dialog_tambah_user() -> None:
|
||
username = st.text_input(
|
||
"Username", key="tu_username",
|
||
on_change=ui_components.strip_spasi_username, args=("tu_username",),
|
||
)
|
||
nama = st.text_input("Nama tampilan", key="tu_nama")
|
||
role = st.selectbox("Role", options=["pemilik", "admin"], key="tu_role")
|
||
password = st.text_input("Password awal", type="password", placeholder="Wajib ada huruf & angka", key="tu_password")
|
||
konfirmasi = st.text_input("Konfirmasi password", type="password", key="tu_konfirmasi")
|
||
|
||
col1, col2 = st.columns(2)
|
||
if col1.button("Simpan", type="primary", width="stretch", key="btn_tu_simpan"):
|
||
if password != konfirmasi:
|
||
st.error("Password dan konfirmasinya tidak sama.")
|
||
else:
|
||
berhasil, pesan = auth.add_user(username=username, password=password, role=role, nama=nama)
|
||
if berhasil:
|
||
_tutup_dan_refresh(pesan)
|
||
else:
|
||
st.error(pesan)
|
||
if col2.button("Batal", width="stretch", key="btn_tu_batal"):
|
||
_tutup_dan_refresh()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Popup: Edit User (nama + role + password baru, digabung jadi satu)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@st.dialog("✏️ Edit User", on_dismiss=_batalkan_aksi)
|
||
def _dialog_edit_user(username: str, nama_lama: str, role_lama: str) -> None:
|
||
# >>> FIX v2: pendekatan CSS :has(input:disabled) sebelumnya tidak
|
||
# match (struktur HTML Streamlit versi ini beda dari dugaan awal).
|
||
# Sekarang dibungkus st.container(key=...) supaya dapat class CSS
|
||
# pasti (".st-key-eu_username_wrapper"), lalu SEMUA label di dalam
|
||
# wrapper itu dipaksa warnanya normal -- tidak bergantung tebakan
|
||
# struktur disabled/aria-disabled Streamlit lagi.
|
||
st.markdown(
|
||
"""
|
||
<style>
|
||
.st-key-eu_username_wrapper label p,
|
||
.st-key-eu_username_wrapper label {
|
||
color: inherit !important;
|
||
opacity: 1 !important;
|
||
-webkit-text-fill-color: currentColor !important;
|
||
}
|
||
</style>
|
||
""",
|
||
unsafe_allow_html=True,
|
||
)
|
||
with st.container(key="eu_username_wrapper"):
|
||
st.text_input("Username", value=username, disabled=True, key=f"eu_username_display_{username}")
|
||
|
||
# >>> FIX: key widget disisipi 'username' supaya UNIK per-user.
|
||
# Sebelumnya key-nya statis ("eu_nama", "eu_role", dst) -- akibatnya
|
||
# kalau Anda edit user A lalu tutup, lalu edit user B, kolom Nama/
|
||
# Role/Password bisa ikut menampilkan sisa nilai dari user A
|
||
# (Streamlit menyimpan nilai widget berdasarkan key, bukan berdasarkan
|
||
# parameter value= yang dikirim ulang).
|
||
nama = st.text_input("Nama tampilan", value=nama_lama, key=f"eu_nama_{username}")
|
||
role = st.selectbox(
|
||
"Role", options=["admin", "pemilik"],
|
||
index=["admin", "pemilik"].index(role_lama), key=f"eu_role_{username}",
|
||
)
|
||
|
||
password_baru = st.text_input("Password baru", type="password", key=f"eu_password_{username}")
|
||
konfirmasi_baru = st.text_input("Konfirmasi password baru", type="password", key=f"eu_konfirmasi_{username}")
|
||
|
||
col1, col2 = st.columns(2)
|
||
if col1.button("Simpan", type="primary", width="stretch", key="btn_eu_simpan"):
|
||
# >>> FIX BUG: sebelumnya pakai 'return' di sini kalau password
|
||
# tidak cocok -- di Python, 'return' menghentikan SELURUH fungsi,
|
||
# jadi baris col2.button("Batal") di bawah ini ikut tidak sempat
|
||
# digambar pada render itu (makanya tombol Batal terlihat
|
||
# "hilang sesaat" setelah error muncul). Diganti if/else supaya
|
||
# fungsi tetap lanjut sampai akhir tanpa melewati Batal.
|
||
if password_baru and password_baru != konfirmasi_baru:
|
||
st.error("Password baru dan konfirmasinya tidak sama.")
|
||
else:
|
||
berhasil, pesan = auth.admin_edit_user(
|
||
username=username, nama=nama, role=role, new_password=password_baru or None,
|
||
)
|
||
if berhasil:
|
||
_tutup_dan_refresh(pesan)
|
||
else:
|
||
st.error(pesan)
|
||
if col2.button("Batal", width="stretch", key="btn_eu_batal"):
|
||
_tutup_dan_refresh()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Popup: Konfirmasi Hapus User
|
||
# ---------------------------------------------------------------------------
|
||
|
||
@st.dialog("🗑️ Hapus User", on_dismiss=_batalkan_aksi)
|
||
def _dialog_hapus_user(username: str) -> None:
|
||
st.warning(f"Yakin ingin menghapus user **{username}**? Tindakan ini tidak bisa dibatalkan.")
|
||
|
||
col1, col2 = st.columns(2)
|
||
if col1.button("Ya, Hapus", type="primary", width="stretch", key="btn_hu_ya"):
|
||
berhasil, pesan = auth.delete_user(username, requested_by=auth.get_current_username())
|
||
if berhasil:
|
||
_tutup_dan_refresh(pesan)
|
||
else:
|
||
st.error(pesan)
|
||
if col2.button("Batal", width="stretch", key="btn_hu_batal"):
|
||
_tutup_dan_refresh()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Halaman utama
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def render_manajemen_user() -> None:
|
||
# >>> Cuma SATU aturan CSS untuk icon toolbar tabel (mengecilkan ke
|
||
# ukuran normal 0.9rem). Versi sebelumnya sempat ada 2 aturan CSS
|
||
# icon yang saling bentrok (satu memperbesar 1.3x, satu memperkecil
|
||
# ke 0.9rem) -- sudah dibersihkan, cuma disisakan yang mengecilkan.
|
||
st.markdown(
|
||
"""
|
||
<style>
|
||
[data-testid="stElementToolbarButton"] svg {
|
||
width: 0.9rem !important;
|
||
height: 0.9rem !important;
|
||
}
|
||
[data-testid="stElementToolbarButton"] {
|
||
padding: 0.25rem !important;
|
||
}
|
||
</style>
|
||
""",
|
||
unsafe_allow_html=True,
|
||
)
|
||
|
||
# Pakai st.title() supaya konsisten dengan judul di halaman lain.
|
||
# CSS di bawah cuma mengecilkan margin bawahnya saja -- ini yang
|
||
# sebelumnya bikin jarak ke "Daftar User" terasa kelebaran.
|
||
st.markdown(
|
||
"""
|
||
<style>
|
||
div[data-testid="stAppViewContainer"] h1 {
|
||
margin-bottom: 0.4rem;
|
||
}
|
||
</style>
|
||
""",
|
||
unsafe_allow_html=True,
|
||
)
|
||
st.title("Manajemen User")
|
||
|
||
# Popup sukses (kalau ada pesan "dititipkan" dari aksi sebelum rerun)
|
||
pending_success = st.session_state.pop("_um_success_msg", None)
|
||
if pending_success:
|
||
_dialog_sukses(pending_success)
|
||
|
||
# Buka popup sesuai aksi yang sedang aktif (tersimpan di session_state
|
||
# supaya tetap terbuka selagi user mengisi form di dalam popup-nya)
|
||
aksi = st.session_state.get("_um_action")
|
||
daftar = auth.list_users()
|
||
peta_user = {u["username"]: u for u in daftar}
|
||
|
||
if aksi is not None:
|
||
jenis, target_username = aksi
|
||
if jenis == "tambah":
|
||
_dialog_tambah_user()
|
||
elif jenis == "edit" and target_username in peta_user:
|
||
data = peta_user[target_username]
|
||
_dialog_edit_user(target_username, data["nama"], data["role"])
|
||
elif jenis == "hapus" and target_username in peta_user:
|
||
_dialog_hapus_user(target_username)
|
||
|
||
# --- Header: "Daftar User" + tombol Tambah User di kanan ---
|
||
# vertical_alignment="center" supaya teks & tombol sejajar tengah
|
||
# (defaultnya top-aligned, makanya sebelumnya terlihat tidak lurus).
|
||
col_judul, col_tombol = st.columns([4, 1], vertical_alignment="center")
|
||
with col_judul:
|
||
st.markdown("**Daftar User**")
|
||
with col_tombol:
|
||
if st.button("➕ Tambah User", width="stretch", key="btn_buka_tambah"):
|
||
st.session_state["_um_action"] = ("tambah", None)
|
||
st.rerun()
|
||
|
||
# --- Tabel: pakai st.dataframe() ASLI (sama seperti tabel di halaman
|
||
# lain -- otomatis rapi & responsif), bukan st.columns() manual lagi.
|
||
#
|
||
# Konsekuensinya: st.dataframe tidak bisa diisi tombol di dalam sel,
|
||
# jadi kolom "Aksi" (edit/hapus per baris) diganti pola "klik baris
|
||
# untuk pilih -> tombol aksi muncul di bawah tabel" (selection_mode
|
||
# bawaan Streamlit). Klik baris yang sudah terpilih lagi untuk
|
||
# membatalkan pilihan.
|
||
username_login_ini = auth.get_current_username()
|
||
|
||
tampil = [
|
||
{"No": i, "Username": u["username"], "Nama": u["nama"], "Role": u["role"]}
|
||
for i, u in enumerate(daftar, start=1)
|
||
]
|
||
|
||
table_version = st.session_state.get("_um_table_version", 0)
|
||
event = st.dataframe(
|
||
tampil,
|
||
width="stretch",
|
||
hide_index=True,
|
||
on_select="rerun",
|
||
selection_mode="single-row",
|
||
key=f"tabel_manajemen_user_{table_version}",
|
||
)
|
||
|
||
baris_terpilih = event.selection.rows if event and event.selection else []
|
||
|
||
if not baris_terpilih:
|
||
st.caption("👆 Klik salah satu baris di atas untuk mengedit atau menghapus user tersebut.")
|
||
else:
|
||
user_terpilih = daftar[baris_terpilih[0]]
|
||
st.markdown(f"**User terpilih: `{user_terpilih['username']}`**")
|
||
|
||
col_edit, col_hapus = st.columns(2)
|
||
if col_edit.button("✏️ Edit User Ini", width="stretch", key="btn_edit_terpilih"):
|
||
st.session_state["_um_action"] = ("edit", user_terpilih["username"])
|
||
st.rerun()
|
||
|
||
# User terpilih = akun sendiri yang sedang login -> tombol tetap
|
||
# tampil tapi dinonaktifkan (bukan disembunyikan/diganti teks)
|
||
is_akun_sendiri = user_terpilih["username"] == username_login_ini
|
||
if col_hapus.button(
|
||
"🗑️ Hapus User Ini",
|
||
width="stretch",
|
||
key="btn_hapus_terpilih",
|
||
disabled=is_akun_sendiri,
|
||
help="Tidak bisa menghapus akun sendiri." if is_akun_sendiri else None,
|
||
):
|
||
st.session_state["_um_action"] = ("hapus", user_terpilih["username"])
|
||
st.rerun() |