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" pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy