🏡 index : ~doyle/chartered.git

author Jordan Doyle <jordan@doyle.la> 2021-10-15 21:53:02.0 +01:00:00
committer Jordan Doyle <jordan@doyle.la> 2021-10-15 21:53:02.0 +01:00:00
commit
e443ec3cd7c83e5d4511b92d3e401f6881552567 [patch]
tree
21db5e1a4465f13ed94e6d052db64a96ebaed087
parent
b1f975f2c070ec4d49f51f1d2c577692dd4c524e
download
e443ec3cd7c83e5d4511b92d3e401f6881552567.tar.gz

Cleanup navbar, display user picture with dropdown for user-related things



Diff

 chartered-db/src/organisations.rs                                 |  2 +-
 chartered-frontend/src/index.sass                                 |  1 +
 chartered-frontend/src/useAuth.tsx                                | 27 +++++++++++++++++++++++++++
 chartered-frontend/src/util.tsx                                   | 14 ++++++++------
 chartered-frontend/src/pages/Dashboard.tsx                        | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------
 chartered-frontend/src/sections/Nav.tsx                           | 72 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------
 chartered-frontend/src/pages/organisations/CreateOrganisation.tsx | 19 ++++++++++---------
 chartered-web/src/endpoints/web_api/auth/mod.rs                   |  2 ++
 8 files changed, 143 insertions(+), 74 deletions(-)

diff --git a/chartered-db/src/organisations.rs b/chartered-db/src/organisations.rs
index 00953b0..b4495b8 100644
--- a/chartered-db/src/organisations.rs
+++ a/chartered-db/src/organisations.rs
@@ -106,7 +106,7 @@
            let conn = conn.get()?;

            conn.transaction::<_, crate::Error, _>(|| {
                use organisations::dsl::{description, id, name, uuid, public};
                use organisations::dsl::{description, id, name, public, uuid};
                use user_organisation_permissions::dsl::{organisation_id, permissions, user_id};

                let generated_uuid = SqlUuid::random();
diff --git a/chartered-frontend/src/index.sass b/chartered-frontend/src/index.sass
index 1697bf7..1d272bf 100644
--- a/chartered-frontend/src/index.sass
+++ a/chartered-frontend/src/index.sass
@@ -1,5 +1,6 @@
$primary: #0d6efd
$font-family-monospace: "Source Code Pro", SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace
$font-size-base: 0.875rem
$enable-cssgrid: true

@import "../node_modules/react-placeholder/lib/reactPlaceholder.css"
diff --git a/chartered-frontend/src/useAuth.tsx b/chartered-frontend/src/useAuth.tsx
index 8fd1679..99f28f3 100644
--- a/chartered-frontend/src/useAuth.tsx
+++ a/chartered-frontend/src/useAuth.tsx
@@ -13,6 +13,7 @@
  key: string;
  expires: number;
  error?: string;
  picture_url?: string;
}

export interface AuthContext {
@@ -21,6 +22,7 @@
  logout: () => Promise<void>;
  getAuthKey: () => Promise<string | null>;
  getUserUuid: () => string;
  getPictureUrl: () => string;
  handleLoginResponse: (json: LoginResponse) => any;
}

@@ -66,7 +68,12 @@
function useProvideAuth(): AuthContext {
  const [auth, setAuth] = useState(() => {
    let authStorage = getAuthStorage();
    return [authStorage.userUuid, authStorage.authKey, authStorage.expires];
    return [
      authStorage.userUuid,
      authStorage.authKey,
      authStorage.expires,
      authStorage.pictureUrl,
    ];
  });

  useEffect(() => {
@@ -76,6 +83,7 @@
        userUuid: auth?.[0],
        authKey: auth?.[1],
        expires: auth?.[2],
        pictureUrl: auth?.[3],
      })
    );
  }, [auth]);
