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(-)
@@ -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();
@@ -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"
@@ -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,
};
}
@@ -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>
);
}
@@ -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}`}
@@ -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>
);
}
@@ -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>
@@ -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,
})
}