Skip to content

Commit

Permalink
add user management page, RowyRunModal, ConfirmDialog
Browse files Browse the repository at this point in the history
  • Loading branch information
notsidney committed May 2, 2022
1 parent 0b51acc commit 70fb0c6
Show file tree
Hide file tree
Showing 31 changed files with 1,342 additions and 50 deletions.
2 changes: 1 addition & 1 deletion emulators/auth_export/accounts.json
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1651129533821","displayName":"Admin User","photoUrl":"","customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","email":"[email protected]"}],"validSince":"1651117599","email":"[email protected]","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-04-29T00:47:58.946Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651023059443","displayName":"Editor User","providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"[email protected]"}],"validSince":"1651117599","email":"[email protected]","emailVerified":true,"disabled":false}]}
{"kind":"identitytoolkit#DownloadAccountResponse","users":[{"localId":"26CJMrwlouNRwkiLofNK07DNgKhw","createdAt":"1651022832613","lastLoginAt":"1651297974462","displayName":"Admin User","photoUrl":"","customAttributes":"{\"roles\": [\"ADMIN\"]}","providerUserInfo":[{"providerId":"google.com","rawId":"abc123","federatedId":"abc123","displayName":"Admin User","email":"[email protected]"}],"validSince":"1651195467","email":"[email protected]","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-04-30T08:44:58.158Z"},{"localId":"3xTRVPnJGT2GE6lkiWKZp1jShuXj","createdAt":"1651023059442","lastLoginAt":"1651223181908","displayName":"Editor User","providerUserInfo":[{"providerId":"google.com","rawId":"1535779573397289142795231390488730790451","federatedId":"1535779573397289142795231390488730790451","displayName":"Editor User","email":"[email protected]"}],"validSince":"1651195467","email":"[email protected]","emailVerified":true,"disabled":false,"lastRefreshAt":"2022-04-30T08:44:53.855Z"}]}
Binary file not shown.
Binary file modified emulators/firestore_export/all_namespaces/all_kinds/output-0
Binary file not shown.
Binary file not shown.
2 changes: 2 additions & 0 deletions eslint-local-rules.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const JOTAI_USE_ATOM_HOOKS = [
"useAtom",
"useSetAtom",
"useAtomValue",
"useUpdateAtom",
"useAtomValue",
"useResetAtom",
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@
"dompurify": "^2.3.6",
"firebase": "^9.6.11",
"firebaseui": "^6.0.1",
"jotai": "^1.6.4",
"jotai": "^1.6.5",
"lodash-es": "^4.17.21",
"match-sorter": "^6.3.1",
"notistack": "^2.0.4",
"react": "^18.0.0",
"react-color-palette": "^6.2.0",
Expand Down
12 changes: 11 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { useAtom } from "jotai";

import Loading from "@src/components/Loading";
import ProjectSourceFirebase from "@src/sources/ProjectSourceFirebase";
import ConfirmDialog from "@src/components/ConfirmDialog";
import RowyRunModal from "@src/components/RowyRunModal";
import NotFound from "@src/pages/NotFound";
import RequireAuth from "@src/layouts/RequireAuth";
import Navigation from "@src/layouts/Navigation";
Expand Down Expand Up @@ -32,6 +34,8 @@ const UserSettingsPage = lazy(() => import("@src/pages/Settings/UserSettings" /*
// prettier-ignore
const ProjectSettingsPage = lazy(() => import("@src/pages/Settings/ProjectSettings" /* webpackChunkName: "ProjectSettingsPage" */));
// prettier-ignore
const UserManagementPage = lazy(() => import("@src/pages/Settings/UserManagement" /* webpackChunkName: "UserManagementPage" */));
// prettier-ignore
// const RowyRunTestPage = lazy(() => import("@src/pages/RowyRunTest" /* webpackChunkName: "RowyRunTestPage" */));

export default function App() {
Expand All @@ -40,6 +44,8 @@ export default function App() {
return (
<Suspense fallback={<Loading fullScreen />}>
<ProjectSourceFirebase />
<ConfirmDialog />
<RowyRunModal/>

{currentUser === undefined ? (
<Loading fullScreen message="Authenticating" />
Expand Down Expand Up @@ -76,10 +82,14 @@ export default function App() {
/>
<Route path={ROUTES.userSettings} element={<UserSettingsPage />} />
<Route path={ROUTES.projectSettings} element={<ProjectSettingsPage />} />
<Route path={ROUTES.userManagement} element={<UserManagementPage />} />
{/* <Route path={ROUTES.rowyRunTest} element={<RowyRunTestPage />} /> */}
</Route>

<Route path="/jotaiTest" element={<JotaiTestPage />} />

</Route>

{/* <Route path="/jotaiTest" element={<JotaiTestPage />} /> */}
</Routes>
)}
</Suspense>
Expand Down
15 changes: 11 additions & 4 deletions src/atoms/project.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { atom } from "jotai";
import { atom, PrimitiveAtom } from "jotai";
import { sortBy } from "lodash-es";
import { ThemeOptions } from "@mui/material";

import { userRolesAtom } from "./auth";
import { UpdateFunction } from "./types";
import { UpdateDocFunction, UpdateCollectionFunction } from "./types";
import { UserSettings } from "./user";

export const projectIdAtom = atom<string>("");

Expand All @@ -27,7 +28,7 @@ export type PublicSettings = Partial<{
export const publicSettingsAtom = atom<PublicSettings>({});
/** Stores a function that updates public settings */
export const updatePublicSettingsAtom =
atom<UpdateFunction<PublicSettings> | null>(null);
atom<UpdateDocFunction<PublicSettings> | null>(null);

/** Project settings are visible to authenticated users */
export type ProjectSettings = Partial<{
Expand All @@ -48,7 +49,7 @@ export type ProjectSettings = Partial<{
export const projectSettingsAtom = atom<ProjectSettings>({});
/** Stores a function that updates project settings */
export const updateProjectSettingsAtom =
atom<UpdateFunction<ProjectSettings> | null>(null);
atom<UpdateDocFunction<ProjectSettings> | null>(null);

/** Table settings stored in project settings */
export type TableSettings = {
Expand Down Expand Up @@ -98,3 +99,9 @@ export const rolesAtom = atom((get) =>
)
)
);

/** User management page: all users */
export const allUsersAtom = atom<UserSettings[]>([]);
/** Stores a function that updates a user document */
export const updateUserAtom =
atom<UpdateCollectionFunction<UserSettings> | null>(null);
15 changes: 11 additions & 4 deletions src/atoms/rowyRun.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@ export interface IRowyRunRequestProps {
json?: boolean;
/** Optionally pass an abort signal to abort the request */
signal?: AbortSignal;
/** Optionally pass a callback that’s called if Rowy Run not set up */
handleNotSetUp?: () => void;
}

/**
* An atom that returns a function to call Rowy Run endpoints using the URL
* defined in project settings and retrieving a JWT token
* defined in project settings and retrieving a JWT token.
*
* Returns `false` if user not signed in or Rowy Run not set up.
*
* @example Basic usage:
* ```
Expand All @@ -65,10 +69,12 @@ export const rowyRunAtom = atom((get) => {
body,
signal,
json = true,
}: IRowyRunRequestProps): Promise<Response | any | void> => {
handleNotSetUp,
}: IRowyRunRequestProps): Promise<Response | any | false> => {
if (!currentUser) {
console.log("Rowy Run: Not signed in");
return;
if (handleNotSetUp) handleNotSetUp();
return false;
}
const authToken = await getIdTokenResult(currentUser!, forceRefresh);

Expand All @@ -79,7 +85,8 @@ export const rowyRunAtom = atom((get) => {
: rowyRunUrl;
if (!serviceUrl) {
console.log("Rowy Run: Not set up");
return;
if (handleNotSetUp) handleNotSetUp();
return false;
}

const { method, path } = route;
Expand Down
7 changes: 6 additions & 1 deletion src/atoms/types.d.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export type UpdateFunction<T> = (update: Partial<T>) => Promise<any>;
export type UpdateDocFunction<T> = (update: Partial<T>) => Promise<void>;

export type UpdateCollectionFunction<T> = (
path: string,
update: Partial<T>
) => Promise<void>;
71 changes: 71 additions & 0 deletions src/atoms/ui.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,77 @@
import { atom } from "jotai";
import { atomWithStorage } from "jotai/utils";

import { DialogProps, ButtonProps } from "@mui/material";

/** Nav open state stored in local storage. */
export const navOpenAtom = atomWithStorage("__ROWY__NAV_OPEN", false);
/** Nav pinned state stored in local storage. */
export const navPinnedAtom = atomWithStorage("__ROWY__NAV_PINNED", false);

export type ConfirmDialogProps = {
open: boolean;

title?: string;
/** Pass a string to display basic styled text */
body?: React.ReactNode;

/** Callback called when user clicks confirm */
handleConfirm?: () => void;
/** Optionally override confirm button text */
confirm?: string | JSX.Element;
/** Optionally require user to type this string to enable the confirm button */
confirmationCommand?: string;
/** Optionally set confirm button color */
confirmColor?: ButtonProps["color"];

/** Callback called when user clicks cancel */
handleCancel?: () => void;
/** Optionally override cancel button text */
cancel?: string;
/** Optionally hide cancel button */
hideCancel?: boolean;

/** Optionally set dialog max width */
maxWidth?: DialogProps["maxWidth"];
};
/**
* Open a confirm dialog
*
* @example Basic usage:
* ```
* const confirm = useSetAtom(confirmDialogAtom, globalScope);
* confirm({
* open: true,
* handleConfirm: () => ...
* });
* ```
*/
export const confirmDialogAtom = atom<ConfirmDialogProps>({ open: false });

/**
* Open global Rowy Run modal if feature not available
* {@link openRowyRunModalAtom | Use `openRowyRunModalAtom` to open}
*/
export const rowyRunModalAtom = atom({ open: false, feature: "", version: "" });
/**
* Helper atom to open Rowy Run Modal
*
* @example Basic usage:
* ```
* const openRowyRun = useSetAtom(openRowyRunModalAtom, globalScope);
* openRowyRun({
* feature: ...
* version: ...
* });
* ```
*/
export const openRowyRunModalAtom = atom(
null,
(_, set, update?: Partial<Record<"feature" | "version", string>>) => {
set(rowyRunModalAtom, {
open: true,
feature: update?.feature || "",
version: update?.version || "",
});
}
);
8 changes: 4 additions & 4 deletions src/atoms/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { ThemeOptions } from "@mui/material";
import themes from "@src/theme";
import { publicSettingsAtom } from "./project";
import { TableFilter } from "./table";
import { UpdateFunction } from "./types";
import { UpdateDocFunction } from "./types";

/** User info and settings */
export type UserSettings = Partial<{
_rowy_id: string;
/** Synced from user auth info */
user: {
email: string;
Expand All @@ -34,9 +35,8 @@ export type UserSettings = Partial<{
/** User info and settings */
export const userSettingsAtom = atom<UserSettings>({});
/** Stores a function that updates user settings */
export const updateUserSettingsAtom = atom<UpdateFunction<UserSettings> | null>(
null
);
export const updateUserSettingsAtom =
atom<UpdateDocFunction<UserSettings> | null>(null);

/**
* Stores which theme is currently active, based on user or OS setting.
Expand Down
93 changes: 93 additions & 0 deletions src/components/AccessDenied.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useAtom } from "jotai";

import {
Typography,
Stack,
Avatar,
Alert,
Divider,
Link as MuiLink,
Button,
} from "@mui/material";
import SecurityIcon from "@mui/icons-material/SecurityOutlined";

import EmptyState from "@src/components/EmptyState";

import {
globalScope,
currentUserAtom,
userRolesAtom,
} from "@src/atoms/globalScope";
import { WIKI_LINKS } from "@src/constants/externalLinks";
import { ROUTES } from "@src/constants/routes";

export default function AccessDenied() {
const [currentUser] = useAtom(currentUserAtom, globalScope);
const [userRoles] = useAtom(userRolesAtom, globalScope);

if (!currentUser) window.location.reload();

return (
<EmptyState
fullScreen
Icon={SecurityIcon}
message="Access denied"
description={
<>
<div style={{ textAlign: "left", width: "100%" }}>
<Stack
direction="row"
spacing={1.25}
alignItems="flex-start"
sx={{ mt: 2 }}
>
<Avatar src={currentUser?.photoURL || ""} />
<div>
<Typography>{currentUser?.displayName}</Typography>
<Typography variant="button">{currentUser?.email}</Typography>
</div>
</Stack>

{(!userRoles || userRoles.length === 0) && (
<Alert severity="warning" sx={{ mt: 2 }}>
Your account has no roles set
</Alert>
)}
</div>

<Typography>
You do not have access to this project. Please contact the project
owner.
</Typography>

<Button href={ROUTES.signOut}>Sign out</Button>

<Divider flexItem sx={{ typography: "overline" }}>
OR
</Divider>

<Typography>
If you are the project owner, please follow{" "}
<MuiLink
href={WIKI_LINKS.setupRoles}
target="_blank"
rel="noopener noreferrer"
>
these instructions
</MuiLink>{" "}
to set up this project’s security rules.
</Typography>
</>
}
sx={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
bgcolor: "background.default",
zIndex: 9999,
}}
/>
);
}
Loading

0 comments on commit 70fb0c6

Please sign in to comment.