;(function() { const api = { post(url, body) { return m.request({method: "POST", url, body}) }, myTenants() { return api.post("/dashboard/api/my-tenants", {}) }, tenants: { create(name) { return api.post("/dashboard/api/tenants/create", {Name: name}) }, get(id) { return api.post("/dashboard/api/tenants/get", {ID: id}) }, }, members: { list(tenantID) { return api.post("/dashboard/api/members/list", {TenantID: tenantID}) }, invite(tenantID, email) { return api.post("/dashboard/api/members/invite", {TenantID: tenantID, Email: email}) }, remove(tenantID, userID) { return api.post("/dashboard/api/members/remove", {TenantID: tenantID, UserID: userID}) }, }, groups: { list(ns) { return api.post("/dashboard/api/groups/list", {Namespace: ns, Page: {Limit: 100}}) }, get(id) { return api.post("/dashboard/api/groups/get", {ID: id}) }, create(data) { return api.post("/dashboard/api/groups/create", data) }, update(data) { return api.post("/dashboard/api/groups/update", data) }, delete(id) { return api.post("/dashboard/api/groups/delete", {ID: id}) }, }, roles: { list(ns) { return api.post("/dashboard/api/roles/list", {Namespace: ns, Page: {Limit: 100}}) }, }, grants: { list(ns) { return api.post("/dashboard/api/grants/list", {Namespace: ns, Page: {Limit: 100}}) }, }, invitations: { list() { return api.post("/dashboard/api/invitations/list", {}) }, accept(tenantID) { return api.post("/dashboard/api/invitations/accept", {TenantID: tenantID}) }, }, profile: { get() { return api.post("/dashboard/api/profile/get", {}) }, update(data) { return api.post("/dashboard/api/profile/update", data) }, }, } function navLink(path, label) { const cur = m.route.get() const active = cur === path || cur.startsWith(path + "/") return m("a", {href: "#!/" + path, class: active ? "active" : ""}, label) } const tenantNames = {} function getTenantName(id) { if (tenantNames[id]) return tenantNames[id] tenantNames[id] = id api.tenants.get(id).then(t => { tenantNames[id] = t.Name; m.redraw() }) return id } const Layout = { view(vnode) { const cur = m.route.get() const match = cur.match(/^\/tenants\/([^/]+)\//) const ns = match ? match[1] : null return m(".layout", [ m("nav", [ m(".logo", "Dashboard"), navLink("profile", "Profile"), navLink("tenants", "My Tenants"), ns ? [ m(".nav-section", getTenantName(ns)), navLink("tenants/" + ns + "/members", "Members"), navLink("tenants/" + ns + "/groups", "Groups"), navLink("tenants/" + ns + "/roles", "Roles"), navLink("tenants/" + ns + "/grants", "Grants"), ] : null, m(".nav-section", ""), m("a", {href: "/logout", onclick: e => { e.preventDefault() m.request({method: "POST", url: "/logout"}).then(() => { window.location.href = "/login" }) }}, "Log Out"), ]), m("main", vnode.children), ]) } } function table(cols, rows, empty) { if (!rows || rows.length === 0) return m(".empty", empty || "No items found.") return m("table", [ m("thead", m("tr", cols.map(c => m("th", c.label)))), m("tbody", rows.map(row => m("tr", cols.map(c => m("td", {class: c.cls || ""}, c.value(row)))) )), ]) } // Profile function ProfilePage() { let form = {Name: "", Bio: "", Location: "", Website: "", PictureURL: ""} let email = "" let loading = true let saving = false let error = null let success = null api.profile.get().then(p => { email = p.Email form = {Name: p.Name || "", Bio: p.Bio || "", Location: p.Location || "", Website: p.Website || "", PictureURL: p.PictureURL || ""} loading = false m.redraw() }).catch(() => { error = "Failed to load profile"; loading = false; m.redraw() }) const bind = key => ({value: form[key], oninput: e => { form[key] = e.target.value; success = null }}) const submit = e => { e.preventDefault() saving = true error = null success = null api.profile.update(form) .then(() => { saving = false; success = "Profile saved."; m.redraw() }) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; saving = false; m.redraw() }) } return { view() { return m(Layout, [ m("h1", "Profile"), loading ? m(".empty", "Loading...") : [ error && m(".error", error), success && m(".success", success), m(".form-field", [m("label", "Email"), m("input[type=email][disabled]", {value: email})]), m("form.form", {onsubmit: submit}, [ m(".form-field", [m("label", "Name"), m("input[type=text]", {...bind("Name")})]), m(".form-field", [m("label", "Bio"), m("input[type=text]", {...bind("Bio")})]), m(".form-field", [m("label", "Location"), m("input[type=text]", {...bind("Location")})]), m(".form-field", [m("label", "Website"), m("input[type=text]", {...bind("Website")})]), m(".form-field", [m("label", "Picture URL"), m("input[type=text]", {...bind("PictureURL")})]), m(".form-actions", [ m("button[type=submit].btn", {disabled: saving}, saving ? "Saving..." : "Save"), ]), ]), ], ]) }, } } // Tenants list function TenantsPage() { let items = [] let loading = true let name = "" let creating = false let error = null let invitations = [] let invitationsLoading = true function load() { api.myTenants().then(res => { items = res.Items || []; loading = false; m.redraw() }) } function loadInvitations() { api.invitations.list().then(res => { invitations = res.Items || []; invitationsLoading = false; m.redraw() }) } load() loadInvitations() const create = e => { e.preventDefault() creating = true error = null api.tenants.create(name) .then(() => { name = ""; creating = false; loading = true; load() }) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; creating = false; m.redraw() }) } const accept = tenantID => { error = null api.invitations.accept(tenantID) .then(() => { loadInvitations(); loading = true; load() }) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; m.redraw() }) } return { view() { return m(Layout, [ m(".page-header", [ m("h1", "My Tenants"), ]), error && m(".error", error), !invitationsLoading && invitations.length > 0 ? [ m("h2", {style: "font-size: 1.1rem; margin-bottom: 0.75rem;"}, "Pending Invitations"), table([ {label: "Tenant", value: inv => inv.TenantName}, {label: "", value: inv => m("button.btn", {onclick: () => accept(inv.TenantID)}, "Accept")}, ], invitations), m("div", {style: "margin-bottom: 1.5rem;"}), ] : null, m("form.form.invite-form", {onsubmit: create}, [ m("input[type=text]", {required: true, placeholder: "New tenant name", value: name, oninput: e => name = e.target.value}), m("button[type=submit].btn", {disabled: creating}, creating ? "Creating..." : "Create"), ]), loading ? m(".empty", "Loading...") : table([ {label: "Name", value: t => t.Name}, {label: "Type", value: t => t.Type}, {label: "", value: t => m("a.action-link", {href: "#!/tenants/" + t.ID + "/members"}, "Manage")}, ], items, "No tenants found."), ]) }, } } // Tenant members function TenantMembersPage(vnode) { const ns = vnode.attrs.ns let items = [] let loading = true let email = "" let inviting = false let error = null function load() { api.members.list(ns).then(res => { items = res.Items || []; loading = false; m.redraw() }) } load() const invite = e => { e.preventDefault() inviting = true error = null api.members.invite(ns, email) .then(() => { email = ""; inviting = false; loading = true; load() }) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; inviting = false; m.redraw() }) } const remove = userID => { api.members.remove(ns, userID) .then(() => { loading = true; load() }) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; m.redraw() }) } return { view() { return m(Layout, [ m("h1", "Members"), error && m(".error", error), m("form.form.invite-form", {onsubmit: invite}, [ m("input[type=email]", {required: true, placeholder: "Email address", value: email, oninput: e => email = e.target.value}), m("button[type=submit].btn", {disabled: inviting}, inviting ? "Inviting..." : "Invite"), ]), loading ? m(".empty", "Loading...") : table([ {label: "User ID", cls: "mono", value: mb => mb.UserID}, {label: "Status", value: mb => mb.Status === "active" ? m("span.badge.badge-green", "Active") : m("span.badge.badge-gray", "Invited")}, {label: "", value: mb => m("a.action-link", {href: "#", onclick: e => { e.preventDefault(); remove(mb.UserID) }}, "Remove")}, ], items, "No members found."), ]) }, } } // Tenant groups function TenantGroupsPage(vnode) { const ns = vnode.attrs.ns let items = [] let loading = true api.groups.list(ns).then(res => { items = res.Items || []; loading = false; m.redraw() }) return { view() { return m(Layout, [ m(".page-header", [ m("h1", "Groups"), m("a.btn", {href: "#!/tenants/" + ns + "/groups/new"}, "New Group"), ]), loading ? m(".empty", "Loading...") : table([ {label: "ID", cls: "mono", value: g => g.ID}, {label: "Path", cls: "mono", value: g => g.Path}, {label: "Name", value: g => g.Name}, {label: "", value: g => m("a.action-link", {href: "#!/tenants/" + ns + "/groups/" + g.ID + "/edit"}, "Edit")}, ], items, "No groups found."), ]) }, } } // Group form function TenantGroupFormPage(vnode) { const ns = vnode.attrs.ns const prefix = ns + "." let form = {PathSuffix: "", Name: ""} let saving = false let loading = false let error = null const id = vnode.attrs.id if (id) { loading = true api.groups.get(id).then(g => { const path = g.Path || "" form = {PathSuffix: path.startsWith(prefix) ? path.slice(prefix.length) : path, Name: g.Name || ""} loading = false m.redraw() }).catch(() => { error = "Failed to load group"; loading = false; m.redraw() }) } const bind = key => ({value: form[key], oninput: e => form[key] = e.target.value}) const submit = e => { e.preventDefault() saving = true error = null const data = {Path: prefix + form.PathSuffix, Name: form.Name} const req = id ? api.groups.update({ID: id, ...data}) : api.groups.create(data) req.then(() => m.route.set("/tenants/" + ns + "/groups")) .catch(err => { error = (err.response && err.response.Message) || "An error occurred"; saving = false; m.redraw() }) } return { view() { return m(Layout, [ m("h1", id ? "Edit Group" : "New Group"), loading ? m(".empty", "Loading...") : [ error && m(".error", error), m("form.form", {onsubmit: submit}, [ m(".form-field", [m("label", "Path"), m(".path-input", [m("span.path-prefix", prefix), m("input[type=text].mono", {required: true, placeholder: "engineering", ...bind("PathSuffix")})])]), m(".form-field", [m("label", "Name"), m("input[type=text]", {required: true, ...bind("Name")})]), m(".form-actions", [ m("button[type=submit].btn", {disabled: saving}, saving ? "Saving..." : (id ? "Save Changes" : "Create Group")), m("a.cancel-link", {href: "#!/tenants/" + ns + "/groups"}, "Cancel"), ]), ]), ], ]) }, } } // Tenant roles function TenantRolesPage(vnode) { const ns = vnode.attrs.ns let items = [] let loading = true api.roles.list(ns).then(res => { items = res.Items || []; loading = false; m.redraw() }) return { view() { return m(Layout, [ m("h1", "Roles"), loading ? m(".empty", "Loading...") : table([ {label: "ID", cls: "mono", value: r => r.ID}, {label: "Name", value: r => r.Name}, {label: "Actions", cls: "mono", value: r => (r.Actions || []).join(", ")}, ], items, "No roles found."), ]) }, } } // Tenant grants function TenantGrantsPage(vnode) { const ns = vnode.attrs.ns let items = [] let loading = true api.grants.list(ns).then(res => { items = res.Items || []; loading = false; m.redraw() }) return { view() { return m(Layout, [ m("h1", "Grants"), loading ? m(".empty", "Loading...") : table([ {label: "Principal ID", cls: "mono", value: g => g.PrincipalID}, {label: "Role ID", cls: "mono", value: g => g.RoleID}, ], items, "No grants found."), ]) }, } } m.route(document.getElementById("app"), "/profile", { "/profile": ProfilePage, "/tenants": TenantsPage, "/tenants/:ns/members": TenantMembersPage, "/tenants/:ns/groups": TenantGroupsPage, "/tenants/:ns/groups/new": TenantGroupFormPage, "/tenants/:ns/groups/:id/edit": TenantGroupFormPage, "/tenants/:ns/roles": TenantRolesPage, "/tenants/:ns/grants": TenantGrantsPage, }) })()