From 9ef7e8c46391ffc3c53879ae40c72fc724780235 Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Sun, 26 Sep 2021 18:45:40 +0100 Subject: [PATCH] Allow users to create organisations in Web UI --- chartered-db/src/lib.rs | 2 +- chartered-db/src/organisations.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/index.tsx | 6 ++++++ chartered-frontend/src/util.tsx | 32 ++++++++++++++++++++++++++++++-- chartered-web/src/main.rs | 4 ++++ chartered-frontend/src/pages/crate/CrateView.tsx | 9 +++++++-- chartered-frontend/src/pages/crate/Members.tsx | 13 +++++++++++-- chartered-frontend/src/pages/crate/OrganisationView.tsx | 30 +++++++++++++++++++++++++++--- chartered-frontend/src/pages/organisations/CreateOrganisation.tsx | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ chartered-frontend/src/pages/organisations/ListOrganisations.tsx | 14 +++++++++++++- chartered-web/src/endpoints/web_api/organisations/crud.rs | 39 +++++++++++++++++++++++++++++++++++++++ chartered-web/src/endpoints/web_api/organisations/list.rs | 4 +--- chartered-web/src/endpoints/web_api/organisations/mod.rs | 2 ++ 13 files changed, 319 insertions(+), 18 deletions(-) diff --git a/chartered-db/src/lib.rs b/chartered-db/src/lib.rs index bc4c9af..b18f8af 100644 --- a/chartered-db/src/lib.rs +++ a/chartered-db/src/lib.rs @@ -62,7 +62,7 @@ pub enum Error { /// Failed to initialise to database connection pool Connection(#[from] diesel::r2d2::PoolError), - /// Failed to run query + /// {0} Query(#[from] diesel::result::Error), /// Failed to complete query task TaskJoin(#[from] tokio::task::JoinError), diff --git a/chartered-db/src/organisations.rs b/chartered-db/src/organisations.rs index aadf4b9..259fdc2 100644 --- a/chartered-db/src/organisations.rs +++ a/chartered-db/src/organisations.rs @@ -87,6 +87,50 @@ }) .await? } + + pub async fn create( + conn: ConnectionPool, + given_name: String, + given_description: String, + requesting_user_id: i32, + ) -> Result<()> { + tokio::task::spawn_blocking(move || { + let conn = conn.get()?; + + conn.transaction::<_, crate::Error, _>(|| { + use organisations::dsl::{description, id, name, uuid}; + use user_organisation_permissions::dsl::{organisation_id, permissions, user_id}; + + let generated_uuid = SqlUuid::random(); + + diesel::insert_into(organisations::table) + .values(( + uuid.eq(generated_uuid), + name.eq(given_name), + description.eq(given_description), + )) + .execute(&conn)?; + + let inserted_id: i32 = organisations::table + .filter(uuid.eq(generated_uuid)) + .select(id) + .get_result(&conn)?; + + diesel::insert_into(user_organisation_permissions::table) + .values(( + user_id.eq(requesting_user_id), + organisation_id.eq(inserted_id), + permissions.eq(UserPermission::all().bits()), + )) + .execute(&conn)?; + + Ok(()) + })?; + + Ok(()) + }) + .await? + } } pub struct OrganisationWithPermissions { diff --git a/chartered-frontend/src/index.tsx b/chartered-frontend/src/index.tsx index aa3b7f3..783ed87 100644 --- a/chartered-frontend/src/index.tsx +++ a/chartered-frontend/src/index.tsx @@ -21,6 +21,7 @@ import AddSshKeys from "./pages/ssh-keys/AddSshKeys"; import ListOrganisations from "./pages/organisations/ListOrganisations"; import OrganisationView from "./pages/crate/OrganisationView"; +import CreateOrganisation from "./pages/organisations/CreateOrganisation"; function App() { return ( @@ -78,6 +79,11 @@ exact path="/organisations/list" component={() => } + /> + } /> diff --git a/chartered-frontend/src/util.tsx b/chartered-frontend/src/util.tsx index 23b861c..27b8940 100644 --- a/chartered-frontend/src/util.tsx +++ a/chartered-frontend/src/util.tsx @@ -40,14 +40,38 @@ return { response, error }; } -export function RoundedPicture({ src, height, width, className }: { src: string, height: string, width: string, className?: string }) { +export function RoundedPicture({ + src, + height, + width, + className, +}: { + src: string; + height: string; + width: string; + className?: string; +}) { const [imageLoaded, setImageLoaded] = React.useState(false); return ( -
- <> +
+ + <> + setImageLoaded(true)} className="rounded-circle" diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index 21ea132..dbb71e7 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -76,6 +76,10 @@ get(endpoints::web_api::organisations::list) ) .route( + "/organisations", + put(endpoints::web_api::organisations::create) + ) + .route( "/organisations/:org", get(endpoints::web_api::organisations::info) ) diff --git a/chartered-frontend/src/pages/crate/CrateView.tsx b/chartered-frontend/src/pages/crate/CrateView.tsx index b849334..3a86021 100644 --- a/chartered-frontend/src/pages/crate/CrateView.tsx +++ a/chartered-frontend/src/pages/crate/CrateView.tsx @@ -19,7 +19,12 @@ Square, } from "react-bootstrap-icons"; import { useParams, NavLink, Redirect, Link } from "react-router-dom"; -import { authenticatedEndpoint, RoundedPicture, RoundedPicture, useAuthenticatedRequest } from "../../util"; +import { + authenticatedEndpoint, + RoundedPicture, + RoundedPicture, + useAuthenticatedRequest, +} from "../../util"; import Prism from "react-syntax-highlighter/dist/cjs/prism"; import ReactMarkdown from "react-markdown"; @@ -188,7 +193,7 @@
-
+
{currentTab == "readme" ? : <>} {currentTab == "versions" ? ( diff --git a/chartered-frontend/src/pages/crate/Members.tsx b/chartered-frontend/src/pages/crate/Members.tsx index 0ab68c1..ceb1502 100644 --- a/chartered-frontend/src/pages/crate/Members.tsx +++ a/chartered-frontend/src/pages/crate/Members.tsx @@ -7,7 +7,12 @@ Save, PlusLg, } from "react-bootstrap-icons"; -import { authenticatedEndpoint, RoundedPicture, RoundedPicture, useAuthenticatedRequest } from "../../util"; +import { + authenticatedEndpoint, + RoundedPicture, + RoundedPicture, + useAuthenticatedRequest, +} from "../../util"; import { useAuth } from "../../useAuth"; import { Button, Modal } from "react-bootstrap"; import { AsyncTypeahead } from "react-bootstrap-typeahead"; @@ -204,7 +209,11 @@ - + diff --git a/chartered-frontend/src/pages/crate/OrganisationView.tsx b/chartered-frontend/src/pages/crate/OrganisationView.tsx index 2020605..9005417 100644 --- a/chartered-frontend/src/pages/crate/OrganisationView.tsx +++ a/chartered-frontend/src/pages/crate/OrganisationView.tsx @@ -1,10 +1,14 @@ import React = require("react"); import { useState, useEffect } from "react"; import { Link, useParams } from "react-router-dom"; import Nav from "../../sections/Nav"; import { useAuth } from "../../useAuth"; -import { useAuthenticatedRequest, authenticatedEndpoint, RoundedPicture } from "../../util"; +import { + useAuthenticatedRequest, + authenticatedEndpoint, + RoundedPicture, +} from "../../util"; import { BoxSeam, Plus, Trash } from "react-bootstrap-icons"; import { @@ -76,12 +80,26 @@
- +

{organisation}

- -

{organisationDetails?.description || No description given.}

+ +

+ {organisationDetails?.description || ( + No description given. + )} +

@@ -136,7 +154,9 @@ setReload(reload + 1)} /> ) : ( diff --git a/chartered-frontend/src/pages/organisations/CreateOrganisation.tsx b/chartered-frontend/src/pages/organisations/CreateOrganisation.tsx new file mode 100644 index 0000000..3b1e9cd 100644 --- /dev/null +++ a/chartered-frontend/src/pages/organisations/CreateOrganisation.tsx @@ -1,0 +1,138 @@ +import React = require("react"); +import { useState, useEffect } from "react"; +import { Link, useHistory } from "react-router-dom"; + +import Nav from "../../sections/Nav"; +import { useAuth } from "../../useAuth"; +import { authenticatedEndpoint } from "../../util"; + +import { Plus } from "react-bootstrap-icons"; + +export default function CreateOrganisation() { + const auth = useAuth(); + const router = useHistory(); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(""); + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + + const createOrganisation = async (evt) => { + evt.preventDefault(); + + setError(""); + setLoading(true); + + try { + let res = await fetch(authenticatedEndpoint(auth, "organisations"), { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ name, description }), + }); + let json = await res.json(); + + if (json.error) { + throw new Error(json.error); + } + + setName(""); + setDescription(""); + router.push(`/crates/${name}`); + } catch (e) { + setError(e.message); + } finally { + setLoading(false); + } + }; + + return ( +
+