@@ -85,7 +93,12 @@
      throw new Error(response.error);
    }

    setAuth([response.user_uuid, response.key, new Date(response.expires)]);
    setAuth([
      response.user_uuid,
      response.key,
      new Date(response.expires),
      response.picture_url,
    ]);
  };

  const login = async (username: string, password: string) => {
@@ -143,11 +156,20 @@
    }
  };

  const getPictureUrl = () => {
    if (auth?.[2] > new Date()) {
      return auth[3];
    } else if (auth) {
      return null;
    }
  };

  return {
    login,
    logout,
    getAuthKey,
    getUserUuid,
    getPictureUrl,
    oauthLogin,
    handleLoginResponse,
  };
@@ -160,5 +182,6 @@
    userUuid: initial?.userUuid || null,
    authKey: initial?.authKey || null,
    expires: initial?.expires ? new Date(initial.expires) : null,
    pictureUrl: initial?.pictureUrl,
  };
}
diff --git a/chartered-frontend/src/util.tsx b/chartered-frontend/src/util.tsx
index d3addf4..5c57d39 100644
--- a/chartered-frontend/src/util.tsx
+++ a/chartered-frontend/src/util.tsx
@@ -1,7 +1,7 @@
import React = require("react");
import ReactPlaceholder from "react-placeholder";
import { AuthContext } from "./useAuth";
import {PersonFill} from "react-bootstrap-icons";
import { PersonFill } from "react-bootstrap-icons";

export const BASE_URL = process.env.BASE_URL || "http://localhost:8888";

@@ -99,11 +99,13 @@
        className={`position-relative rounded-circle d-inline-flex justify-content-center align-items-center ${className}`}
        style={{ width, height, background: "rgb(235, 235, 235)" }}
      >
        <PersonFill style={{
          width: `calc(${width} / 2)`,
          height: `calc(${height} / 2)`,
          color: "rgba(0, 0, 0, .1)"
        }} />
        <PersonFill
          style={{
            width: `calc(${width} / 2)`,
            height: `calc(${height} / 2)`,
            color: "rgba(0, 0, 0, .1)",
          }}
        />
      </div>
    );
  }
diff --git a/chartered-frontend/src/pages/Dashboard.tsx b/chartered-frontend/src/pages/Dashboard.tsx
index cf72159..b9318da 100644
--- a/chartered-frontend/src/pages/Dashboard.tsx
+++ a/chartered-frontend/src/pages/Dashboard.tsx
@@ -1,12 +1,12 @@
import React = require("react");

import { Link } from "react-router-dom";
import { useAuth } from "../useAuth";
import Nav from "../sections/Nav";
import {Calendar3, ChevronRight, Download} from "react-bootstrap-icons";
import { Calendar3, ChevronRight, Download } from "react-bootstrap-icons";
import { useAuthenticatedRequest } from "../util";
import HumanTime from "react-human-time";
import {OverlayTrigger, Tooltip} from "react-bootstrap";
import { OverlayTrigger, Tooltip } from "react-bootstrap";

interface RecentlyCreatedResponse {
  crates: RecentlyCreatedResponseVersion[];
@@ -42,10 +42,10 @@
  const auth = useAuth();

  const { response: recentlyCreated, error: recentlyCreatedError } =
      useAuthenticatedRequest<RecentlyCreatedResponse>({
        auth,
        endpoint: "crates/recently-created",
      });
    useAuthenticatedRequest<RecentlyCreatedResponse>({
      auth,
      endpoint: "crates/recently-created",
    });

  const { response: recentlyUpdated, error: recentlyUpdatedError } =
    useAuthenticatedRequest<RecentlyUpdatedResponse>({
@@ -54,10 +54,10 @@
    });

  const { response: mostDownloaded, error: mostDownloadedError } =
      useAuthenticatedRequest<MostDownloadedResponse>({
        auth,
        endpoint: "crates/most-downloaded",
      });
    useAuthenticatedRequest<MostDownloadedResponse>({
      auth,
      endpoint: "crates/most-downloaded",
    });

