diff --git a/api/src/resolver_repo.ts b/api/src/resolver_repo.ts index 204ffd75..acbc32b7 100644 --- a/api/src/resolver_repo.ts +++ b/api/src/resolver_repo.ts @@ -41,13 +41,22 @@ async function ensurePodEditAccess({ id, userId }) { } } -async function myRepos(_, __, { userId }) { +async function getDashboardRepos(_, __, { userId }) { if (!userId) throw Error("Unauthenticated"); const repos = await prisma.repo.findMany({ where: { - owner: { - id: userId, - }, + OR: [ + { + owner: { + id: userId, + }, + }, + { + collaborators: { + some: { id: userId }, + }, + }, + ], }, include: { UserRepoData: { @@ -58,24 +67,10 @@ async function myRepos(_, __, { userId }) { stargazers: true, }, }); - // Sort by last access time. - repos.sort((a, b) => { - if (a.UserRepoData.length > 0) { - if (b.UserRepoData.length > 0) { - return ( - b.UserRepoData[0].accessedAt.valueOf() - - a.UserRepoData[0].accessedAt.valueOf() - ); - } - return -1; - } - return a.updatedAt.valueOf() - b.updatedAt.valueOf(); - }); - // Re-use updatedAt field (this is actually the lastviewed field). return repos.map((repo) => { return { ...repo, - updatedAt: + accessedAt: repo.UserRepoData.length > 0 ? repo.UserRepoData[0].accessedAt : repo.updatedAt, @@ -83,21 +78,6 @@ async function myRepos(_, __, { userId }) { }); } -async function myCollabRepos(_, __, { userId }) { - if (!userId) throw Error("Unauthenticated"); - const repos = await prisma.repo.findMany({ - where: { - collaborators: { - some: { id: userId }, - }, - }, - include: { - stargazers: true, - }, - }); - return repos; -} - async function updateUserRepoData({ userId, repoId }) { // FIXME I should probably rename this from query to mutation? // @@ -609,9 +589,8 @@ async function copyRepo(_, { repoId }, { userId }) { export default { Query: { - myRepos, repo, - myCollabRepos, + getDashboardRepos, getVisibility, }, Mutation: { diff --git a/api/src/typedefs.ts b/api/src/typedefs.ts index 6405095f..aa8553e0 100644 --- a/api/src/typedefs.ts +++ b/api/src/typedefs.ts @@ -31,6 +31,7 @@ export const typeDefs = gql` public: Boolean createdAt: String updatedAt: String + accessedAt: String } type Edge { @@ -105,11 +106,10 @@ export const typeDefs = gql` repos: [Repo] repo(id: String!): Repo pod(id: ID!): Pod - myRepos: [Repo] + getDashboardRepos: [Repo] activeSessions: [String] getVisibility(repoId: String!): Visibility listAllRuntimes: [RuntimeInfo] - myCollabRepos: [Repo] infoRuntime(sessionId: String!): RuntimeInfo } diff --git a/ui/src/pages/repos.tsx b/ui/src/pages/repos.tsx index 6616a4ed..9a7d1432 100644 --- a/ui/src/pages/repos.tsx +++ b/ui/src/pages/repos.tsx @@ -7,80 +7,69 @@ import { useNavigate } from "react-router-dom"; import Box from "@mui/material/Box"; import Alert from "@mui/material/Alert"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableCell from "@mui/material/TableCell"; -import TableContainer from "@mui/material/TableContainer"; -import TableHead from "@mui/material/TableHead"; -import TableRow from "@mui/material/TableRow"; -import Paper from "@mui/material/Paper"; import DeleteIcon from "@mui/icons-material/Delete"; import StopCircleIcon from "@mui/icons-material/StopCircle"; import CircularProgress from "@mui/material/CircularProgress"; -import SourceIcon from "@mui/icons-material/Source"; import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; import StarIcon from "@mui/icons-material/Star"; import StarBorderIcon from "@mui/icons-material/StarBorder"; +import MoreVertIcon from "@mui/icons-material/MoreVert"; +import GroupsIcon from "@mui/icons-material/Groups"; +import PublicIcon from "@mui/icons-material/Public"; +import PublicOffIcon from "@mui/icons-material/PublicOff"; import Tooltip from "@mui/material/Tooltip"; import IconButton from "@mui/material/IconButton"; -import ShareIcon from "@mui/icons-material/Share"; -import Chip from "@mui/material/Chip"; -import { ShareProjDialog } from "../components/ShareProjDialog"; import useMe from "../lib/me"; import { getUpTime } from "../lib/utils"; import { Button, + Card, + CardActions, + CardHeader, Dialog, DialogActions, DialogContent, DialogTitle, Stack, - useTheme, } from "@mui/material"; import { useAuth } from "../lib/auth"; import { GoogleSignin } from "./login"; import { timeDifference } from "../lib/utils"; import { useSnackbar } from "notistack"; -const GET_REPOS = gql` - query GetRepos { - myRepos { - name - id - public - stargazers { - id - } - updatedAt - createdAt - } - } -`; - -function RepoLine({ - repo, - deletable, - sharable, - runtimeInfo, - onDeleteRepo, - deleting, -}) { - const { me } = useMe(); - const theme = useTheme(); - const [killRuntime] = useMutation( +function CreateRepoForm(props) { + const [createRepo] = useMutation( gql` - mutation killRuntime($sessionId: String!) { - killRuntime(sessionId: $sessionId) + mutation CreateRepo { + createRepo { + id + } } `, { - refetchQueries: ["ListAllRuntimes"], + refetchQueries: ["GetDashboardRepos"], } ); + const navigate = useNavigate(); + return ( + + + + ); +} - // haochen: any reason not using Loading state from useMutation? - const [killing, setKilling] = useState(false); - +const Star = ({ repo }) => { + const { me } = useMe(); const [star, { loading: starLoading }] = useMutation( gql` mutation star($repoId: ID!) { @@ -88,7 +77,7 @@ function RepoLine({ } `, { - refetchQueries: ["GetRepos"], + refetchQueries: ["GetDashboardRepos"], } ); const [unstar, { loading: unstarLoading }] = useMutation( @@ -98,226 +87,138 @@ function RepoLine({ } `, { - refetchQueries: ["GetRepos"], + refetchQueries: ["GetDashboardRepos"], } ); - + const isStarred = repo.stargazers?.map((_) => _.id).includes(me.id); return ( - - - - {repo.stargazers?.map((_) => _.id).includes(me.id) ? ( - - { - unstar({ variables: { repoId: repo.id } }); - }} - disabled={unstarLoading} - > - - {repo.stargazers.length} - - - ) : ( - - { - star({ variables: { repoId: repo.id } }); + <> + {isStarred ? ( + + + + ) : ( + + + + )} + ); -} +}; -function CreateRepoForm(props) { - const [createRepo] = useMutation( +const KillRuntimeButton = ({ repo }) => { + const { loading, data } = useQuery(gql` + query ListAllRuntimes { + listAllRuntimes { + sessionId + lastActive + } + } + `); + const { me } = useMe(); + const [killRuntime, { loading: killing }] = useMutation( gql` - mutation CreateRepo { - createRepo { - id - } + mutation killRuntime($sessionId: String!) { + killRuntime(sessionId: $sessionId) } `, { - refetchQueries: ["GetRepos"], + refetchQueries: ["ListAllRuntimes"], } ); - const navigate = useNavigate(); + + if (loading) return null; + const info = data.listAllRuntimes.find( + ({ sessionId }) => sessionId === `${me.id}_${repo.id}` + ); + if (!info) return null; + if (!info.lastActive) return null; return ( - + {/* last active: {getUpTime(info.lastActive)} */} + + {/* */} + + { + killRuntime({ + variables: { + sessionId: `${me.id}_${repo.id}`, + }, + }); + }} + > + {killing ? ( + + ) : ( + + )} + + + {/* */} + ); -} +}; -function RepoList({ repos }) { - const { me } = useMe(); - const [clickedRepo, setClickedRepo] = useState< - { id: string; name: string } | undefined - >(); - const [isConfirmDeleteDialogOpen, setConfirmDeleteDialogOpen] = - useState(false); +const DeleteRepoButton = ({ repo }) => { + const [open, setOpen] = useState(false); const { enqueueSnackbar } = useSnackbar(); - const client = useApolloClient(); - const [deleteRepo, deleteRepoResult] = useMutation( + const [deleteRepo, { loading }] = useMutation( gql` mutation deleteRepo($id: ID!) { deleteRepo(id: $id) } `, { + refetchQueries: ["GetDashboardRepos"], onCompleted() { - client.writeQuery({ - query: GET_REPOS, - data: { - myRepos: repos.filter((repo) => repo.id !== clickedRepo?.id), - }, - }); enqueueSnackbar("Successfully deleted repo", { variant: "success" }); }, onError() { @@ -325,142 +226,163 @@ function RepoList({ repos }) { }, } ); - // FIXME once ttl is reached, the runtime is killed, but this query is not - // updated. - const { loading, data } = useQuery(gql` - query ListAllRuntimes { - listAllRuntimes { - sessionId - lastActive - } - } - `); - - const onConfirmDeleteRepo = useCallback(() => { - setConfirmDeleteDialogOpen(false); - deleteRepo({ - variables: { - id: clickedRepo?.id, - }, - }).then(() => setClickedRepo(undefined)); - }, [clickedRepo?.id, deleteRepo]); - return ( - <> - - - - - Name - Visibility - Status (TTL: 12h) - Last Viewed - Operations - - - - {repos.map((repo) => ( - sessionId === `${me.id}_${repo.id}` - ) - } - key={repo.id} - onDeleteRepo={(repo) => { - setClickedRepo(repo); - setConfirmDeleteDialogOpen(true); - }} - /> - ))} - -
-
- { - setClickedRepo(undefined); - setConfirmDeleteDialogOpen(false); - }} - handleConfirm={onConfirmDeleteRepo} - /> - + + + { + setOpen(true); + }} + > + {loading ? ( + + ) : ( + + )} + + + setOpen(false)} fullWidth> + {`Delete ${repo.name}`} + Are you sure? + + + + + + ); -} - -function MyRepos() { - const { loading, error, data } = useQuery(GET_REPOS); +}; - if (loading) { - return ; - } - if (error) { - return ERROR: {error.message}; - } - const repos = data.myRepos.slice(); +const RepoCard = ({ repo }) => { + const { me } = useMe(); + // peiredically re-render so that the "last viwed time" and "lact active time" + // are updated every second. + const [counter, setCounter] = useState(0); + useEffect(() => { + const interval = setInterval(() => { + setCounter(counter + 1); + }, 1000); + return () => clearInterval(interval); + }, [counter]); return ( - - + + + {repo.userId !== me.id ? ( + + + + ) : repo.public ? ( + + + + ) : ( + + + + )} + + } + action={ + // TODO replace with a drop-down menu. + + + + } + title={ + + + + + {repo.name || "Untitled"} + + + + } + subheader={ + + Viewed{" "} + {timeDifference(new Date(), new Date(parseInt(repo.accessedAt)))} + + } + /> + {/* */} + + - My projects ({repos.length}) + - - - {repos.length === 0 && ( - - You don't have any projects yet. Click "Create New Project" to get - started. - - )} - - + + + ); -} +}; -function SharedWithMe() { +const RepoLists = () => { const { loading, error, data } = useQuery(gql` - query GetCollabRepos { - myCollabRepos { + query GetDashboardRepos { + getDashboardRepos { name id + userId public stargazers { id } updatedAt createdAt + accessedAt } } `); + if (loading) { return ; } if (error) { return ERROR: {error.message}; } - const repos = data.myCollabRepos.slice(); + const repos = data.getDashboardRepos.slice(); + // sort repos by last access time + repos.sort((a, b) => { + if (a.accessedAt && b.accessedAt) { + return parseInt(b.accessedAt) - parseInt(a.accessedAt); + } else if (a.accessedAt) { + return -1; + } else if (b.accessedAt) { + return 1; + } else { + return 0; + } + }); return ( - Projects shared with me ({repos.length}) + My projects ({repos.length}) + - {repos.length > 0 ? ( - - ) : ( - - No projects are shared with you. You can share your projects with - others by clicking "Share" in the project page. - + {repos.length === 0 && ( + + You don't have any projects yet. Click "Create New Project" to get + started. + )} + + {repos.map((repo) => ( + + + + ))} + ); -} - -function NoLogginErrorAlert() { - const nevigate = useNavigate(); - const [seconds, setSeconds] = useState(3); - - useEffect(() => { - if (seconds === 0) { - setSeconds(null); - nevigate("/login"); - return; - } - if (seconds === null) return; - - const timer = setTimeout(() => { - setSeconds((prev) => prev! - 1); - }, 1000); - - return () => clearTimeout(timer); - }, [nevigate, seconds]); - - return ( - - - Please login first! Automatically jump to{" "} - - login - {" "} - page in {seconds} seconds. - - - ); -} - -function RepoLists() { - // peiredically re-render so that the "last active time" is updated - const [counter, setCounter] = useState(0); - useEffect(() => { - const interval = setInterval(() => { - setCounter(counter + 1); - }, 1000); - return () => clearInterval(interval); - }, [counter]); - return ( - <> - - - - ); -} - -function ConfirmDeleteDialog({ - open, - repoName, - handleConfirm, - handleCancel, -}: { - open: boolean; - repoName?: string; - handleConfirm: () => void; - handleCancel: () => void; -}) { - const name = repoName ?? "Repo"; - return ( - - {`Delete ${name}`} - Are you sure? - - - - - - ); -} +}; export default function Page() { const { me } = useMe(); @@ -579,14 +443,10 @@ export default function Page() { }, [hasToken]); if (!me) { - // return ; return Loading user ..; } return ( - {/* TODO some meta information about the user */} - {/* */} - {/* TODO the repos of this user */}