codex: Show group memberships no user page
And add group user, which shows a link to the group in the page.
This commit is contained in:
@@ -18,6 +18,8 @@
|
||||
<Compile Include="OpenFGA/useReadTuples.fs" />
|
||||
<Compile Include="OpenFGA/Checkbox.fs" />
|
||||
<Compile Include="OpenFGA/ArchiveOwnerList.fs" />
|
||||
<Compile Include="Users/DeleteForm.fs" />
|
||||
<Compile Include="Users/OpenFgaList.fs" />
|
||||
<Compile Include="Groups/Utils.fs" />
|
||||
<Compile Include="Groups/useGroups.fs" />
|
||||
<Compile Include="Groups/List.fs" />
|
||||
@@ -34,6 +36,7 @@
|
||||
<Compile Include="Groups.fs" />
|
||||
<Compile Include="GroupArchiveAddForm.fs" />
|
||||
<Compile Include="GroupArchive.fs" />
|
||||
<Compile Include="GroupUser.fs" />
|
||||
<Compile Include="Group.fs" />
|
||||
<Compile Include="ArchivesList.fs" />
|
||||
<Compile Include="Archives.fs" />
|
||||
|
||||
@@ -155,7 +155,7 @@ type Components =
|
||||
| [ "groups" ] -> Groups.View ()
|
||||
| [ "groups"; group ] -> Group.View group
|
||||
| [ "groups"; group; "archives"; Route.Guid id ] -> GroupArchive.View group id
|
||||
| [ "groups"; group; "users"; user ] -> User.View user
|
||||
| [ "groups"; group; "users"; user ] -> GroupUser.View group user
|
||||
| [ "users"; user ] -> User.View user
|
||||
| [ "organizations" ] -> Organizations.List ()
|
||||
| [ "organizations"; org ] -> Organization.View org
|
||||
|
||||
131
src/Codex/src/Client/GroupUser.fs
Normal file
131
src/Codex/src/Client/GroupUser.fs
Normal file
@@ -0,0 +1,131 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
|
||||
module GroupUser =
|
||||
[<ReactComponent>]
|
||||
let View (group: string) (user: string) =
|
||||
let fgaUser = sprintf "user:%s" user
|
||||
let execCtx = {|
|
||||
time = System.DateTime.Now
|
||||
task = "*"
|
||||
usage = "-1"
|
||||
|}
|
||||
|
||||
Html.main [
|
||||
Html.h1 [
|
||||
prop.children [
|
||||
Html.text "Group "
|
||||
Html.a [
|
||||
prop.href (Router.format ("groups", group))
|
||||
prop.text group
|
||||
]
|
||||
Html.text " / "
|
||||
Html.text "User "
|
||||
Html.a [
|
||||
prop.href (Router.format ("users", user))
|
||||
prop.text user
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
Users.DeleteForm user
|
||||
]
|
||||
]
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
Html.h2 "Archmaester"
|
||||
|
||||
Html.p "TODO"
|
||||
]
|
||||
]
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
Html.h2 "OpenFGA"
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "flex-wrap"; "gap-8" ]
|
||||
prop.children [
|
||||
OpenFGA.Checkbox("active-checkbox", "Active", fgaUser, "active", fgaUser)
|
||||
OpenFGA.Checkbox("registered-checkbox", "Registered", fgaUser, "registered", fgaUser)
|
||||
OpenFGA.Checkbox("disabled-checkbox", "Disabled", fgaUser, "disabled", fgaUser)
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "flex-row"; "flex-wrap"; "gap-32" ]
|
||||
prop.children [
|
||||
Html.div [
|
||||
prop.classes [ "grow" ]
|
||||
prop.style [
|
||||
style.flexBasis (length.px 320)
|
||||
style.maxWidth (length.px 512)
|
||||
style.minWidth (length.px 320)
|
||||
]
|
||||
prop.children [
|
||||
Html.h3 "Archives with Owner"
|
||||
Html.div [
|
||||
prop.style [
|
||||
style.overflowY.auto
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
Users.OpenFgaList(fgaUser, "owner", "archive")
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "grow" ]
|
||||
prop.style [
|
||||
style.flexBasis (length.px 320)
|
||||
style.maxWidth (length.px 512)
|
||||
style.minWidth (length.px 320)
|
||||
]
|
||||
prop.children [
|
||||
Html.h3 "Archives with View"
|
||||
|
||||
Html.div [
|
||||
prop.style [
|
||||
style.overflowY.auto
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
Users.OpenFgaList(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.div [
|
||||
prop.classes [ "grow" ]
|
||||
prop.style [
|
||||
style.flexBasis (length.px 320)
|
||||
style.maxWidth (length.px 512)
|
||||
style.minWidth (length.px 320)
|
||||
]
|
||||
prop.children [
|
||||
Html.h3 "Archives with exec"
|
||||
|
||||
Html.div [
|
||||
prop.style [
|
||||
style.overflowY.auto
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
Users.OpenFgaList(fgaUser, "exec", "archive", execCtx)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -1,123 +1,9 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open Fable.Remoting.Client
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
|
||||
[<Erase>]
|
||||
type User =
|
||||
[<ReactComponent>]
|
||||
static member List(user: string, relation: string, objectType: string, ?context: obj) =
|
||||
let objects = OpenFGA.useObjects(user, relation, objectType, context)
|
||||
|
||||
if objects.Loading then
|
||||
Html.p "Loading ..."
|
||||
else
|
||||
if Array.isEmpty objects.Objects then
|
||||
Html.p (sprintf "No objects with user %s relation %s of type %s" user relation objectType)
|
||||
else
|
||||
Html.ul [
|
||||
prop.children (
|
||||
objects.Objects
|
||||
|> Array.sort
|
||||
|> Array.map (fun object ->
|
||||
let split = object.Split ':'
|
||||
match split with
|
||||
| [| objectType; id |] ->
|
||||
Html.li [
|
||||
prop.key id
|
||||
prop.children [
|
||||
Html.a [
|
||||
prop.href (Router.format("archives", id))
|
||||
prop.text id
|
||||
]
|
||||
]
|
||||
]
|
||||
| _ ->
|
||||
Html.li [
|
||||
prop.text "Invalid object format"
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
module User =
|
||||
[<ReactComponent>]
|
||||
let private DeleteForm (user: string) =
|
||||
let deleted, setDeleted = React.useState<Result<unit, string> option> None
|
||||
let deleting, setDeleting = React.useState false
|
||||
|
||||
let handleDelete =
|
||||
React.useCallback (
|
||||
(fun () ->
|
||||
setDeleting true
|
||||
console.info("[User] Deleting user %s", user)
|
||||
Remoting.adminApi.removeUsers [| user |]
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.catch (fun ex ->
|
||||
match ex with
|
||||
| :? ProxyRequestException as e ->
|
||||
let proxyError : Types.ProxyError = JS.JSON.parse e.ResponseText |> unbox
|
||||
let msg = proxyError.error.errorMsg
|
||||
Error msg
|
||||
| ex ->
|
||||
Error ex.Message
|
||||
)
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
console.info("[User] Successfully deleted user %s", user)
|
||||
setDeleted (Some (Ok ()))
|
||||
| Error err ->
|
||||
console.error("[User] Error deleting user %s: %s", user, err)
|
||||
setDeleted (Some (Error err))
|
||||
)
|
||||
),
|
||||
[| box user |]
|
||||
)
|
||||
|
||||
React.fragment [
|
||||
match deleted with
|
||||
| Some (Ok ()) ->
|
||||
Html.p "User successfully deleted."
|
||||
Html.a [
|
||||
prop.onClick (fun ev ->
|
||||
ev.preventDefault ()
|
||||
Router.navigateBack ()
|
||||
)
|
||||
prop.href (Router.format "")
|
||||
prop.text "Back"
|
||||
]
|
||||
| Some (Error err) ->
|
||||
Html.p (sprintf "Error deleting user: %s" err)
|
||||
| None ->
|
||||
if deleting then
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center"; "gap-8" ]
|
||||
prop.children [
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> handleDelete ())
|
||||
prop.text "Are you sure?"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> setDeleting false)
|
||||
prop.text "Cancel"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.p "This will delete the user from the databases. Not disable the user."
|
||||
else
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> setDeleting true)
|
||||
prop.text "Delete"
|
||||
]
|
||||
]
|
||||
|
||||
[<ReactComponent>]
|
||||
let View (user: string) =
|
||||
let fgaUser = sprintf "user:%s" user
|
||||
@@ -127,15 +13,49 @@ module User =
|
||||
usage = "-1"
|
||||
|}
|
||||
|
||||
let groups = OpenFGA.useObjects(fgaUser, "member", "group")
|
||||
|
||||
Html.main [
|
||||
Html.h1 user
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
DeleteForm user
|
||||
Users.DeleteForm user
|
||||
]
|
||||
]
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
Html.h2 "Groups"
|
||||
|
||||
Html.div [
|
||||
prop.children [
|
||||
Html.ul [
|
||||
prop.children (
|
||||
groups.Objects
|
||||
|> Array.map (fun group ->
|
||||
let split = group.Split ':'
|
||||
match split with
|
||||
| [| objectType; groupName |] ->
|
||||
Html.li [
|
||||
prop.children [
|
||||
Html.a [
|
||||
prop.href (Router.format("groups", groupName))
|
||||
prop.text groupName
|
||||
]
|
||||
]
|
||||
]
|
||||
| _ ->
|
||||
Html.none
|
||||
)
|
||||
)
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
Html.section [
|
||||
prop.children [
|
||||
Html.h2 "Archmaester"
|
||||
@@ -175,7 +95,7 @@ module User =
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
User.List(fgaUser, "owner", "archive")
|
||||
Users.OpenFgaList(fgaUser, "owner", "archive")
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -197,7 +117,7 @@ module User =
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
User.List(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
|
||||
Users.OpenFgaList(fgaUser, "view", "archive", {| time = System.DateTime.Now |})
|
||||
]
|
||||
]
|
||||
]
|
||||
@@ -219,7 +139,7 @@ module User =
|
||||
style.maxHeight (length.px 512)
|
||||
]
|
||||
prop.children [
|
||||
User.List(fgaUser, "exec", "archive", execCtx)
|
||||
Users.OpenFgaList(fgaUser, "exec", "archive", execCtx)
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
81
src/Codex/src/Client/Users/DeleteForm.fs
Normal file
81
src/Codex/src/Client/Users/DeleteForm.fs
Normal file
@@ -0,0 +1,81 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
open Browser
|
||||
open Fable.Core
|
||||
open Fable.Remoting.Client
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
|
||||
module Users =
|
||||
[<ReactComponent>]
|
||||
let DeleteForm (user: string) =
|
||||
let deleted, setDeleted = React.useState<Result<unit, string> option> None
|
||||
let deleting, setDeleting = React.useState false
|
||||
|
||||
let handleDelete =
|
||||
React.useCallback (
|
||||
(fun () ->
|
||||
setDeleting true
|
||||
console.info("[User] Deleting user %s", user)
|
||||
Remoting.adminApi.removeUsers [| user |]
|
||||
|> Async.StartAsPromise
|
||||
|> Promise.catch (fun ex ->
|
||||
match ex with
|
||||
| :? ProxyRequestException as e ->
|
||||
let proxyError : Types.ProxyError = JS.JSON.parse e.ResponseText |> unbox
|
||||
let msg = proxyError.error.errorMsg
|
||||
Error msg
|
||||
| ex ->
|
||||
Error ex.Message
|
||||
)
|
||||
|> Promise.iter (fun res ->
|
||||
match res with
|
||||
| Ok () ->
|
||||
console.info("[User] Successfully deleted user %s", user)
|
||||
setDeleted (Some (Ok ()))
|
||||
| Error err ->
|
||||
console.error("[User] Error deleting user %s: %s", user, err)
|
||||
setDeleted (Some (Error err))
|
||||
)
|
||||
),
|
||||
[| box user |]
|
||||
)
|
||||
|
||||
React.fragment [
|
||||
match deleted with
|
||||
| Some (Ok ()) ->
|
||||
Html.p "User successfully deleted."
|
||||
Html.a [
|
||||
prop.onClick (fun ev ->
|
||||
ev.preventDefault ()
|
||||
Router.navigateBack ()
|
||||
)
|
||||
prop.href (Router.format "")
|
||||
prop.text "Back"
|
||||
]
|
||||
| Some (Error err) ->
|
||||
Html.p (sprintf "Error deleting user: %s" err)
|
||||
| None ->
|
||||
if deleting then
|
||||
Html.div [
|
||||
prop.classes [ "flex-row-center"; "gap-8" ]
|
||||
prop.children [
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> handleDelete ())
|
||||
prop.text "Are you sure?"
|
||||
]
|
||||
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> setDeleting false)
|
||||
prop.text "Cancel"
|
||||
]
|
||||
]
|
||||
]
|
||||
|
||||
Html.p "This will delete the user from the databases. Not disable the user."
|
||||
else
|
||||
Html.button [
|
||||
prop.onClick (fun _ -> setDeleting true)
|
||||
prop.text "Delete"
|
||||
]
|
||||
]
|
||||
42
src/Codex/src/Client/Users/OpenFgaList.fs
Normal file
42
src/Codex/src/Client/Users/OpenFgaList.fs
Normal file
@@ -0,0 +1,42 @@
|
||||
namespace Oceanbox.Codex
|
||||
|
||||
open Fable.Core
|
||||
open Feliz
|
||||
open Feliz.Router
|
||||
|
||||
[<Erase>]
|
||||
type Users =
|
||||
[<ReactComponent>]
|
||||
static member OpenFgaList(user: string, relation: string, objectType: string, ?context: obj) =
|
||||
let objects = OpenFGA.useObjects(user, relation, objectType, context)
|
||||
|
||||
if objects.Loading then
|
||||
Html.p "Loading ..."
|
||||
else
|
||||
if Array.isEmpty objects.Objects then
|
||||
Html.p (sprintf "No objects with user %s relation %s of type %s" user relation objectType)
|
||||
else
|
||||
Html.ul [
|
||||
prop.children (
|
||||
objects.Objects
|
||||
|> Array.sort
|
||||
|> Array.map (fun object ->
|
||||
let split = object.Split ':'
|
||||
match split with
|
||||
| [| objectType; id |] ->
|
||||
Html.li [
|
||||
prop.key id
|
||||
prop.children [
|
||||
Html.a [
|
||||
prop.href (Router.format("archives", id))
|
||||
prop.text id
|
||||
]
|
||||
]
|
||||
]
|
||||
| _ ->
|
||||
Html.li [
|
||||
prop.text "Invalid object format"
|
||||
]
|
||||
)
|
||||
)
|
||||
]
|
||||
Reference in New Issue
Block a user