  return (
    <div className="text-white">
@@ -84,40 +84,50 @@
          <div className="col-12 col-md-4">
            <h4>Newly Created</h4>
            {(recentlyCreated?.crates || []).map((v) => (
                <CrateCard key={v.name} organisation={v.organisation} name={v.name}>
                  <OverlayTrigger
                      overlay={
                        <Tooltip
                            id={`tooltip-${v.name}-date`}
                        >
                          {new Date(v.created_at).toLocaleString()}
                        </Tooltip>
                      }
                  >
                    <span>
                      <Calendar3 />{" "}
                      <HumanTime
                          time={new Date(v.created_at).getTime()}
                      />
                    </span>
                  </OverlayTrigger>
                </CrateCard>
              <CrateCard
                key={v.name}
                organisation={v.organisation}
                name={v.name}
              >
                <OverlayTrigger
                  overlay={
                    <Tooltip id={`tooltip-${v.name}-date`}>
                      {new Date(v.created_at).toLocaleString()}
                    </Tooltip>
                  }
                >
                  <span>
                    <Calendar3 />{" "}
                    <HumanTime time={new Date(v.created_at).getTime()} />
                  </span>
                </OverlayTrigger>
              </CrateCard>
            ))}
          </div>

          <div className="col-12 col-md-4">
            <h4>Recently Updated</h4>
            {(recentlyUpdated?.versions || []).map((v) => (
                <CrateCard key={v.name} organisation={v.organisation} name={v.name}>v{v.version}</CrateCard>
              <CrateCard
                key={v.name}
                organisation={v.organisation}
                name={v.name}
              >
                v{v.version}
              </CrateCard>
            ))}
          </div>

          <div className="col-12 col-md-4">
            <h4>Most Downloaded</h4>
            {(mostDownloaded?.crates || []).map((v) => (
                <CrateCard key={v.name} organisation={v.organisation} name={v.name}>
                  <Download /> {v.downloads.toLocaleString()}
                </CrateCard>
              <CrateCard
                key={v.name}
                organisation={v.organisation}
                name={v.name}
              >
                <Download /> {v.downloads.toLocaleString()}
              </CrateCard>
            ))}
          </div>
        </div>
@@ -126,7 +136,11 @@
  );
}

