diff --git a/site/components/QuestionHelp.tsx b/site/components/QuestionHelp.tsx new file mode 100644 index 0000000000000..5a4f44851c12a --- /dev/null +++ b/site/components/QuestionHelp.tsx @@ -0,0 +1,21 @@ +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/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 +} + +export const ResourceMonitor: React.FC = (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 ( + { + window.Chart.defaults.global.defaultFontFamily = "'Fira Code', Inter" + }} + /> + ) +} 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..5cf84335395cd --- /dev/null +++ b/site/components/Timeline/index.tsx @@ -0,0 +1,280 @@ +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 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 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", + 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[] +} + +// 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 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 ( +
+ + {expanded && } +
+ ) +} + +const useBuildLogStyles = (status: BuildLogStatus) => + makeStyles((theme) => ({ + container: { + borderLeft: `2px solid ${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], + }, + }, + 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 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" + }, +})) diff --git a/site/components/Workspace/Workspace.tsx b/site/components/Workspace/Workspace.tsx new file mode 100644 index 0000000000000..fdb1d96a214e7 --- /dev/null +++ b/site/components/Workspace/Workspace.tsx @@ -0,0 +1,320 @@ +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, { 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 +} + +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() + + if (status == "loading") { + return + } else { + const className = status === "active" ? styles.active : styles.inactive + return
+ } +} + +type ResourceStatus = "active" | "inactive" | "loading" + +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() + + const [menuAnchorEl, setMenuAnchorEl] = useState(null) + + return ( +
+
+ +
+
+ {href ? ( + + {name} + + + ) : ( + {name} + )} +
+
+ +
+
+ setMenuAnchorEl(ev.currentTarget)}> + + + + setMenuAnchorEl(null)}> + { + setMenuAnchorEl(null) + }} + > + SSH + + { + setMenuAnchorEl(null) + }} + > + Remote Desktop + + +
+
+ ) +} + +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", + }, + action: { + margin: `0 ${theme.spacing(0.5)}px`, + opacity: 0.7, + fontSize: 16, + }, +})) + +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", + flexDirection: "row", + height: theme.spacing(6), + marginBottom: theme.spacing(2), + marginTop: -theme.spacing(1), + paddingBottom: theme.spacing(1), + paddingLeft: Constants.CardPadding + theme.spacing(1), + paddingRight: Constants.CardPadding / 2, + }, +})) + +const TitleIconSize = 48 + +export const Workspace: React.FC = ({ workspace }) => { + const styles = useStyles() + + const [resources, setResources] = useState([ + { + 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 ( +
+ +
+ + + +
+ {workspace.name} + + test-org + {" / "} + test-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> + + + +
+
+ ) +} + +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", + }, + vertical: { + display: "flex", + flexDirection: "column", + }, + section: common, + sideBar: { + display: "flex", + flexDirection: "column", + width: "400px", + }, + main: { + ...common, + flex: 1, + }, + } +}) diff --git a/site/components/Workspace/index.ts b/site/components/Workspace/index.ts new file mode 100644 index 0000000000000..4c8c38cc721c8 --- /dev/null +++ b/site/components/Workspace/index.ts @@ -0,0 +1 @@ +export * from "./Workspace" 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" diff --git a/site/package.json b/site/package.json index 80ccbe0071ebd..b3038fc2a6cc1 100644 --- a/site/package.json +++ b/site/package.json @@ -20,13 +20,14 @@ "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", "@react-theming/storybook-addon": "1.1.5", "@storybook/addon-actions": "6.4.18", "@storybook/addon-essentials": "6.4.18", "@storybook/addon-links": "6.4.18", "@storybook/react": "6.4.18", "@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", @@ -35,6 +36,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", @@ -55,6 +57,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.1", diff --git a/site/pages/projects/[organization]/[project]/create.tsx b/site/pages/projects/[organization]/[project]/create.tsx index 07e829426aff6..26f26b76a77d0 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 } diff --git a/site/pages/workspaces/[user]/[workspace]/index.tsx b/site/pages/workspaces/[user]/[workspace]/index.tsx new file mode 100644 index 0000000000000..c019f7d7f8e90 --- /dev/null +++ b/site/pages/workspaces/[user]/[workspace]/index.tsx @@ -0,0 +1,45 @@ +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 WorkspacesPage: 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 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/site/yarn.lock b/site/yarn.lock index c026a2de73f2f..88f1777ad489a 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -1546,16 +1546,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" @@ -2799,6 +2799,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/color-convert@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/color-convert/-/color-convert-2.0.0.tgz#8f5ee6b9e863dcbee5703f5a517ffb13d3ea4e22" @@ -4505,6 +4512,29 @@ character-reference-invalid@^1.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz#083329cda0eae272ab3dbbf37e9a382c13af1560" integrity sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg== +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" + chokidar@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -8988,6 +9018,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== +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== + move-concurrently@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/move-concurrently/-/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92" @@ -10130,6 +10165,14 @@ react-base16-styling@^0.6.0: lodash.flow "^3.3.0" pure-color "^1.2.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-color@^2.18.0: version "2.19.3" resolved "https://registry.yarnpkg.com/react-color/-/react-color-2.19.3.tgz#ec6c6b4568312a3c6a18420ab0472e146aa5683d"