Skip to content

Commit

Permalink
feat: add login screen
Browse files Browse the repository at this point in the history
  • Loading branch information
huwshimi committed Apr 19, 2024
1 parent b634016 commit 1befe87
Show file tree
Hide file tree
Showing 12 changed files with 307 additions and 32 deletions.
2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
},
"dependencies": {
"@canonical/react-components": "0.51.1",
"@canonical/rebac-admin": "0.0.1-alpha.1",
"@canonical/rebac-admin": "0.0.1-alpha.2",
"@tanstack/react-query": "^5.28.6",
"@use-it/event-listener": "0.1.7",
"axios": "1.6.8",
Expand Down
69 changes: 54 additions & 15 deletions ui/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,68 @@
import { FC, Suspense } from "react";
import { Navigate, Route, Routes } from "react-router-dom";
import { ReBACAdmin } from "@canonical/rebac-admin";
import Loader from "components/Loader";
import Login from "components/Login";
import ClientList from "pages/clients/ClientList";
import NoMatch from "components/NoMatch";
import ProviderList from "pages/providers/ProviderList";
import IdentityList from "pages/identities/IdentityList";
import SchemaList from "pages/schemas/SchemaList";
import { ReBACAdmin } from "@canonical/rebac-admin";
import Navigation from "components/Navigation";
import Panels from "components/Panels";
import useLocalStorage from "util/useLocalStorage";

const App: FC = () => {
// Store a user token that will be passed to the API using the
// X-Authorization header so that the user can be identified. This will be
// replaced by API authentication when it has been implemented.
const [authUser, setAuthUser] = useLocalStorage<{
username: string;
token: string;
} | null>("user", null);
return (
<Suspense fallback={<Loader />}>
<Routes>
<Route path="/" element={<Navigate to="/provider" replace={true} />} />
<Route path="/provider" element={<ProviderList />} />
<Route path="/client" element={<ClientList />} />
<Route path="/identity" element={<IdentityList />} />
<Route path="/schema" element={<SchemaList />} />
<Route
path="/*"
element={<ReBACAdmin apiURL={import.meta.env.VITE_API_URL} />}
/>
<Route path="*" element={<NoMatch />} />
</Routes>
</Suspense>
<div className="l-application" role="presentation">
<Navigation
username={authUser?.username}
logout={() => {
setAuthUser(null);
window.location.reload();
}}
/>
<main className="l-main">
<Suspense fallback={<Loader />}>
<Routes>
<Route
path="/"
element={
<Login isAuthenticated={!!authUser} setAuthUser={setAuthUser} />
}
>
<Route
path="/"
element={<Navigate to="/provider" replace={true} />}
/>
<Route path="/provider" element={<ProviderList />} />
<Route path="/client" element={<ClientList />} />
<Route path="/identity" element={<IdentityList />} />
<Route path="/schema" element={<SchemaList />} />
<Route
path="/*"
element={
<ReBACAdmin
apiURL={import.meta.env.VITE_API_URL}
asidePanelId="rebac-admin-panel"
authToken={authUser?.token}
/>
}
/>
<Route path="*" element={<NoMatch />} />
</Route>
</Routes>
</Suspense>
</main>
<Panels />
</div>
);
};

Expand Down
59 changes: 59 additions & 0 deletions ui/src/components/Login.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { Button, FormikField } from "@canonical/react-components";
import { Form, Formik } from "formik";
import { FC } from "react";
import { Outlet } from "react-router-dom";
import * as Yup from "yup";
import { AuthUser } from "types/auth-user";

type Props = {
isAuthenticated?: boolean;
setAuthUser: (user: AuthUser) => void;
};

const schema = Yup.object().shape({
username: Yup.string().required("Required"),
});

const Login: FC<Props> = ({ isAuthenticated, setAuthUser }) => {
return isAuthenticated ? (
<Outlet />
) : (
<div className="p-login">
<div className="p-login__inner p-card--highlighted">
<div className="p-login__tagged-logo">
<span className="p-login__logo-container">
<div className="p-login__logo-tag">
<img
className="p-login__logo-icon"
src="https://assets.ubuntu.com/v1/82818827-CoF_white.svg"
alt=""
/>
</div>
<span className="p-login__logo-title">Identity platform</span>
</span>
</div>
<Formik
initialValues={{ username: "" }}
onSubmit={({ username }) => {
setAuthUser({ username, token: btoa(username) });
}}
validationSchema={schema}
>
<Form>
<FormikField
label="Username"
name="username"
takeFocus
type="text"
/>
<Button appearance="positive" type="submit">
Log in
</Button>
</Form>
</Formik>
</div>
</div>
);
};

export default Login;
31 changes: 30 additions & 1 deletion ui/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import classnames from "classnames";
import Logo from "components/Logo";
import { GroupsLink, RolesLink } from "@canonical/rebac-admin";

const Navigation: FC = () => {
type Props = {
username?: string;
logout: () => void;
};

const Navigation: FC<Props> = ({ username, logout }) => {
return (
<>
<header className="l-navigation-bar">
Expand Down Expand Up @@ -99,6 +104,30 @@ const Navigation: FC = () => {
</li>
</ul>
</div>
<div className="p-side-navigation--icons is-dark p-side-navigation--user-menu">
<ul className="p-side-navigation__list">
<li className="p-side-navigation__item">
<span className="p-side-navigation__text">
<Icon
className="is-light p-side-navigation__icon"
name="user"
/>{" "}
{username}
</span>
</li>
<li className="p-side-navigation__item">
<Button
appearance="link"
className="p-side-navigation__link"
onClick={() => {
logout();
}}
>
Logout
</Button>
</li>
</ul>
</div>
</div>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions ui/src/components/Panels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@ const Panels = () => {
case panels.identityCreate:
return <IdentityCreate />;
default:
return null;
return <div id="rebac-admin-panel"></div>;
}
};
return <>{panelParams.panel && generatePanel()}</>;
return <>{generatePanel()}</>;
};

export default Panels;
10 changes: 1 addition & 9 deletions ui/src/index.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import { createRoot } from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import Navigation from "components/Navigation";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import App from "./App";
import "./sass/styles.scss";
import { NotificationProvider } from "@canonical/react-components";
import Panels from "components/Panels";

const queryClient = new QueryClient();

Expand All @@ -16,13 +14,7 @@ root.render(
<Router>
<QueryClientProvider client={queryClient}>
<NotificationProvider>
<div className="l-application" role="presentation">
<Navigation />
<main className="l-main">
<App />
</main>
<Panels />
</div>
<App />
</NotificationProvider>
</QueryClientProvider>
</Router>
Expand Down
78 changes: 78 additions & 0 deletions ui/src/sass/_login.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
.p-login {
align-items: center;
background-color: rgb(255 255 255 / 75%);
display: flex;
height: 100vh;
left: 0;
place-content: center;
position: fixed;
top: 0;
width: 100vw;
z-index: 999;

&__inner {
width: 20rem;

.p-button--positive {
margin: 0 auto;
width: 100%;
}
}

// A modified version of the navigation logo:
// https://github.com/canonical/vanilla-framework/blob/fe40c9d38febd159a280b8f70713e02598a3a3d2/scss/_patterns_navigation.scss#L314
.p-login__tagged-logo {
display: flex; // to prevent logo link from expanding full width
margin-right: 0;

@media (min-width: $breakpoint-navigation-threshold) {
min-width: 13rem;
}

.p-login__logo-tag {
background-color: $color-ubuntu;
height: $navigation-logo-tag-height;
left: 0;
position: absolute;
top: 0;
width: $navigation-logo-tag-width;

@media (min-width: $breakpoint-navigation-threshold) {
height: $navigation-logo-tag-height-desktop;
}
}

.p-login__logo-icon {
bottom: $spv-navigation-logo-bottom-position;
height: $navigation-logo-size;
left: 50%;
position: absolute;
transform: translate(-50%, 0);
width: $navigation-logo-size;
}

.p-login__logo-title {
color: $colors--theme--text-default;
font-size: 1.3rem;
font-weight: 300;
line-height: $navigation-logo-size;
}

.p-login__logo-container {
@extend %navigation-link;

// within logo we don't need a regular item padding
@extend %vf-reset-horizontal-padding;

padding-left: $navigation-logo-padding;

&:hover {
background-color: transparent !important;
}

&::before {
content: none;
}
}
}
}
10 changes: 10 additions & 0 deletions ui/src/sass/_navigation.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.p-side-navigation--user-menu {
bottom: 0;
position: absolute;
width: 100%;

button {
justify-content: left;
width: 100%;
}
}
2 changes: 2 additions & 0 deletions ui/src/sass/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@
@include vf-p-icon-user-group;

@import "forms";
@import "login";
@import "logo";
@import "navigation";
@import "scrollable_container";
4 changes: 4 additions & 0 deletions ui/src/types/auth-user.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type AuthUser = {
username: string;
token: string;
};
36 changes: 36 additions & 0 deletions ui/src/util/useLocalStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { useState } from "react";

function useLocalStorage<V>(
key: string,
initialValue: V,
): [V, (value: V) => void] {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState<V>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// Not shown in UI. Logged for debugging purposes.
console.error("Unable to parse local storage:", error);
return initialValue;
}
});

// Return a wrapped version of useState's setter function that persists the
// new value to localStorage.
const setValue = (value: V) => {
try {
const stringified = JSON.stringify(value);
setStoredValue(value);
window.localStorage.setItem(key, stringified);
} catch (error) {
// Not shown in UI. Logged for debugging purposes.
console.error(error);
}
};

return [storedValue, setValue];
}

export default useLocalStorage;
Loading

0 comments on commit 1befe87

Please sign in to comment.