From a512f16a5dcce418f049491a969f85f7248f2486 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Thu, 27 Jan 2022 03:42:06 +0000 Subject: [PATCH 01/10] Workspace stub --- site/components/Workspace/Workspace.tsx | 62 +++++++++++++++++++ site/components/Workspace/index.ts | 1 + .../workspaces/[user]/[workspace]/index.tsx | 46 ++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 site/components/Workspace/Workspace.tsx create mode 100644 site/components/Workspace/index.ts create mode 100644 site/pages/workspaces/[user]/[workspace]/index.tsx diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx new file mode 100644 index 0000000000000..e4569ef9063f8 --- /dev/null +++ b/site/components/Workspace/Workspace.tsx @@ -0,0 +1,62 @@ +import Paper from "@material-ui/core/Paper" +import { makeStyles } from "@material-ui/core/styles" +import React from "react" + +import * as API from "../../api" + +export interface WorkspaceProps { + workspace: API.Workspace +} + +export const Workspace: React.FC = ({ workspace }) => { + const styles = useStyles() + + return
+ +
Hello
+
+
+ +
Apps
+
+ +
Build stuff
+
+
+
+} + +namespace Constants { + export const CardRadius = 8 + export const CardPadding = 20 +} + +export const useStyles = makeStyles((theme) => { + + const common = { + border: `1px solid ${theme.palette.divider}`, + borderRadius: Constants.CardRadius, + margin: theme.spacing(1), + padding: Constants.CardPadding + } + + return { + root: { + display: "flex", + flexDirection: "column" + }, + horizontal: { + display: "flex", + flexDirection: "row" + }, + section: common, + sideBar: { + ...common, + width: "400px" + }, + main: { + ...common, + flex: 1 + } + } +}) \ No newline at end of file diff --git a/site/components/Workspace/index.ts b/site/components/Workspace/index.ts new file mode 100644 index 0000000000000..23b9b908c9768 --- /dev/null +++ b/site/components/Workspace/index.ts @@ -0,0 +1 @@ +export * from "./Workspace" \ No newline at end of file diff --git a/site/pages/workspaces/[user]/[workspace]/index.tsx b/site/pages/workspaces/[user]/[workspace]/index.tsx new file mode 100644 index 0000000000000..3b618d14abf6f --- /dev/null +++ b/site/pages/workspaces/[user]/[workspace]/index.tsx @@ -0,0 +1,46 @@ +import React from "react" +import { makeStyles } from "@material-ui/core/styles" +import Paper from "@material-ui/core/Paper" +import { useRouter } from "next/router" +import Link from "next/link" +import { Navbar } from "../../../../components/Navbar" +import { Footer } from "../../../../components/Page" +import { useUser } from "../../../../contexts/UserContext" + +import { Workspace } from "../../../../components/Workspace" +import { MockWorkspace } from "../../../../test_helpers" + + +const ProjectsPage: React.FC = () => { + const styles = useStyles() + const router = useRouter() + const { me, signOut } = useUser(true) + + const { user, workspace } = router.query + + return ( +
+ + +
+ +
+ +
+
+ ) +} + +const useStyles = makeStyles(() => ({ + root: { + display: "flex", + flexDirection: "column", + }, + inner: { + maxWidth: "1380px", + margin: "1em auto", + width: "100%" + } +})) + +export default ProjectsPage From fa6c8c390d831ef0761d571f1b69159a004b9289 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Sat, 29 Jan 2022 23:41:55 +0000 Subject: [PATCH 02/10] More prototyping --- package.json | 2 +- site/components/Workspace/Workspace.tsx | 186 +++++++++++++++++- .../workspaces/[user]/[workspace]/index.tsx | 4 +- site/static/apple-logo.svg | 11 ++ site/static/google-storage-logo.svg | 1 + site/static/react-icon.svg | 9 + site/static/terminal.svg | 1 + site/static/vscode.svg | 1 + site/static/windows-logo.svg | 1 + yarn.lock | 12 +- 10 files changed, 213 insertions(+), 15 deletions(-) create mode 100644 site/static/apple-logo.svg create mode 100644 site/static/google-storage-logo.svg create mode 100644 site/static/react-icon.svg create mode 100644 site/static/terminal.svg create mode 100644 site/static/vscode.svg create mode 100644 site/static/windows-logo.svg diff --git a/package.json b/package.json index 6ae8df91d9cde..f919623f64ad5 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "devDependencies": { "@material-ui/core": "4.9.4", "@material-ui/icons": "4.5.1", - "@material-ui/lab": "4.0.0-alpha.42", + "@material-ui/lab": "4.0.0-alpha.60", "@testing-library/react": "12.1.2", "@types/express": "4.17.13", "@types/jest": "27.4.0", diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index e4569ef9063f8..bc960b54e29a6 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -1,5 +1,6 @@ import Paper from "@material-ui/core/Paper" import { makeStyles } from "@material-ui/core/styles" +import Typography from "@material-ui/core/Typography" import React from "react" import * as API from "../../api" @@ -8,19 +9,187 @@ export interface WorkspaceProps { workspace: API.Workspace } +const useStatusStyles = makeStyles((theme) => { + + const common = { + width: theme.spacing(1), + height: theme.spacing(1), + borderRadius: "100%", + backgroundColor: theme.palette.action.disabled, + transition: "background-color 200ms ease", + }; + + return { + inactive: common, + active: { + ...common, + backgroundColor: theme.palette.primary.main, + } + } +}) + +/** + * A component that displays the Dev URL indicator. The indicator status represents + * loading, online, offline, or error. + */ +export const StatusIndicator: React.FC<{ status: ResourceStatus }> = ({ status }) => { + const styles = useStatusStyles() + + const className = status === "active" ? styles.active : styles.inactive + return ( +
+ ) +} + + +type ResourceStatus = "active" | "inactive" + +export interface ResourceRowProps { + name: string + icon: string + //href: string + status: ResourceStatus +} + +const ResourceIconSize = 20 + +export const ResourceRow: React.FC = ({ icon, /*href,*/ name, status }) => { + const styles = useResourceRowStyles() + + return
+
+ +
+
+ {name} +
+
+ +
+
+} + +const useResourceRowStyles = makeStyles((theme) => ({ + root: { + display: "flex", + flexDirection: "row", + justifyContent: "center", + alignItems: "center", + }, + iconContainer: { + width: ResourceIconSize + theme.spacing(1), + height: ResourceIconSize + theme.spacing(1), + display: "flex", + justifyContent: "center", + alignItems: "center", + flex: 0, + }, + nameContainer: { + margin: theme.spacing(1), + paddingLeft: theme.spacing(1), + flex: 1, + width: "100%", + }, + statusContainer: { + width: 24, + height: 24, + flex: 0, + display: "flex", + justifyContent: "center", + alignItems: "center", + } +})) + +export const Title: React.FC = ({ children }) => { + const styles = useTitleStyles(); + + return
+ {children} +
+} + +const useTitleStyles = makeStyles((theme) => ({ + header: { + alignItems: "center", + borderBottom: `1px solid ${theme.palette.divider}`, + display: "flex", + height: theme.spacing(6), + justifyContent: "space-between", + marginBottom: theme.spacing(2), + marginTop: -theme.spacing(1), + paddingBottom: theme.spacing(1), + paddingLeft: Constants.CardPadding + theme.spacing(1), + paddingRight: Constants.CardPadding / 2, + }, +})) + +import Timeline from "@material-ui/lab/Timeline" +import TimelineItem from '@material-ui/lab/TimelineItem'; +import TimelineSeparator from '@material-ui/lab/TimelineSeparator'; +import TimelineConnector from '@material-ui/lab/TimelineConnector'; +import TimelineContent from '@material-ui/lab/TimelineContent'; +import TimelineDot from '@material-ui/lab/TimelineDot'; + +export const WorkspaceTimeline: React.FC = () => { + return + + + + + + Eat + + + + + + + Code + + + + + + Sleep + + +} + export const Workspace: React.FC = ({ workspace }) => { const styles = useStyles() return
-
Hello
+ {workspace.name} + + {"TODO: Project"} +
- -
Apps
-
+
+ + Applications + +
+ + + + +
+
+ + Resources + +
+ + + +
+
+
-
Build stuff
+ Timeline +
@@ -49,9 +218,14 @@ export const useStyles = makeStyles((theme) => { display: "flex", flexDirection: "row" }, + vertical: { + display: "flex", + flexDirection: "column" + }, section: common, sideBar: { - ...common, + display: "flex", + flexDirection: "column", width: "400px" }, main: { diff --git a/site/pages/workspaces/[user]/[workspace]/index.tsx b/site/pages/workspaces/[user]/[workspace]/index.tsx index 3b618d14abf6f..b568e43982a59 100644 --- a/site/pages/workspaces/[user]/[workspace]/index.tsx +++ b/site/pages/workspaces/[user]/[workspace]/index.tsx @@ -11,7 +11,7 @@ import { Workspace } from "../../../../components/Workspace" import { MockWorkspace } from "../../../../test_helpers" -const ProjectsPage: React.FC = () => { +const WorkspacesPage: React.FC = () => { const styles = useStyles() const router = useRouter() const { me, signOut } = useUser(true) @@ -43,4 +43,4 @@ const useStyles = makeStyles(() => ({ } })) -export default ProjectsPage +export default WorkspacesPage diff --git a/site/static/apple-logo.svg b/site/static/apple-logo.svg new file mode 100644 index 0000000000000..2e7254d234e4d --- /dev/null +++ b/site/static/apple-logo.svg @@ -0,0 +1,11 @@ + + + + + diff --git a/site/static/google-storage-logo.svg b/site/static/google-storage-logo.svg new file mode 100644 index 0000000000000..d30e0030858b7 --- /dev/null +++ b/site/static/google-storage-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/static/react-icon.svg b/site/static/react-icon.svg new file mode 100644 index 0000000000000..ea77a618d9486 --- /dev/null +++ b/site/static/react-icon.svg @@ -0,0 +1,9 @@ + + React Logo + + + + + + + diff --git a/site/static/terminal.svg b/site/static/terminal.svg new file mode 100644 index 0000000000000..21d039ce38fb9 --- /dev/null +++ b/site/static/terminal.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/static/vscode.svg b/site/static/vscode.svg new file mode 100644 index 0000000000000..62505392b2eeb --- /dev/null +++ b/site/static/vscode.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/site/static/windows-logo.svg b/site/static/windows-logo.svg new file mode 100644 index 0000000000000..6fac0df1bd603 --- /dev/null +++ b/site/static/windows-logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index cc89e71215e95..45f21026ab8f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -606,16 +606,16 @@ dependencies: "@babel/runtime" "^7.4.4" -"@material-ui/lab@4.0.0-alpha.42": - version "4.0.0-alpha.42" - resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.42.tgz#f8789d3ba39a1e5a13f462d618c2eec53f87ae10" - integrity sha512-JbKEMIXSslh03u6HNU1Pp1VXd9ycJ1dqkI+iQK6yR+Sng2mvMKzJ80GCV5ROXAXwwNnD8zHOopLZNIpTsEAVgQ== +"@material-ui/lab@4.0.0-alpha.60": + version "4.0.0-alpha.60" + resolved "https://registry.yarnpkg.com/@material-ui/lab/-/lab-4.0.0-alpha.60.tgz#5ad203aed5a8569b0f1753945a21a05efa2234d2" + integrity sha512-fadlYsPJF+0fx2lRuyqAuJj7hAS1tLDdIEEdov5jlrpb5pp4b+mRDUqQTUxi4inRZHS1bEXpU8QWUhO6xX88aA== dependencies: "@babel/runtime" "^7.4.4" - "@material-ui/utils" "^4.7.1" + "@material-ui/utils" "^4.11.2" clsx "^1.0.4" prop-types "^15.7.2" - react-is "^16.8.0" + react-is "^16.8.0 || ^17.0.0" "@material-ui/styles@^4.9.0": version "4.11.4" From 0e933bb1c15ff04db7b49231d51c1f2ebe68e993 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Mon, 31 Jan 2022 18:31:50 +0000 Subject: [PATCH 03/10] Route to correct path --- site/pages/projects/[organization]/[project]/create.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/pages/projects/[organization]/[project]/create.tsx b/site/pages/projects/[organization]/[project]/create.tsx index ce2c66508a808..54e37d0847229 100644 --- a/site/pages/projects/[organization]/[project]/create.tsx +++ b/site/pages/projects/[organization]/[project]/create.tsx @@ -32,7 +32,7 @@ const CreateWorkspacePage: React.FC = () => { const onSubmit = async (req: API.CreateWorkspaceRequest) => { const workspace = await API.Workspace.create(req) - await router.push(`/workspaces/${workspace.id}`) + await router.push(`/workspaces/${me.username}/${workspace.name}`) return workspace } From ad56a134d46ddc50ab8b188b513c3bc55c7f4980 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Mon, 31 Jan 2022 18:38:13 +0000 Subject: [PATCH 04/10] Port over QuestionHelp component --- site/components/QuestionHelp.tsx | 23 +++++++++++++++++++++++ site/components/Workspace/Workspace.tsx | 3 ++- site/components/index.tsx | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 site/components/QuestionHelp.tsx diff --git a/site/components/QuestionHelp.tsx b/site/components/QuestionHelp.tsx new file mode 100644 index 0000000000000..b1ee4782d89d1 --- /dev/null +++ b/site/components/QuestionHelp.tsx @@ -0,0 +1,23 @@ +import { makeStyles } from "@material-ui/core/styles" +import HelpIcon from "@material-ui/icons/Help" +import * as React from "react" + +export const QuestionHelp: React.FC = () => { + const styles = useStyles() + return ( + + ) +} + +const useStyles = makeStyles((theme) => ({ + icon: { + display: "block", + height: 20, + width: 20, + color: theme.palette.text.secondary, + opacity: 0.5, + "&:hover": { + opacity: 1, + }, + }, +})) diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index bc960b54e29a6..4d8ae30c8327d 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -129,6 +129,7 @@ import TimelineSeparator from '@material-ui/lab/TimelineSeparator'; import TimelineConnector from '@material-ui/lab/TimelineConnector'; import TimelineContent from '@material-ui/lab/TimelineContent'; import TimelineDot from '@material-ui/lab/TimelineDot'; +import { QuestionHelp } from "../QuestionHelp" export const WorkspaceTimeline: React.FC = () => { return @@ -168,7 +169,7 @@ export const Workspace: React.FC = ({ workspace }) => {
- Applications + <span>Applications</span><QuestionHelp />
diff --git a/site/components/index.tsx b/site/components/index.tsx index ebb1a90188bb8..7abcb3943c90b 100644 --- a/site/components/index.tsx +++ b/site/components/index.tsx @@ -1,4 +1,5 @@ export * from "./Button" export * from "./EmptyState" export * from "./Page" +export * from "./QuestionHelp" export * from "./Redirect" From 07d5711c1143875d370b447c9c093ab41f95d44f Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Mon, 31 Jan 2022 19:00:17 +0000 Subject: [PATCH 05/10] Add spinner --- site/components/QuestionHelp.tsx | 4 +- site/components/Workspace/Workspace.tsx | 213 ++++++++++-------- site/components/Workspace/index.ts | 2 +- .../workspaces/[user]/[workspace]/index.tsx | 5 +- 4 files changed, 123 insertions(+), 101 deletions(-) diff --git a/site/components/QuestionHelp.tsx b/site/components/QuestionHelp.tsx index b1ee4782d89d1..5a4f44851c12a 100644 --- a/site/components/QuestionHelp.tsx +++ b/site/components/QuestionHelp.tsx @@ -4,9 +4,7 @@ import * as React from "react" export const QuestionHelp: React.FC = () => { const styles = useStyles() - return ( - - ) + return } const useStyles = makeStyles((theme) => ({ diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index 4d8ae30c8327d..d9a5a07d9ab7c 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -1,6 +1,7 @@ import Paper from "@material-ui/core/Paper" import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" +import OpenInNewIcon from "@material-ui/icons/OpenInNew" import React from "react" import * as API from "../../api" @@ -10,21 +11,20 @@ export interface WorkspaceProps { } const useStatusStyles = makeStyles((theme) => { - const common = { width: theme.spacing(1), height: theme.spacing(1), borderRadius: "100%", backgroundColor: theme.palette.action.disabled, transition: "background-color 200ms ease", - }; + } return { inactive: common, active: { ...common, backgroundColor: theme.palette.primary.main, - } + }, } }) @@ -35,14 +35,15 @@ const useStatusStyles = makeStyles((theme) => { export const StatusIndicator: React.FC<{ status: ResourceStatus }> = ({ status }) => { const styles = useStatusStyles() - const className = status === "active" ? styles.active : styles.inactive - return ( -
- ) + if (status == "loading") { + return + } else { + const className = status === "active" ? styles.active : styles.inactive + return
+ } } - -type ResourceStatus = "active" | "inactive" +type ResourceStatus = "active" | "inactive" | "loading" export interface ResourceRowProps { name: string @@ -53,20 +54,31 @@ export interface ResourceRowProps { const ResourceIconSize = 20 -export const ResourceRow: React.FC = ({ icon, /*href,*/ name, status }) => { +export const ResourceRow: React.FC = ({ icon, href, name, status }) => { const styles = useResourceRowStyles() - return
-
- -
-
- {name} -
-
- + return ( +
+
+ +
+
+ + {name} + + +
+
+ +
-
+ ) } const useResourceRowStyles = makeStyles((theme) => ({ @@ -97,15 +109,13 @@ const useResourceRowStyles = makeStyles((theme) => ({ display: "flex", justifyContent: "center", alignItems: "center", - } + }, })) export const Title: React.FC = ({ children }) => { - const styles = useTitleStyles(); + const styles = useTitleStyles() - return
- {children} -
+ return
{children}
} const useTitleStyles = makeStyles((theme) => ({ @@ -113,8 +123,8 @@ const useTitleStyles = makeStyles((theme) => ({ alignItems: "center", borderBottom: `1px solid ${theme.palette.divider}`, display: "flex", + flexDirection: "row", height: theme.spacing(6), - justifyContent: "space-between", marginBottom: theme.spacing(2), marginTop: -theme.spacing(1), paddingBottom: theme.spacing(1), @@ -124,76 +134,92 @@ const useTitleStyles = makeStyles((theme) => ({ })) import Timeline from "@material-ui/lab/Timeline" -import TimelineItem from '@material-ui/lab/TimelineItem'; -import TimelineSeparator from '@material-ui/lab/TimelineSeparator'; -import TimelineConnector from '@material-ui/lab/TimelineConnector'; -import TimelineContent from '@material-ui/lab/TimelineContent'; -import TimelineDot from '@material-ui/lab/TimelineDot'; +import TimelineItem from "@material-ui/lab/TimelineItem" +import TimelineSeparator from "@material-ui/lab/TimelineSeparator" +import TimelineConnector from "@material-ui/lab/TimelineConnector" +import TimelineContent from "@material-ui/lab/TimelineContent" +import TimelineDot from "@material-ui/lab/TimelineDot" import { QuestionHelp } from "../QuestionHelp" +import { CircularProgress, Link } from "@material-ui/core" export const WorkspaceTimeline: React.FC = () => { - return - - - - - - Eat - - - - - - - Code - - - - - - Sleep - - + return ( + + + + + + + Eat + + + + + + + Code + + + + + + Sleep + + + ) } export const Workspace: React.FC = ({ workspace }) => { const styles = useStyles() - return
- - {workspace.name} - - {"TODO: Project"} - - -
-
- - <span>Applications</span><QuestionHelp /> - -
- - - - -
-
- - Resources - -
- - - -
+ return ( +
+ + {workspace.name} + + {"TODO: Project"} + + +
+
+ + + <Typography variant="h6">Applications</Typography> + <div style={{ margin: "0em 1em" }}> + <QuestionHelp /> + </div> + + +
+ + + +
+
+ + + <Typography variant="h6">Resources</Typography> + <div style={{ margin: "0em 1em" }}> + <QuestionHelp /> + </div> + + +
+ + + +
+
+
+ + + <Typography variant="h6">Timeline</Typography> + +
- - Timeline - -
-
+ ) } namespace Constants { @@ -202,36 +228,35 @@ namespace Constants { } export const useStyles = makeStyles((theme) => { - const common = { border: `1px solid ${theme.palette.divider}`, borderRadius: Constants.CardRadius, margin: theme.spacing(1), - padding: Constants.CardPadding + padding: Constants.CardPadding, } return { root: { display: "flex", - flexDirection: "column" + flexDirection: "column", }, horizontal: { display: "flex", - flexDirection: "row" + flexDirection: "row", }, vertical: { display: "flex", - flexDirection: "column" + flexDirection: "column", }, section: common, sideBar: { display: "flex", flexDirection: "column", - width: "400px" + width: "400px", }, main: { ...common, - flex: 1 - } + flex: 1, + }, } -}) \ No newline at end of file +}) diff --git a/site/components/Workspace/index.ts b/site/components/Workspace/index.ts index 23b9b908c9768..4c8c38cc721c8 100644 --- a/site/components/Workspace/index.ts +++ b/site/components/Workspace/index.ts @@ -1 +1 @@ -export * from "./Workspace" \ No newline at end of file +export * from "./Workspace" diff --git a/site/pages/workspaces/[user]/[workspace]/index.tsx b/site/pages/workspaces/[user]/[workspace]/index.tsx index b568e43982a59..c019f7d7f8e90 100644 --- a/site/pages/workspaces/[user]/[workspace]/index.tsx +++ b/site/pages/workspaces/[user]/[workspace]/index.tsx @@ -10,7 +10,6 @@ import { useUser } from "../../../../contexts/UserContext" import { Workspace } from "../../../../components/Workspace" import { MockWorkspace } from "../../../../test_helpers" - const WorkspacesPage: React.FC = () => { const styles = useStyles() const router = useRouter() @@ -39,8 +38,8 @@ const useStyles = makeStyles(() => ({ inner: { maxWidth: "1380px", margin: "1em auto", - width: "100%" - } + width: "100%", + }, })) export default WorkspacesPage From 48bb588385040b55245753993da2f07a35cd19fa Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 1 Feb 2022 00:14:05 +0000 Subject: [PATCH 06/10] Model workspace with static data --- site/components/Timeline/TerminalOutput.tsx | 38 ++++ site/components/Timeline/index.tsx | 235 ++++++++++++++++++++ site/components/Workspace/Workspace.tsx | 68 ++---- 3 files changed, 297 insertions(+), 44 deletions(-) create mode 100644 site/components/Timeline/TerminalOutput.tsx create mode 100644 site/components/Timeline/index.tsx diff --git a/site/components/Timeline/TerminalOutput.tsx b/site/components/Timeline/TerminalOutput.tsx new file mode 100644 index 0000000000000..2a55322e6e363 --- /dev/null +++ b/site/components/Timeline/TerminalOutput.tsx @@ -0,0 +1,38 @@ +import { makeStyles } from "@material-ui/core/styles" +import React from "react" + +interface Props { + output: string[] + className?: string +} + +export const TerminalOutput: React.FC = ({ className, output }) => { + const styles = useStyles() + + return ( +
+ {output.map((line, idx) => ( +
+ {line} +
+ ))} +
+ ) +} +export const MONOSPACE_FONT_FAMILY = + "'Fira Code', 'Lucida Console', 'Lucida Sans Typewriter', 'Liberation Mono', 'Monaco', 'Courier New', Courier, monospace" +const useStyles = makeStyles((theme) => ({ + root: { + minHeight: 156, + background: theme.palette.background.default, + //color: theme.palette.codeBlock.contrastText, + fontFamily: MONOSPACE_FONT_FAMILY, + fontSize: 13, + wordBreak: "break-all", + padding: theme.spacing(2), + borderRadius: theme.shape.borderRadius, + }, + line: { + whiteSpace: "pre-wrap", + }, +})) diff --git a/site/components/Timeline/index.tsx b/site/components/Timeline/index.tsx new file mode 100644 index 0000000000000..be1f462fd7764 --- /dev/null +++ b/site/components/Timeline/index.tsx @@ -0,0 +1,235 @@ +import { Avatar, Box, SvgIcon, Typography } from "@material-ui/core" +import makeStyles from "@material-ui/styles/makeStyles"; +import React, { useState } from "react" +import { TerminalOutput } from "./TerminalOutput"; + +export interface TimelineEntry { + date: Date + title: string + description?: string +} + +const today = new Date(); +const yesterday = new Date() +yesterday.setHours(-24) +const weekAgo = new Date() +weekAgo.setHours(-24 * 7) + +const sampleOutput = ` +Successfully assigned coder/bryan-prototype-jppnd to gke-master-workspaces-1-ef039342-cybd +Container image "gke.gcr.io/istio/proxyv2:1.4.10-gke.8" already present on machine +Created container istio-init +Started container istio-init +Pulling image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8d4ee2e9e-20220131" +Successfully pulled image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8d4ee2e9e-20220131" in 7.423772294s +Successfully assigned coder/bryan-prototype-jppnd to gke-master-workspaces-1-ef039342-cybd +Container image "gke.gcr.io/istio/proxyv2:1.4.10-gke.8" already present on machine +Created container istio-init +Started container istio-init +Pulling image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8d4ee2e9e-20220131" +Successfully pulled image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8d4ee2e9e-20220131" in 7.423772294s +Successfully assigned coder/bryan-prototype-jppnd to gke-master-workspaces-1-ef039342-cybd +Container image "gke.gcr.io/istio/proxyv2:1.4.10-gke.8" already present on machine +Created container istio-init +Started container istio-init +Pulling image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8d4ee2e9e-20220131" +Successfully pulled image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8d4ee2e9e-20220131" in 7.423772294s +`.split("\n") + +export const mockEntries: TimelineEntry[] = [{ + date: weekAgo, + description: "Created Workspace", + title: "Admin", +}, { + date: yesterday, + description: "Modified Workspace", + title: "Admin" +}, { + date: today, + description: "Modified Workspace", + title: "Admin" +}, { + date: today, + description: "Restarted Workspace", + title: "Admin" + +}] + +export interface TimelineEntryProps { + entries: TimelineEntry[] +} + +// Group timeline entry by date + +const getDateWithoutTime = (date: Date) => { + // TODO: Handle conversion to local time from UTC, as this may shift the actual day + const dateWithoutTime = new Date(date.getTime()) + dateWithoutTime.setHours(0, 0, 0, 0) + return dateWithoutTime +} + +export const groupByDate = (entries: TimelineEntry[]): Record => { + const initial: Record = {}; + return entries.reduce>((acc, curr) => { + const dateWithoutTime = getDateWithoutTime(curr.date); + const key = dateWithoutTime.getTime().toString() + const currentEntry = acc[key]; + if (currentEntry) { + return { + ...acc, + [key]: [...currentEntry, curr] + } + } else { + return { + ...acc, + [key]: [curr] + } + } + }, initial) + +} + +const formatDate = (date: Date) => { + let formatter = new Intl.DateTimeFormat("en", { + dateStyle: "long" + }); + return formatter.format(date) +} + +const formatTime = (date: Date) => { + let formatter = new Intl.DateTimeFormat("en", { + timeStyle: "short" + }); + return formatter.format(date) +} + + + +export interface EntryProps { + entry: TimelineEntry +} + +export const Entry: React.FC = ({ entry }) => { + const styles = useEntryStyles() + const [expanded, setExpanded] = useState(false) + + const toggleExpanded = () => { + setExpanded((prev: boolean) => !prev) + } + + return + + + {"A"} + + + + + {entry.title} + {formatTime(entry.date)} + + {entry.description} + + + + + + + +} + +export const useEntryStyles = makeStyles((theme) => ({ + +})) + +export type BuildLogStatus = "success" | "failure" | "pending" + +export interface BuildLogProps { + summary: string + status: BuildLogStatus + expanded?: boolean +} + +export const BuildLog: React.FC = ({ summary, status, expanded }) => { + const styles = useBuildLogStyles(status)() + + return
+ +
+ +} + +const useBuildLogStyles = (status: BuildLogStatus) => makeStyles((theme) => ({ + container: { + borderLeft: `2px solid ${status === "failure" ? theme.palette.error.main : theme.palette.info.main}`, + margin: "1em 0em", + }, + collapseButton: { + color: "inherit", + textAlign: "left", + width: "100%", + background: "none", + border: 0, + alignItems: "center", + borderRadius: theme.spacing(0.5), + cursor: "pointer", + "&:disabled": { + color: "inherit", + cursor: "initial", + }, + "&:hover:not(:disabled)": { + backgroundColor: theme.palette.type === "dark" ? theme.palette.grey[800] : theme.palette.grey[100], + }, + }, +})) + +export const Timeline: React.FC = () => { + const styles = useStyles() + + const entries = mockEntries + const groupedByDate = groupByDate(entries) + const allDates = Object.keys(groupedByDate); + const sortedDates = allDates.sort((a, b) => b.localeCompare(a)) + + const days = sortedDates.map((date) => { + + const entriesForDay = groupedByDate[date]; + + const entryElements = entriesForDay.map((entry) => ) + + + return
+ {formatDate(new Date(Number.parseInt(date)))} + {entryElements} + +
+ }) + + return
+ {days} +
+ +} + +export const useStyles = makeStyles((theme) => ({ + root: { + display: "flex", + width: "100%", + flexDirection: "column" + }, + container: { + display: "flex", + flexDirection: "column", + }, + header: { + display: "flex", + justifyContent: "center", + alignItems: "center", + //textTransform: "uppercase" + } +})) \ No newline at end of file diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index d9a5a07d9ab7c..6c4fe167fd649 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -3,6 +3,11 @@ import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import OpenInNewIcon from "@material-ui/icons/OpenInNew" import React from "react" +import MoreVertIcon from "@material-ui/icons/MoreVert" +import { QuestionHelp } from "../QuestionHelp" +import { CircularProgress, IconButton, Link } from "@material-ui/core" + +import { Timeline as TestTimeline } from "../Timeline" import * as API from "../../api" @@ -36,7 +41,7 @@ export const StatusIndicator: React.FC<{ status: ResourceStatus }> = ({ status } const styles = useStatusStyles() if (status == "loading") { - return + return } else { const className = status === "active" ? styles.active : styles.inactive return
@@ -48,7 +53,7 @@ type ResourceStatus = "active" | "inactive" | "loading" export interface ResourceRowProps { name: string icon: string - //href: string + href?: string status: ResourceStatus } @@ -63,7 +68,7 @@ export const ResourceRow: React.FC = ({ icon, href, name, stat
- = ({ icon, href, name, stat > {name} - + : {name}}
+
+ + + +
) } @@ -110,6 +120,11 @@ const useResourceRowStyles = makeStyles((theme) => ({ justifyContent: "center", alignItems: "center", }, + action: { + margin: `0 ${theme.spacing(0.5)}px`, + opacity: 0.7, + fontSize: 16, + } })) export const Title: React.FC = ({ children }) => { @@ -133,41 +148,6 @@ const useTitleStyles = makeStyles((theme) => ({ }, })) -import Timeline from "@material-ui/lab/Timeline" -import TimelineItem from "@material-ui/lab/TimelineItem" -import TimelineSeparator from "@material-ui/lab/TimelineSeparator" -import TimelineConnector from "@material-ui/lab/TimelineConnector" -import TimelineContent from "@material-ui/lab/TimelineContent" -import TimelineDot from "@material-ui/lab/TimelineDot" -import { QuestionHelp } from "../QuestionHelp" -import { CircularProgress, Link } from "@material-ui/core" - -export const WorkspaceTimeline: React.FC = () => { - return ( - - - - - - - Eat - - - - - - - Code - - - - - - Sleep - - - ) -} export const Workspace: React.FC = ({ workspace }) => { const styles = useStyles() @@ -177,7 +157,7 @@ export const Workspace: React.FC = ({ workspace }) => { {workspace.name} - {"TODO: Project"} + test-org{" / "}test-project
@@ -191,8 +171,8 @@ export const Workspace: React.FC = ({ workspace }) => {
- - + +
@@ -205,7 +185,7 @@ export const Workspace: React.FC = ({ workspace }) => {
- +
@@ -215,7 +195,7 @@ export const Workspace: React.FC = ({ workspace }) => { <Typography variant="h6">Timeline</Typography> - +
From 8b73c2c34243934f3344f86378b88e3311f9e8f5 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 1 Feb 2022 00:28:58 +0000 Subject: [PATCH 07/10] Update timeline and components --- site/components/Timeline/index.tsx | 245 ++++++++++++++---------- site/components/Workspace/Workspace.tsx | 38 ++-- 2 files changed, 169 insertions(+), 114 deletions(-) diff --git a/site/components/Timeline/index.tsx b/site/components/Timeline/index.tsx index be1f462fd7764..5cf84335395cd 100644 --- a/site/components/Timeline/index.tsx +++ b/site/components/Timeline/index.tsx @@ -1,15 +1,23 @@ -import { Avatar, Box, SvgIcon, Typography } from "@material-ui/core" -import makeStyles from "@material-ui/styles/makeStyles"; +import { Avatar, Box, CircularProgress, SvgIcon, Typography } from "@material-ui/core" +import makeStyles from "@material-ui/styles/makeStyles" import React, { useState } from "react" -import { TerminalOutput } from "./TerminalOutput"; +import { TerminalOutput } from "./TerminalOutput" +import StageCompleteIcon from "@material-ui/icons/Done" +import StageExpandedIcon from "@material-ui/icons/KeyboardArrowDown" +import StageErrorIcon from "@material-ui/icons/Warning" + +export type BuildLogStatus = "success" | "failed" | "pending" export interface TimelineEntry { date: Date title: string description?: string + status: BuildLogStatus + buildSummary: string + buildLogs: string[] } -const today = new Date(); +const today = new Date() const yesterday = new Date() yesterday.setHours(-24) const weekAgo = new Date() @@ -36,24 +44,40 @@ Pulling image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8 Successfully pulled image "gcr.io/coder-enterprise-nightlies/coder/envbox:1.27.0-rc.0-145-g8d4ee2e9e-20220131" in 7.423772294s `.split("\n") -export const mockEntries: TimelineEntry[] = [{ - date: weekAgo, - description: "Created Workspace", - title: "Admin", -}, { - date: yesterday, - description: "Modified Workspace", - title: "Admin" -}, { - date: today, - description: "Modified Workspace", - title: "Admin" -}, { - date: today, - description: "Restarted Workspace", - title: "Admin" - -}] +export const mockEntries: TimelineEntry[] = [ + { + date: weekAgo, + description: "Created Workspace", + title: "Admin", + status: "success", + buildLogs: sampleOutput, + buildSummary: "Succeeded in 82s", + }, + { + date: yesterday, + description: "Modified Workspace", + title: "Admin", + status: "failed", + buildLogs: sampleOutput, + buildSummary: "Encountered error after 49s", + }, + { + date: today, + description: "Modified Workspace", + title: "Admin", + status: "pending", + buildLogs: sampleOutput, + buildSummary: "Operation in progress...", + }, + { + date: today, + description: "Restarted Workspace", + title: "Admin", + status: "success", + buildLogs: sampleOutput, + buildSummary: "Succeeded in 15s", + }, +] export interface TimelineEntryProps { entries: TimelineEntry[] @@ -69,42 +93,39 @@ const getDateWithoutTime = (date: Date) => { } export const groupByDate = (entries: TimelineEntry[]): Record => { - const initial: Record = {}; + const initial: Record = {} return entries.reduce>((acc, curr) => { - const dateWithoutTime = getDateWithoutTime(curr.date); + const dateWithoutTime = getDateWithoutTime(curr.date) const key = dateWithoutTime.getTime().toString() - const currentEntry = acc[key]; + const currentEntry = acc[key] if (currentEntry) { return { ...acc, - [key]: [...currentEntry, curr] + [key]: [...currentEntry, curr], } } else { return { ...acc, - [key]: [curr] + [key]: [curr], } } }, initial) - } const formatDate = (date: Date) => { let formatter = new Intl.DateTimeFormat("en", { - dateStyle: "long" - }); + dateStyle: "long", + }) return formatter.format(date) } const formatTime = (date: Date) => { let formatter = new Intl.DateTimeFormat("en", { - timeStyle: "short" - }); + timeStyle: "short", + }) return formatter.format(date) } - - export interface EntryProps { entry: TimelineEntry } @@ -117,110 +138,134 @@ export const Entry: React.FC = ({ entry }) => { setExpanded((prev: boolean) => !prev) } - return - - - {"A"} - - - - - {entry.title} - {formatTime(entry.date)} + return ( + + + + {"A"} - {entry.description} - - + + + {entry.title} + + {formatTime(entry.date)} + + + {entry.description} + + + - - + ) } -export const useEntryStyles = makeStyles((theme) => ({ - -})) - -export type BuildLogStatus = "success" | "failure" | "pending" +export const useEntryStyles = makeStyles((theme) => ({})) export interface BuildLogProps { summary: string status: BuildLogStatus expanded?: boolean } - +const STATUS_ICON_SIZE = 18 +const LOADING_SPINNER_SIZE = 14 export const BuildLog: React.FC = ({ summary, status, expanded }) => { const styles = useBuildLogStyles(status)() + let icon: JSX.Element + if (status === "failed") { + icon = + } else if (status === "pending") { + icon = + } else { + icon = + } - return
- -
- + return ( +
+ + {expanded && } +
+ ) } -const useBuildLogStyles = (status: BuildLogStatus) => makeStyles((theme) => ({ - container: { - borderLeft: `2px solid ${status === "failure" ? theme.palette.error.main : theme.palette.info.main}`, - margin: "1em 0em", - }, - collapseButton: { - color: "inherit", - textAlign: "left", - width: "100%", - background: "none", - border: 0, - alignItems: "center", - borderRadius: theme.spacing(0.5), - cursor: "pointer", - "&:disabled": { +const useBuildLogStyles = (status: BuildLogStatus) => + makeStyles((theme) => ({ + container: { + borderLeft: `2px solid ${theme.palette.info.main}`, + margin: "1em 0em", + }, + collapseButton: { color: "inherit", - cursor: "initial", + textAlign: "left", + width: "100%", + background: "none", + border: 0, + alignItems: "center", + borderRadius: theme.spacing(0.5), + cursor: "pointer", + "&:disabled": { + color: "inherit", + cursor: "initial", + }, + "&:hover:not(:disabled)": { + backgroundColor: theme.palette.type === "dark" ? theme.palette.grey[800] : theme.palette.grey[100], + }, }, - "&:hover:not(:disabled)": { - backgroundColor: theme.palette.type === "dark" ? theme.palette.grey[800] : theme.palette.grey[100], + statusIcon: { + width: STATUS_ICON_SIZE, + height: STATUS_ICON_SIZE, + color: theme.palette.text.secondary, }, - }, -})) + statusIconError: { + color: theme.palette.error.main, + }, + statusIconSuccess: { + color: theme.palette.success.main, + }, + })) export const Timeline: React.FC = () => { const styles = useStyles() const entries = mockEntries const groupedByDate = groupByDate(entries) - const allDates = Object.keys(groupedByDate); + const allDates = Object.keys(groupedByDate) const sortedDates = allDates.sort((a, b) => b.localeCompare(a)) const days = sortedDates.map((date) => { - - const entriesForDay = groupedByDate[date]; + const entriesForDay = groupedByDate[date] const entryElements = entriesForDay.map((entry) => ) - - return
- {formatDate(new Date(Number.parseInt(date)))} - {entryElements} - -
+ return ( +
+ + {formatDate(new Date(Number.parseInt(date)))} + + {entryElements} +
+ ) }) - return
- {days} -
- + return
{days}
} export const useStyles = makeStyles((theme) => ({ root: { display: "flex", width: "100%", - flexDirection: "column" + flexDirection: "column", }, container: { display: "flex", @@ -231,5 +276,5 @@ export const useStyles = makeStyles((theme) => ({ justifyContent: "center", alignItems: "center", //textTransform: "uppercase" - } -})) \ No newline at end of file + }, +})) diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index 6c4fe167fd649..6d4375964876b 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -68,16 +68,20 @@ export const ResourceRow: React.FC = ({ icon, href, name, stat
- {href ? + {href ? ( + + {name} + + + ) : ( {name} - - : {name}} + )}
@@ -124,7 +128,7 @@ const useResourceRowStyles = makeStyles((theme) => ({ margin: `0 ${theme.spacing(0.5)}px`, opacity: 0.7, fontSize: 16, - } + }, })) export const Title: React.FC = ({ children }) => { @@ -148,7 +152,6 @@ const useTitleStyles = makeStyles((theme) => ({ }, })) - export const Workspace: React.FC = ({ workspace }) => { const styles = useStyles() @@ -157,7 +160,9 @@ export const Workspace: React.FC = ({ workspace }) => { {workspace.name} - test-org{" / "}test-project + test-org + {" / "} + test-project
@@ -173,7 +178,7 @@ export const Workspace: React.FC = ({ workspace }) => {
- +
@@ -185,7 +190,12 @@ export const Workspace: React.FC = ({ workspace }) => {
- +
From 53ff3257133f2aa8dbd146a5822c11edd0c3462a Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 1 Feb 2022 01:08:48 +0000 Subject: [PATCH 08/10] More prototyping --- site/components/Workspace/Workspace.tsx | 49 ++++++++++++++++++++----- 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index 6d4375964876b..94dec0ac641ac 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -1,11 +1,12 @@ +import Box from "@material-ui/core/Box" import Paper from "@material-ui/core/Paper" import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import OpenInNewIcon from "@material-ui/icons/OpenInNew" -import React from "react" +import React, { useState } from "react" import MoreVertIcon from "@material-ui/icons/MoreVert" import { QuestionHelp } from "../QuestionHelp" -import { CircularProgress, IconButton, Link } from "@material-ui/core" +import { CircularProgress, IconButton, Link, Menu, MenuItem } from "@material-ui/core" import { Timeline as TestTimeline } from "../Timeline" @@ -62,6 +63,8 @@ const ResourceIconSize = 20 export const ResourceRow: React.FC = ({ icon, href, name, status }) => { const styles = useResourceRowStyles() + const [menuAnchorEl, setMenuAnchorEl] = useState(null) + return (
@@ -87,9 +90,26 @@ export const ResourceRow: React.FC = ({ icon, href, name, stat
- + setMenuAnchorEl(ev.currentTarget)}> + + setMenuAnchorEl(undefined)}> + { + setMenuAnchorEl(undefined) + }} + > + SSH + + { + setMenuAnchorEl(undefined) + }} + > + Remote Desktop + +
) @@ -152,18 +172,27 @@ const useTitleStyles = makeStyles((theme) => ({ }, })) +const TitleIconSize = 48 + export const Workspace: React.FC = ({ workspace }) => { const styles = useStyles() return (
- {workspace.name} - - test-org - {" / "} - test-project - +
+ + + +
+ {workspace.name} + + test-org + {" / "} + test-project + +
+
@@ -178,7 +207,7 @@ export const Workspace: React.FC = ({ workspace }) => {
- +
From de7d79beccc6b4ba4a99d7ef4f60b96be3205084 Mon Sep 17 00:00:00 2001 From: Bryan Phelps Date: Tue, 1 Feb 2022 01:39:09 +0000 Subject: [PATCH 09/10] Tweaks --- site/components/Workspace/Workspace.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index 94dec0ac641ac..0ce89b2182473 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -210,6 +210,7 @@ export const Workspace: React.FC = ({ workspace }) => {
+ <Typography variant="h6">Resources</Typography> From dd38938264aeff57d871692db1bcfb7a4972a1f7 Mon Sep 17 00:00:00 2001 From: Bryan Phelps <bryan@coder.com> Date: Wed, 2 Feb 2022 04:59:39 +0000 Subject: [PATCH 10/10] Experiment with resource monitor --- site/components/ResourceMonitor.tsx | 229 ++++++++++++++++++++++++ site/components/Workspace/Workspace.tsx | 50 +++++- site/package.json | 3 + site/yarn.lock | 49 ++++- 4 files changed, 322 insertions(+), 9 deletions(-) create mode 100644 site/components/ResourceMonitor.tsx diff --git a/site/components/ResourceMonitor.tsx b/site/components/ResourceMonitor.tsx new file mode 100644 index 0000000000000..c7738b78483b7 --- /dev/null +++ b/site/components/ResourceMonitor.tsx @@ -0,0 +1,229 @@ +import React from "react" +import { Bar, Line } from "react-chartjs-2" +import { Chart, ChartOptions } from "chart.js" + +const multiply = { + beforeDraw: function (chart: Chart, options: ChartOptions) { + if (chart && chart.ctx) { + chart.ctx.globalCompositeOperation = "multiply" + } + }, + afterDatasetsDraw: function (chart: Chart, options: ChartOptions) { + if (chart && chart.ctx) { + chart.ctx.globalCompositeOperation = "source-over" + } + }, +} + +function formatBytes(bytes: number, decimals = 2) { + if (bytes === 0) return "0 Bytes" + + const k = 1024 + const dm = decimals < 0 ? 0 : decimals + const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] + + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i] +} + +const padding = 64 + +const opts: ChartOptions = { + responsive: true, + maintainAspectRatio: false, + legend: { + fullWidth: true, + display: false, + }, + elements: { + point: { + radius: 0, + hitRadius: 8, + hoverRadius: 8, + }, + rectangle: { + borderWidth: 0, + }, + }, + layout: { + padding: { + top: padding, + bottom: padding, + }, + }, + tooltips: { + mode: "index", + axis: "y", + cornerRadius: 8, + borderWidth: 0, + titleFontStyle: "normal", + callbacks: { + label: (item: any, data: any) => { + const dataset = data.datasets[item.datasetIndex] + const num: number = dataset.data[item.index] as number + if (num) { + return dataset.label + ": " + num.toFixed(2) + "%" + } + }, + labelColor: (item: any, data: any) => { + const dataset = data.data.datasets[item.datasetIndex] + return { + // Trim off the transparent hex code. + backgroundColor: (dataset.pointBackgroundColor as string).substr(0, 7), + borderColor: "#000000", + } + }, + title: (item) => { + console.log(item[0]) + + return "Resources: " + item[0].label + }, + }, + }, + plugins: { + tooltip: { + callbacks: { + beforeTitle: (item: any) => { + console.log("BEFORE TITLE: " + item) + return "Resources" + }, + }, + }, + legend: { + display: false, + }, + }, + scales: { + xAxes: [ + { + display: false, + ticks: { + stepSize: 10, + maxTicksLimit: 4, + maxRotation: 0, + }, + }, + ], + yAxes: [ + { + gridLines: { + color: "rgba(0, 0, 0, 0.09)", + zeroLineColor: "rgba(0, 0, 0, 0.09)", + }, + ticks: { + callback: (v) => v + "%", + max: 100, + maxTicksLimit: 2, + min: 0, + padding: 4, + }, + }, + ], + }, +} + +export interface ResourceUsageSnapshot { + cpuPercentage: number + memoryUsedBytes: number + diskUsedBytes: number +} + +export interface ResourceMonitorProps { + readonly diskTotalBytes: number + readonly memoryTotalBytes: number + readonly resources: ReadonlyArray<ResourceUsageSnapshot> +} + +export const ResourceMonitor: React.FC<ResourceMonitorProps> = (props) => { + const dataF = React.useMemo(() => { + return (canvas: any) => { + // Store gradients inside the canvas object for easy access. + // This function is called everytime resources values change... + // we don't want to allocate a new gradient everytime. + if (!canvas["cpuGradient"]) { + const cpuGradient = canvas.getContext("2d").createLinearGradient(0, 0, 0, canvas.height) + cpuGradient.addColorStop(1, "#9787FF32") + cpuGradient.addColorStop(0, "#5555FFC4") + canvas["cpuGradient"] = cpuGradient + } + + if (!canvas["memGradient"]) { + const memGradient = canvas.getContext("2d").createLinearGradient(0, 0, 0, canvas.height) + memGradient.addColorStop(1, "#55FF8532") + memGradient.addColorStop(0, "#42B863C4") + canvas["memGradient"] = memGradient + } + + if (!canvas["diskGradient"]) { + const diskGradient = canvas.getContext("2d").createLinearGradient(0, 0, 0, canvas.height) + diskGradient.addColorStop(1, "#97979700") + diskGradient.addColorStop(0, "#797979C4") + canvas["diskGradient"] = diskGradient + } + + const cpuPercentages = [] + //const cpuPercentages = Array(20 - props.resources.length).fill(null) + cpuPercentages.push(...props.resources.map((r) => r.cpuPercentage)) + + //const memPercentages = Array(20 - props.resources.length).fill(null) + const memPercentages = [] + memPercentages.push(...props.resources.map((r) => (r.memoryUsedBytes / props.memoryTotalBytes) * 100)) + + const diskPercentages = [] + //const diskPercentages = Array(20 - props.resources.length).fill(null) + diskPercentages.push(...props.resources.map((r) => (r.diskUsedBytes / props.diskTotalBytes) * 100)) + + return { + labels: Array(20) + .fill(0) + .map((_, index) => (20 - index) * 3 + "s ago"), + datasets: [ + { + label: "CPU", + data: cpuPercentages, + backgroundColor: canvas["cpuGradient"], + borderColor: "transparent", + pointBackgroundColor: "#9787FF32", + pointBorderColor: "#FFFFFF", + lineTension: 0.4, + fill: true, + }, + { + label: "Memory", + data: memPercentages, + backgroundColor: canvas["memGradient"], + borderColor: "transparent", + pointBackgroundColor: "#55FF8532", + pointBorderColor: "#FFFFFF", + lineTension: 0.4, + fill: true, + }, + { + label: "Disk", + data: diskPercentages, + backgroundColor: canvas["diskGradient"], + borderColor: "transparent", + pointBackgroundColor: "#97979732", + pointBorderColor: "#FFFFFF", + lineTension: 0.4, + fill: true, + }, + ], + } + } + }, [props.resources]) + + return ( + <Line + type="line" + height={40 + padding * 2} + data={dataF} + options={opts} + plugins={[multiply]} + ref={(ref) => { + window.Chart.defaults.global.defaultFontFamily = "'Fira Code', Inter" + }} + /> + ) +} diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx index 0ce89b2182473..fdb1d96a214e7 100644 --- a/site/components/Workspace/Workspace.tsx +++ b/site/components/Workspace/Workspace.tsx @@ -3,14 +3,15 @@ import Paper from "@material-ui/core/Paper" import { makeStyles } from "@material-ui/core/styles" import Typography from "@material-ui/core/Typography" import OpenInNewIcon from "@material-ui/icons/OpenInNew" -import React, { useState } from "react" +import React, { useEffect, useState } from "react" import MoreVertIcon from "@material-ui/icons/MoreVert" import { QuestionHelp } from "../QuestionHelp" import { CircularProgress, IconButton, Link, Menu, MenuItem } from "@material-ui/core" - +import { ResourceMonitor, ResourceMonitorProps, ResourceUsageSnapshot } from "../ResourceMonitor" import { Timeline as TestTimeline } from "../Timeline" import * as API from "../../api" +import { getAllByTestId } from "@testing-library/react" export interface WorkspaceProps { workspace: API.Workspace @@ -94,17 +95,17 @@ export const ResourceRow: React.FC<ResourceRowProps> = ({ icon, href, name, stat <MoreVertIcon fontSize="inherit" /> </IconButton> - <Menu anchorEl={menuAnchorEl} open={!!menuAnchorEl} onClose={() => setMenuAnchorEl(undefined)}> + <Menu anchorEl={menuAnchorEl} open={!!menuAnchorEl} onClose={() => setMenuAnchorEl(null)}> <MenuItem onClick={() => { - setMenuAnchorEl(undefined) + setMenuAnchorEl(null) }} > SSH </MenuItem> <MenuItem onClick={() => { - setMenuAnchorEl(undefined) + setMenuAnchorEl(null) }} > Remote Desktop @@ -177,6 +178,36 @@ const TitleIconSize = 48 export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => { const styles = useStyles() + const [resources, setResources] = useState<ResourceUsageSnapshot[]>([ + { + cpuPercentage: 50, + memoryUsedBytes: 8 * 1024 * 1024, + diskUsedBytes: 24 * 1024 * 1024, + }, + ]) + + useEffect(() => { + const rand = (range: number) => { + return range * 2 * (Math.random() - 0.5) + } + const timeout = window.setTimeout(() => { + setResources((res: ResourceUsageSnapshot[]) => { + const latest = res[0] + const newEntry = { + cpuPercentage: latest.cpuPercentage + rand(5), + memoryUsedBytes: latest.memoryUsedBytes + rand(256 * 1024), + diskUsedBytes: latest.diskUsedBytes + rand(512 * 1024), + } + const ret: ResourceUsageSnapshot[] = [newEntry, ...res] + return ret + }) + }, 1000) + + return () => { + window.clearTimeout(timeout) + } + }) + return ( <div className={styles.root}> <Paper elevation={0} className={styles.section}> @@ -193,6 +224,13 @@ export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => { </Typography> </div> </div> + <div style={{ height: "200px", position: "relative" }}> + <ResourceMonitor + diskTotalBytes={256 * 1024 * 1024} + memoryTotalBytes={16 * 1024 * 1024} + resources={resources} + /> + </div> </Paper> <div className={styles.horizontal}> <div className={styles.sideBar}> @@ -210,7 +248,7 @@ export const Workspace: React.FC<WorkspaceProps> = ({ workspace }) => { <ResourceRow name={"React App"} icon={"/static/react-icon.svg"} href={"placeholder"} status={"active"} /> </div> </Paper> - + <Paper elevation={0} className={styles.section}> <Title> <Typography variant="h6">Resources</Typography> diff --git a/site/package.json b/site/package.json index 5e899ffea59f6..df1b7e04db741 100644 --- a/site/package.json +++ b/site/package.json @@ -20,6 +20,7 @@ "@material-ui/icons": "4.5.1", "@material-ui/lab": "4.0.0-alpha.60", "@testing-library/react": "12.1.2", + "@types/chart.js": "^2.9.35", "@types/express": "4.17.13", "@types/jest": "27.4.0", "@types/node": "14.18.10", @@ -28,6 +29,7 @@ "@types/superagent": "4.1.15", "@typescript-eslint/eslint-plugin": "4.33.0", "@typescript-eslint/parser": "4.33.0", + "chart.js": "2.9.4", "eslint": "7.32.0", "eslint-config-prettier": "8.3.0", "eslint-import-resolver-alias": "1.1.2", @@ -48,6 +50,7 @@ "next-router-mock": "^0.6.5", "prettier": "2.5.1", "react": "17.0.2", + "react-chartjs-2": "2.11.2", "react-dom": "17.0.2", "sql-formatter": "^4.0.2", "swr": "1.2.0", diff --git a/site/yarn.lock b/site/yarn.lock index 13998609b37b8..7ee5ef8b104df 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -819,6 +819,13 @@ "@types/connect" "*" "@types/node" "*" +"@types/chart.js@^2.9.35": + version "2.9.35" + resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.35.tgz#10ddee097ab9320f8eabd8a31017fda3644d9218" + integrity sha512-MWx/zZlh4wHBbM4Tm4YsZyYGb1/LkTiFLFwX/FXb0EJCvXX2xWTRHwlJ2RAAEXWxLrOdaAWP8vFtJXny+4CpEw== + dependencies: + moment "^2.10.2" + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -1536,6 +1543,29 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chart.js@2.9.4: + version "2.9.4" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684" + integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A== + dependencies: + chartjs-color "^2.1.0" + moment "^2.10.2" + +chartjs-color-string@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71" + integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A== + dependencies: + color-name "^1.0.0" + +chartjs-color@^2.1.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0" + integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w== + dependencies: + chartjs-color-string "^0.6.0" + color-convert "^1.9.3" + ci-info@^3.2.0: version "3.3.0" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.3.0.tgz#b4ed1fb6818dea4803a55c623041f9165d2066b2" @@ -1570,7 +1600,7 @@ collect-v8-coverage@^1.0.0: resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== -color-convert@^1.9.0: +color-convert@^1.9.0, color-convert@^1.9.3: version "1.9.3" resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== @@ -1589,7 +1619,7 @@ color-name@1.1.3: resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= -color-name@~1.1.4: +color-name@^1.0.0, color-name@~1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== @@ -3590,7 +3620,7 @@ lodash.truncate@^4.4.2: resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193" integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM= -lodash@^4.17.21, lodash@^4.7.0: +lodash@^4.17.19, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3700,6 +3730,11 @@ minimist@^1.2.0, minimist@^1.2.5: resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602" integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw== +moment@^2.10.2: + version "2.29.1" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" + integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + ms@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" @@ -4139,6 +4174,14 @@ raw-body@2.4.2: iconv-lite "0.4.24" unpipe "1.0.0" +react-chartjs-2@2.11.2: + version "2.11.2" + resolved "https://registry.yarnpkg.com/react-chartjs-2/-/react-chartjs-2-2.11.2.tgz#156c0d2618600561efc23bef278bd48a335cadb6" + integrity sha512-hcPS9vmRJeAALPPf0uo02BiD8BDm0HNmneJYTZVR74UKprXOpql+Jy1rVuj93rKw0Jfx77mkcRfXPxTe5K83uw== + dependencies: + lodash "^4.17.19" + prop-types "^15.7.2" + react-dom@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-17.0.2.tgz#ecffb6845e3ad8dbfcdc498f0d0a939736502c23"