Skip to content

Fix: auth error auto redirect & tree-sitter load path & toast notification #101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Nov 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions api/src/resolver_repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,11 +76,12 @@ export async function myCollabRepos(_, __, {userId}) {
}

export async function repo(_, { id }, { userId }) {
// a user can only access a private repo if he is the owner or a collaborator
const repo = await prisma.repo.findFirst({
where: { OR: [
{ id, public: true },
{ id, owner: { id: userId!} },
{ id, collaboratorIds: { has: userId!} },
{ id, owner: { id: userId || "undefined"} },
{ id, collaboratorIds: { has: userId || "undefined"} },
Comment on lines +83 to +84
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the purpose of the string "undefined" here?

Copy link
Collaborator Author

@li-xin-yi li-xin-yi Nov 29, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a guest visits a public repo, the userId could be null, but we should still return the repo. Though OR relationship may be short-circuited by the condition public==true, a null userId still causes a type error for the following two conditions. (Yes, we can check the public field first and then start another query, but it may bring overhead)

]
},
include: {
Expand All @@ -96,6 +97,7 @@ export async function repo(_, { id }, { userId }) {
},
},
});
if(!repo) throw Error("Repo not found");
return repo;
}

Expand Down
113 changes: 71 additions & 42 deletions ui/src/components/ShareProjDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,19 @@ import DialogContentText from "@mui/material/DialogContentText";
import TextField from "@mui/material/TextField";
import Button from "@mui/material/Button";
import Alert from "@mui/material/Alert";
import { useState } from "react";
import { useQuery, useMutation, gql } from "@apollo/client";
import { AlertColor } from "@mui/material/Alert";
import Snackbar from "@mui/material/Snackbar";
import { useState, useEffect } from "react";
import { useMutation, gql } from "@apollo/client";
import MuiAlert, { AlertProps } from "@mui/material/Alert";
import React from "react";

// const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
// props,
// ref,
// ) {
// return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />;
// });