function CrateCard({ name, organisation, children }: React.PropsWithChildren<{ name: string, organisation: string }>) {
function CrateCard({
  name,
  organisation,
  children,
}: React.PropsWithChildren<{ name: string; organisation: string }>) {
  return (
    <Link
      to={`/crates/${organisation}/${name}`}
diff --git a/chartered-frontend/src/sections/Nav.tsx b/chartered-frontend/src/sections/Nav.tsx
index 772f065..531ae82 100644
--- a/chartered-frontend/src/sections/Nav.tsx
+++ a/chartered-frontend/src/sections/Nav.tsx
@@ -1,9 +1,11 @@
import React = require("react");
import { useHistory, useLocation } from "react-router-dom";
import { NavLink, Link } from "react-router-dom";

import { BoxArrowRight, Search } from "react-bootstrap-icons";
import { BoxArrowRight, CaretDownFill, Search } from "react-bootstrap-icons";
import { useAuth } from "../useAuth";
import { Dropdown, Navbar, NavDropdown, NavItem } from "react-bootstrap";
import { ProfilePicture } from "../util";

export default function Nav() {
  const auth = useAuth();
@@ -29,25 +31,15 @@
  };

  return (
    <nav className="navbar navbar-expand-lg navbar-light bg-white shadow-sm">
    <Navbar bg="light" expand="md" className="bg-white shadow-sm">
      <div className="container-fluid">
        <Link className="navbar-brand" to="/dashboard">
          ✈️ chartered
        </Link>
        <button
          className="navbar-toggler"
          type="button"
          data-bs-toggle="collapse"
          data-bs-target="#navbarSupportedContent"
          aria-controls="navbarSupportedContent"
          aria-expanded="false"
          aria-label="Toggle navigation"
        >
          <span className="navbar-toggler-icon"></span>
        </button>

        <div className="collapse navbar-collapse" id="navbarSupportedContent">
          <ul className="navbar-nav me-auto mb-2 mb-lg-0">
        <Navbar.Toggle aria-controls="navbar-contents" />

        <Navbar.Collapse id="navbar-contents" role="navigation">
          <ul className="navbar-nav mb-2 mb-md-0 me-auto">
            <li className="nav-item">
              <NavLink to="/dashboard" className="nav-link">
                Home
@@ -82,13 +74,47 @@
            </div>
          </form>

          <div>
            <a href="#" onClick={logout} className="nav-link text-danger">
              Logout <BoxArrowRight />
            </a>
          </div>
        </div>
          <ul className="navbar-nav">
            <li className="nav-item">
              <Dropdown as="div" className="mt-2 mt-md-0">
                <Dropdown.Toggle
                  as="a"
                  role="button"
                  aria-label="View profile and more"
                  style={{ color: "rgba(0, 0, 0, 0.55)" }}
                  className="d-inline-flex align-items-center"
                >
                  <ProfilePicture
                    src={auth.getPictureUrl()}
                    height="2rem"
                    width="2rem"
                  />
                </Dropdown.Toggle>

                <Dropdown.Menu
                  align={{ md: "end" }}
                  style={{ marginTop: "5px" }}
                >
                  <Dropdown.Item as={Link} to={`/users/${auth.getUserUuid()}`}>
                    Your profile
                  </Dropdown.Item>

                  <Dropdown.Divider />

                  <Dropdown.Item
                    as="a"
                    href="#"
                    onClick={logout}
                    className="text-danger"
                  >
                    Logout <BoxArrowRight />
                  </Dropdown.Item>
                </Dropdown.Menu>
              </Dropdown>
            </li>
          </ul>
        </Navbar.Collapse>
      </div>
    </nav>
    </Navbar>
  );
}
diff --git a/chartered-frontend/src/pages/organisations/CreateOrganisation.tsx b/chartered-frontend/src/pages/organisations/CreateOrganisation.tsx
index 75de76e..b4b7006 100644
--- a/chartered-frontend/src/pages/organisations/CreateOrganisation.tsx
+++ a/chartered-frontend/src/pages/organisations/CreateOrganisation.tsx
@@ -33,7 +33,7 @@
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({ name, description, 'public': publicOrg }),
        body: JSON.stringify({ name, description, public: publicOrg }),
      });
      let json = await res.json();

@@ -70,7 +70,7 @@
            className="btn-close"
            aria-label="Close"
            onClick={() => setError("")}
            />
          />
        </div>

        <div className="card border-0 shadow-sm text-black">
@@ -111,15 +111,16 @@

              <div className="mt-2 form-check">
                <input
                    type="checkbox"
                    checked={publicOrg}
                    id="org-public"
                    className="form-check-input"
                    onChange={(e) => setPublicOrg(e.target.checked)}
                    disabled={loading}
                  type="checkbox"
                  checked={publicOrg}
                  id="org-public"
                  className="form-check-input"
                  onChange={(e) => setPublicOrg(e.target.checked)}
                  disabled={loading}
                />
                <label htmlFor="org-public" className="form-check-label">
                  Give <strong>VISIBLE</strong> permission to all logged in users
                  Give <strong>VISIBLE</strong> permission to all logged in
                  users
                </label>
              </div>

diff --git a/chartered-web/src/endpoints/web_api/auth/mod.rs b/chartered-web/src/endpoints/web_api/auth/mod.rs
index 3711f44..1b3303e 100644
--- a/chartered-web/src/endpoints/web_api/auth/mod.rs
+++ a/chartered-web/src/endpoints/web_api/auth/mod.rs
@@ -38,6 +38,7 @@
    user_uuid: Uuid,
    key: String,
    expires: chrono::DateTime<chrono::Utc>,
    picture_url: Option<String>,
}

pub async fn login(
@@ -67,5 +68,6 @@
        user_uuid: user.uuid.0,
        key: key.session_key,
        expires,
        picture_url: user.picture_url,
    })
}