From f602a22b1bd16b7c28123ceab35bd01966338ce7 Mon Sep 17 00:00:00 2001 From: Carter Date: Thu, 31 Oct 2024 20:30:11 -0600 Subject: [PATCH] added state containers, user model, and the button for user menu --- package-lock.json | 15 +++++ package.json | 2 + src/app/Header.tsx | 18 +++++- src/app/UserWrapper.tsx | 50 ++++++++++----- src/models/user.ts | 103 ++++++++++++++++++++++++++++++ src/providers/StateContainer.tsx | 66 ++++++++++++++++++++ src/user/UserMenu.tsx | 104 +++++++++++++++++++++++++++++++ 7 files changed, 341 insertions(+), 17 deletions(-) create mode 100644 src/models/user.ts create mode 100644 src/providers/StateContainer.tsx create mode 100644 src/user/UserMenu.tsx diff --git a/package-lock.json b/package-lock.json index 4f88974..9ffaf4e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@mui/styles": "^6.1.6", "@types/classnames": "^2.3.0", "axios": "^1.7.7", + "lodash": "^4.17.21", "moment": "^2.30.1", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "react": "^18.3.1", @@ -25,6 +26,7 @@ "devDependencies": { "@babel/types": "^7.25.8", "@eslint/js": "^9.11.1", + "@types/lodash": "^4.17.13", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.2", @@ -1857,6 +1859,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/lodash": { + "version": "4.17.13", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.13.tgz", + "integrity": "sha512-lfx+dftrEZcdBPczf9d0Qv0x+j/rfNCMuC6OcfXmO8gkfeNAY88PgKUbvG56whcN23gc27yenwF6oJZXGFpYxg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/long": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", @@ -3626,6 +3635,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", diff --git a/package.json b/package.json index 2a6ebee..2c4df44 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@mui/styles": "^6.1.6", "@types/classnames": "^2.3.0", "axios": "^1.7.7", + "lodash": "^4.17.21", "moment": "^2.30.1", "protobuf-ts": "git+https://gitlab+deploy-token-50627:hv8mB4WkyvtjBpJKU1rN@gitlab.com/brandx/protobuf-ts.git#staging", "react": "^18.3.1", @@ -34,6 +35,7 @@ "devDependencies": { "@babel/types": "^7.25.8", "@eslint/js": "^9.11.1", + "@types/lodash": "^4.17.13", "@types/react": "^18.3.10", "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.2", diff --git a/src/app/Header.tsx b/src/app/Header.tsx index 29ca19b..04ceb2d 100644 --- a/src/app/Header.tsx +++ b/src/app/Header.tsx @@ -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) { )} + + + ) diff --git a/src/app/UserWrapper.tsx b/src/app/UserWrapper.tsx index 2f87905..3550b15 100644 --- a/src/app/UserWrapper.tsx +++ b/src/app/UserWrapper.tsx @@ -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(undefined) const [message, setMessage] = useState("Loading user profile...") + const [global, setGlobal] = useState(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 ( ) return ( - - -

Vite + React

-
+ + + +

Vite + React

+
@@ -71,6 +89,8 @@ export default function UserWrapper() {

Coming soon

-
+ +
+ ) } \ No newline at end of file diff --git a/src/models/user.ts b/src/models/user.ts new file mode 100644 index 0000000..842a077 --- /dev/null +++ b/src/models/user.ts @@ -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); + } +} diff --git a/src/providers/StateContainer.tsx b/src/providers/StateContainer.tsx new file mode 100644 index 0000000..1558d9d --- /dev/null +++ b/src/providers/StateContainer.tsx @@ -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; +// 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]>([ + {} as GlobalState, + {} as Dispatch +]); + +interface StateProviderProps { + state: GlobalState; + reducer: (ctx: GlobalState, action: GlobalStateAction) => GlobalState; +// user: User; + children?: any; +} + +export const StateProvider = (props: StateProviderProps) => { + return ( + + {props.children} + + ); +}; + +//-------------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; + + 3. Access the context where x is a key of context: + const [{x}, dispatch] = this.context; +*/ diff --git a/src/user/UserMenu.tsx b/src/user/UserMenu.tsx new file mode 100644 index 0000000..8bfdbdf --- /dev/null +++ b/src/user/UserMenu.tsx @@ -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 ( + + ) +} \ No newline at end of file