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 ? (
+
+
-
- )}
-
-
+ {repo.stargazers.length}
+
+
+
+ ) : (
+
+
-
-
-
-
- {runtimeInfo
- ? runtimeInfo.lastActive
- ? "last active: " + getUpTime(runtimeInfo.lastActive)
- : "running"
- : "-"}
-
-
- {timeDifference(new Date(), new Date(parseInt(repo.updatedAt)))}
-
-
- {deletable && (
-
- {
- // FIXME ensure the runtime is killed
- onDeleteRepo(repo);
- }}
- >
- {deleting ? (
-
- ) : (
-
- )}
-
-
- )}
- {runtimeInfo ? (
-
- {
- // FIXME when to set killing=false?
- setKilling(true);
- killRuntime({
- variables: {
- sessionId: `${me.id}_${repo.id}`,
- },
- });
- }}
- >
- {killing ? (
-
- ) : (
-
- )}
-
-
- ) : null}
- {/* {sharable && (
- <>
-
- setOpen(true)}>
-
-
-
- setOpen(false)}
- id={repo.id}
- />
- >
- )} */}
-
-
- );
-}
-
-function RepoHintText({ children }) {
- return (
-
- {children}
-
+
+
+ )}
+ >
);
-}
+};
-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 ? (
+
+ ) : (
+
+ )}
+
+
+
+
);
-}
-
-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 (
-
- );
-}
+};
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 */}