interface ShareProjDialogProps {
open: boolean;
Expand All @@ -23,9 +34,9 @@ export function ShareProjDialog({
id,
}: ShareProjDialogProps) {
const [email, setEmail] = useState("");
const [alert, setAlert] = useState(false);
const [success, setSuccess] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [status, setStatus] = useState<AlertColor>("info");
const [message, setMessage] = useState("inviting...");
const [infoOpen, setInfoOpen] = useState(false);

const query = gql`
mutation addCollaborator($repoId: String, $email: String) {
Expand All @@ -37,10 +48,12 @@ export function ShareProjDialog({
const onChange = (e) => {
setEmail(e.target.value);
};

async function onShare() {
setInfoOpen(true);
if (email === "") {
setAlert(true);
setErrorMsg("Please enter an email address");
setStatus("error");
setMessage("Please enter an email address");
return;
}
try {
Expand All @@ -50,51 +63,67 @@ export function ShareProjDialog({
email,
},
});
setAlert(false);
setSuccess(true);
setStatus("success");
setMessage(`Invitation sent to ${email} successfully`);
// show the success message for 1 second before closing the dialog
setTimeout(() => {
onCloseHandler();
}, 1000);
console.log(status, message);
onCloseHandler();
} catch (error: any) {
setSuccess(false); // just in case
setAlert(true);
setErrorMsg(error?.message || "Unknown error");
setStatus("error"); // just in case
setMessage(error?.message || "Unknown error");
}
}

function onCloseHandler() {
setEmail("");
setAlert(false);
setSuccess(false);
onClose();
}

function onCloseAlert(event: React.SyntheticEvent | Event, reason?: string) {
if (reason === "clickaway") {
return;
}
setInfoOpen(false);
}

return (
<Dialog open={open} onClose={onCloseHandler}>
<DialogTitle> Share Project {title} with</DialogTitle>
{alert && <Alert severity="error"> {errorMsg} </Alert>}
{success && <Alert severity="success"> Invitation Sent </Alert>}
<DialogContent>
<DialogContentText>
Enter the email address of the person you want to share this project
with.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label="Email Address"
type="email"
variant="standard"
fullWidth
onChange={onChange}
/>
<DialogActions>
<Button onClick={onCloseHandler}>Cancel</Button>
<Button onClick={onShare}> Share</Button>
</DialogActions>
</DialogContent>
</Dialog>
<>
<Dialog open={open} onClose={onCloseHandler}>
<DialogTitle> Share Project {title} with</DialogTitle>

<DialogContent>
<DialogContentText>
Enter the email address of the person you want to share this project
with.
</DialogContentText>
<TextField
autoFocus
margin="dense"
id="name"
label="Email Address"
type="email"
variant="standard"
fullWidth
onChange={onChange}
/>
<DialogActions>
<Button
onClick={() => {
setInfoOpen(false);
onCloseHandler();
}}
>
Cancel
</Button>
<Button onClick={onShare}> Share</Button>
</DialogActions>
</DialogContent>
</Dialog>
<Snackbar open={infoOpen} autoHideDuration={3000} onClose={onCloseAlert}>
<Alert severity={status} onClose={onCloseAlert}>
{message}
</Alert>
</Snackbar>
</>
);
}
27 changes: 16 additions & 11 deletions ui/src/lib/fetch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,17 +50,22 @@ export async function doRemoteLoadRepo({ id, client }) {
}
}
`;
let res = await client.query({
query,
variables: {
id,
},
// CAUTION I must set this because refetechQueries does not work.
fetchPolicy: "no-cache",
});
// We need to do a deep copy here, because apollo client returned immutable objects.
let pods = res.data.repo.pods.map((pod) => ({ ...pod }));
return { pods, name: res.data.repo.name };
try {
let res = await client.query({
query,
variables: {
id,
},
// CAUTION I must set this because refetechQueries does not work.
fetchPolicy: "no-cache",
});
// We need to do a deep copy here, because apollo client returned immutable objects.
let pods = res.data.repo.pods.map((pod) => ({ ...pod }));
return { pods, name: res.data.repo.name, error: null };
} catch (e) {
console.log(e);
return { pods: [], name: "", error: e };
}
}

export function normalize(pods) {
Expand Down
2 changes: 1 addition & 1 deletion ui/src/lib/me.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const PROFILE_QUERY = gql`

export default function useMe() {
/* eslint-disable no-unused-vars */
const { client, loading, data } = useQuery(PROFILE_QUERY, {
const { loading, data } = useQuery(PROFILE_QUERY, {
// fetchPolicy: "network-only",
});
return { loading, me: data?.me };
Expand Down
2 changes: 1 addition & 1 deletion ui/src/lib/parser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Parser from "web-tree-sitter";
let parser: Parser | null = null;
Parser.init({
locateFile(scriptName: string, scriptDirectory: string) {
return scriptName;
return "/" + scriptName;
},
}).then(async () => {
/* the library is ready */
Expand Down
10 changes: 9 additions & 1 deletion ui/src/lib/store.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ const initialState = {
//TODO: all presence information are now saved in clients map for future usage. create a modern UI to show those information from clients (e.g., online users)
clients: new Map(),
showLineNumbers: false,
loadError: null,
};

export type Pod = {
Expand Down Expand Up @@ -136,6 +137,7 @@ export interface RepoSlice {
id2children: Record<string, string[]>;
// runtime: string;
repoId: string | null;
loadError: any;
// sessionId?: string;

resetState: () => void;
Expand Down Expand Up @@ -674,10 +676,16 @@ const createRepoSlice: StateCreator<
);
},
loadRepo: async (client, id) => {
const { pods, name } = await doRemoteLoadRepo({ id, client });
const { pods, name, error } = await doRemoteLoadRepo({ id, client });
set(
produce((state) => {
// TODO the children ordered by index
if (error) {
// TOFIX: If you enter a repo by URL directly, it may throw a repo not found error because of your user info is not loaded in time.
console.log("ERROR", error, id);
state.loadError = error;
return;
}
state.pods = normalize(pods);
state.repoName = name;
// fill in the parent/children relationships
Expand Down
48 changes: 46 additions & 2 deletions ui/src/pages/repo.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { useParams } from "react-router-dom";

import { useParams, useNavigate } from "react-router-dom";
import { Link as ReactLink } from "react-router-dom";
import Box from "@mui/material/Box";
import Link from "@mui/material/Link";
import Button from "@mui/material/Button";
import Alert from "@mui/material/Alert";
import AlertTitle from "@mui/material/AlertTitle";

import { useEffect, useState, useRef, useContext } from "react";

Expand Down Expand Up @@ -81,6 +84,40 @@ function RepoWrapper({ children }) {
);
}

function NotFoundAlert({ error }) {
const navigate = useNavigate();
const [seconds, setSeconds] = useState<number | null>(3);

useEffect(() => {
if (seconds === 0) {
setSeconds(null);
navigate("/");
return;
}
if (seconds === null) return;

const timer = setTimeout(() => {
setSeconds((prev) => prev! - 1);
}, 1000);

return () => clearTimeout(timer);
}, [seconds]);

return (
<Box sx={{ maxWidth: "sm", alignItems: "center", m: "auto" }}>
<Alert severity="error">
<AlertTitle>Error: {error}</AlertTitle>
The repo you are looking for is not found. Please check the URL. Go back
your{" "}
<Link component={ReactLink} to="/">
dashboard
</Link>{" "}
page in {seconds} seconds.
</Alert>
</Box>
);
}

function RepoImpl() {
let { id } = useParams();
const store = useContext(RepoContext);
Expand All @@ -89,6 +126,7 @@ function RepoImpl() {
const setRepo = useStore(store, (state) => state.setRepo);
const client = useApolloClient();
const loadRepo = useStore(store, (state) => state.loadRepo);
const loadError = useStore(store, (state) => state.loadError);
const setSessionId = useStore(store, (state) => state.setSessionId);
const repoLoaded = useStore(store, (state) => state.repoLoaded);
const setUser = useStore(store, (state) => state.setUser);
Expand Down Expand Up @@ -139,6 +177,12 @@ function RepoImpl() {

if (loading) return <Box>Loading</Box>;

// TOFIX: consider more types of error and display detailed error message in the future
// TOFIX: if the repo is not found, sidebar should not be rendered and runtime should not be lanuched.
if (!repoLoaded && loadError) {
return <NotFoundAlert error={loadError.message} />;
}

return (
<RepoWrapper>
{!repoLoaded && <Box>Repo Loading ...</Box>}
Expand Down
3 changes: 2 additions & 1 deletion ui/src/pages/repos/CreateRepoForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export default function CreateRepoForm(props: form = {}) {
fontWeight: 500,
}}
>
New Repo
New {isPrivate ? " Private " : " Public "} Repo
</DialogTitle>
<DialogContent>
<Formik
Expand All @@ -89,6 +89,7 @@ export default function CreateRepoForm(props: form = {}) {
variables: {
name: values.reponame,
id,
isPublic: !isPrivate,
},
});
if (res.data) {
Expand Down
Loading