added state containers, user model, and the button for user menu
This commit is contained in:
parent
4fd156f663
commit
f602a22b1b
7 changed files with 341 additions and 17 deletions
|
|
@ -5,6 +5,7 @@ import { makeStyles } from "@mui/styles";
|
|||
import { getDarkLogo, getLightLogo, getSignatureAccentColour, hasTransparentLogoBG, hideLogo } from "../services/whiteLabel";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useThemeType } from "../hooks/useThemeType";
|
||||
import UserMenu from "../user/UserMenu";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
appBar: {
|
||||
|
|
@ -53,10 +54,20 @@ const useStyles = makeStyles((theme: Theme) => ({
|
|||
height: "64px"
|
||||
}
|
||||
},
|
||||
appBarRight: {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
marginLeft: theme.spacing(2),
|
||||
marginRight: theme.spacing(1),
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginLeft: theme.spacing(3),
|
||||
marginRight: theme.spacing(2)
|
||||
}
|
||||
},
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
// toggleTheme: () => void;
|
||||
toggleTheme: () => void;
|
||||
sideIsOpen: boolean;
|
||||
openSide: () => void;
|
||||
// teams: Team[];
|
||||
|
|
@ -65,7 +76,7 @@ interface Props {
|
|||
|
||||
export default function Header(props: Props) {
|
||||
|
||||
const { sideIsOpen, openSide} = props;
|
||||
const { sideIsOpen, openSide, toggleTheme } = props;
|
||||
|
||||
const themeType = useThemeType();
|
||||
const classes = useStyles()
|
||||
|
|
@ -95,6 +106,9 @@ export default function Header(props: Props) {
|
|||
</div>
|
||||
)}
|
||||
</Box>
|
||||
<Box className={classes.appBarRight}>
|
||||
<UserMenu toggleTheme={toggleTheme} />
|
||||
</Box>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,15 @@ import { or } from '../utils/types'
|
|||
import LoadingScreen from './LoadingScreen'
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import NavigationContainer from '../navigation/NavigationContainer'
|
||||
import { GlobalState, GlobalStateAction, StateProvider } from '../providers/StateContainer'
|
||||
import { User } from '../models/user'
|
||||
|
||||
const reducer = (state: GlobalState, action: GlobalStateAction): GlobalState => {
|
||||
return {
|
||||
...state,
|
||||
[action.key]: action.value
|
||||
};
|
||||
};
|
||||
|
||||
export default function UserWrapper() {
|
||||
const [count, setCount] = useState(0)
|
||||
|
|
@ -18,6 +26,7 @@ export default function UserWrapper() {
|
|||
const hasFetched = useRef(false);
|
||||
const [user, setUser] = useState<pond.User | undefined>(undefined)
|
||||
const [message, setMessage] = useState("Loading user profile...")
|
||||
const [global, setGlobal] = useState<undefined | GlobalState>(undefined)
|
||||
|
||||
let user_id = or(useAuth.user?.sub, "")
|
||||
|
||||
|
|
@ -26,7 +35,15 @@ export default function UserWrapper() {
|
|||
setLoading(true)
|
||||
userAPI.getUser(user_id).then(resp => {
|
||||
setUser(resp)
|
||||
setGlobal({
|
||||
user: User.create(resp),
|
||||
as: "",
|
||||
showErrors: false,
|
||||
userTeamPermissions: [],
|
||||
backgroundTasksComplete: false
|
||||
})
|
||||
}).catch(err => {
|
||||
console.log(err.toString())
|
||||
if (err.toString().includes("CORS")) {
|
||||
setMessage("CORS error")
|
||||
} else {
|
||||
|
|
@ -34,8 +51,8 @@ export default function UserWrapper() {
|
|||
}
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false)
|
||||
hasFetched.current = true;
|
||||
setLoading(false)
|
||||
hasFetched.current = true;
|
||||
})
|
||||
}, [])
|
||||
|
||||
|
|
@ -44,24 +61,25 @@ export default function UserWrapper() {
|
|||
loadUser()
|
||||
}, [loading])
|
||||
|
||||
if (!user || loading) return (
|
||||
if (!user || loading || !global) return (
|
||||
<LoadingScreen
|
||||
message={message}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<StateProvider state={global} reducer={reducer}>
|
||||
<NavigationContainer>
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src={viteLogo} className="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://react.dev" target="_blank">
|
||||
<img src={reactLogo} className="logo react" alt="React logo" />
|
||||
</a>
|
||||
</div>
|
||||
<h1>Vite + React</h1>
|
||||
<div className="card">
|
||||
<button onClick={() => setCount((count) => count + 1)}>
|
||||
count is {count}
|
||||
</button>
|
||||
|
|
@ -71,6 +89,8 @@ export default function UserWrapper() {
|
|||
</div><p className="read-the-docs">
|
||||
Coming soon
|
||||
</p>
|
||||
</NavigationContainer>
|
||||
</NavigationContainer>
|
||||
</StateProvider>
|
||||
|
||||
)
|
||||
}
|
||||
103
src/models/user.ts
Normal file
103
src/models/user.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { pond } from "protobuf-ts/pond";
|
||||
import { or } from "../utils/types";
|
||||
import { cloneDeep } from "lodash";
|
||||
|
||||
const adminActions = [
|
||||
"provision",
|
||||
"upload-firmware",
|
||||
"remove-devices",
|
||||
"copy-token",
|
||||
"recluse",
|
||||
"pause-data"
|
||||
];
|
||||
|
||||
const adminFeatures = [
|
||||
"beta",
|
||||
"billing",
|
||||
"security",
|
||||
"dev-channel",
|
||||
"docs",
|
||||
"maps",
|
||||
"sleep",
|
||||
"support",
|
||||
"json",
|
||||
"tasks",
|
||||
"teams",
|
||||
"developer"
|
||||
];
|
||||
|
||||
export class User {
|
||||
public settings: pond.UserSettings = pond.UserSettings.create();
|
||||
public status: pond.UserStatus = pond.UserStatus.create();
|
||||
public permissions: pond.Permission[] = [];
|
||||
public preferences: pond.UserPreferences = pond.UserPreferences.create();
|
||||
|
||||
public static create(pb?: pond.User): User {
|
||||
let my = new User();
|
||||
if (pb) {
|
||||
my.settings = pond.UserSettings.fromObject(cloneDeep(or(pb.settings, {})));
|
||||
my.status = pond.UserStatus.fromObject(cloneDeep(or(pb.status, {})));
|
||||
my.permissions = cloneDeep(pb.permissions);
|
||||
my.preferences = pond.UserPreferences.fromObject(cloneDeep(or(pb.preferences, {})));
|
||||
}
|
||||
return my;
|
||||
}
|
||||
|
||||
public static any(data: any): User {
|
||||
return User.create(pond.User.fromObject(cloneDeep(data)));
|
||||
}
|
||||
|
||||
public empty(): boolean {
|
||||
return this.id() === "";
|
||||
}
|
||||
|
||||
public id(): string {
|
||||
return this.settings.id;
|
||||
}
|
||||
|
||||
public name(): string {
|
||||
return this.settings.name !== "" ? this.settings.name : "User " + this.settings.id;
|
||||
}
|
||||
|
||||
public static clone(other?: User): User {
|
||||
if (other) {
|
||||
return User.create(
|
||||
pond.User.fromObject({
|
||||
settings: pond.UserSettings.fromObject(cloneDeep(or(other.settings, {}))),
|
||||
status: pond.UserStatus.fromObject(cloneDeep(or(other.status, {}))),
|
||||
permissions: cloneDeep(other.permissions),
|
||||
preferences: pond.UserPreferences.fromObject(cloneDeep(or(other.preferences, {})))
|
||||
})
|
||||
);
|
||||
}
|
||||
return User.create();
|
||||
}
|
||||
|
||||
public protobuf(): pond.User {
|
||||
return pond.User.fromObject({
|
||||
settings: this.settings,
|
||||
status: this.status
|
||||
});
|
||||
}
|
||||
|
||||
public hasAdmin(): boolean {
|
||||
if (this.settings.features.includes("admin")) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public allowedTo(action: string): boolean {
|
||||
if (this.hasAdmin() && adminActions.includes(action)) {
|
||||
return true;
|
||||
}
|
||||
return this.settings.actions.includes(action);
|
||||
}
|
||||
|
||||
public hasFeature(flag: string): boolean {
|
||||
if (this.hasAdmin() && adminFeatures.includes(flag)) {
|
||||
return true;
|
||||
}
|
||||
return this.settings.features.includes(flag);
|
||||
}
|
||||
}
|
||||
66
src/providers/StateContainer.tsx
Normal file
66
src/providers/StateContainer.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { createContext, useContext, useReducer, Dispatch } from "react";
|
||||
// import { User } from "../models";
|
||||
import { pond } from "protobuf-ts/pond";
|
||||
import { User } from "../models/user";
|
||||
|
||||
export interface GlobalState {
|
||||
// tags: Tag[];
|
||||
user: User;
|
||||
// team: Team;
|
||||
as: string;
|
||||
// firmware: Map<string, Firmware>;
|
||||
// newStructure: boolean;
|
||||
showErrors: boolean;
|
||||
userTeamPermissions: pond.Permission[];
|
||||
backgroundTasksComplete: boolean;
|
||||
}
|
||||
|
||||
export interface GlobalStateAction {
|
||||
key: keyof GlobalState;
|
||||
value: GlobalState[keyof GlobalState];
|
||||
}
|
||||
|
||||
export const StateContext = createContext<[GlobalState, Dispatch<GlobalStateAction>]>([
|
||||
{} as GlobalState,
|
||||
{} as Dispatch<GlobalStateAction>
|
||||
]);
|
||||
|
||||
interface StateProviderProps {
|
||||
state: GlobalState;
|
||||
reducer: (ctx: GlobalState, action: GlobalStateAction) => GlobalState;
|
||||
// user: User;
|
||||
children?: any;
|
||||
}
|
||||
|
||||
export const StateProvider = (props: StateProviderProps) => {
|
||||
return (
|
||||
<StateContext.Provider value={useReducer(props.reducer, props.state)}>
|
||||
{props.children}
|
||||
</StateContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
//-------------State container usage-------------
|
||||
|
||||
// a) Functional Components (hook)
|
||||
export const useGlobalState = () => useContext(StateContext);
|
||||
/*
|
||||
1. Import the hook:
|
||||
import { useGlobalState } from "providers";
|
||||
|
||||
2. Access the context members by binding it (x is an example context member);
|
||||
const [{x}, dispatch] = useGlobalState();
|
||||
*/
|
||||
|
||||
// b) Class Components (context)
|
||||
/*
|
||||
1. Import state context:
|
||||
import { StateContext } from "providers";
|
||||
|
||||
2. Add the context to the beginning of the class component:
|
||||
static contextType = StateContext;
|
||||
context!: React.ContextType<typeof StateContext>;
|
||||
|
||||
3. Access the context where x is a key of context:
|
||||
const [{x}, dispatch] = this.context;
|
||||
*/
|
||||
104
src/user/UserMenu.tsx
Normal file
104
src/user/UserMenu.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { Avatar, Button, PaletteColor, Theme, Typography, useTheme } from "@mui/material";
|
||||
import { useGlobalState } from "../providers/StateContainer";
|
||||
import { getSecondaryColour, getSignatureAccentColour } from "../services/whiteLabel";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { Person } from "@mui/icons-material";
|
||||
import { useEffect } from "react";
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
userAvatar: {
|
||||
color: "#fff",
|
||||
backgroundColor: getSecondaryColour()["700" as keyof PaletteColor],
|
||||
[theme.breakpoints.down("xs")]: {
|
||||
width: "32px",
|
||||
height: "32px"
|
||||
}
|
||||
},
|
||||
profileButton: {
|
||||
maxWidth: "50vw"
|
||||
},
|
||||
profileName: {
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
textTransform: "capitalize",
|
||||
color: getSignatureAccentColour(),
|
||||
marginBottom: -3,
|
||||
marginTop: -3
|
||||
},
|
||||
red: {
|
||||
color: "var(--status-alert)"
|
||||
}
|
||||
}));
|
||||
|
||||
interface Props {
|
||||
toggleTheme: () => void;
|
||||
}
|
||||
|
||||
export default function UserMenu(props: Props) {
|
||||
|
||||
const { toggleTheme } = props;
|
||||
const [{ user }] = useGlobalState();
|
||||
const classes = useStyles();
|
||||
const theme = useTheme();
|
||||
|
||||
const name = user.name();
|
||||
const picture = user.settings.avatar;
|
||||
const hasAdmin = user.hasFeature ? user.hasFeature("admin") : false;
|
||||
const allowedToCopyToken = user.allowedTo("copy-token");
|
||||
const hasTeams = user.hasFeature ? user.hasFeature("teams") : false;
|
||||
// const hasBilling = user.settings.useTeam
|
||||
// ? userTeamPermissions.includes(pond.Permission.PERMISSION_BILLING)
|
||||
// : true;
|
||||
|
||||
useEffect(() => {
|
||||
console.log(user)
|
||||
}, [user])
|
||||
|
||||
return (
|
||||
<Button
|
||||
id="tour-user-menu"
|
||||
// aria-owns={userMenuIsOpen ? "userMenu" : undefined}
|
||||
aria-haspopup="true"
|
||||
// onClick={openUserMenu}
|
||||
className={classes.profileButton}>
|
||||
<div style={{ display: "flex", flexDirection: "column" }}>
|
||||
<Typography className={classes.profileName} variant="button">
|
||||
{name}
|
||||
</Typography>
|
||||
{/* {team.settings.name && hasTeams && (
|
||||
<Typography className={classes.profileName} variant="button">
|
||||
({team.settings.name})
|
||||
</Typography>
|
||||
)} */}
|
||||
</div>
|
||||
<div style={{ marginLeft: theme.spacing(1) }}>
|
||||
{/* {picture && hasTeams ? (
|
||||
<div>
|
||||
<UserAvatar
|
||||
user={user}
|
||||
className={classes.userAvatar2}
|
||||
style={{ zIndex: as === "" ? 1000 : 0 }}
|
||||
/>
|
||||
<div style={{ position: "relative" }}>
|
||||
<Avatar
|
||||
src={team.settings.avatar}
|
||||
className={classes.teamAvatar}
|
||||
style={{ background: purple[500] }}>
|
||||
<TeamIcon style={{ fontSize: 38 }} />
|
||||
</Avatar>
|
||||
</div>
|
||||
</div>
|
||||
) : picture ? (
|
||||
<Avatar alt={name} src={picture} className={classes.userAvatar}>
|
||||
{!picture && name}
|
||||
</Avatar>
|
||||
) : ( */}
|
||||
<Avatar alt={name} src={picture} className={classes.userAvatar}>
|
||||
{/* <Person /> */}
|
||||
</Avatar>
|
||||
{/* )} */}
|
||||
</div>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue