diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx
index f53fb1ccde8ae..1158305678745 100644
--- a/site/src/AppRouter.tsx
+++ b/site/src/AppRouter.tsx
@@ -420,10 +420,37 @@ export const AppRouter: FC = () => {
/>
- }>
- } />
- } />
- } />
+
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ />
+
+
+
+
+
+ }
+ />
diff --git a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx
index a9d2986ee5046..c88eaa221cbd1 100644
--- a/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx
+++ b/site/src/components/DeploySettingsLayout/DeploySettingsLayout.tsx
@@ -45,7 +45,7 @@ export const DeploySettingsLayout: FC = ({ children }) => {
return (
-
+
{deploymentConfig ? (
diff --git a/site/src/components/DeploySettingsLayout/Sidebar.tsx b/site/src/components/DeploySettingsLayout/Sidebar.tsx
index f051ea930db19..72a0c6661b823 100644
--- a/site/src/components/DeploySettingsLayout/Sidebar.tsx
+++ b/site/src/components/DeploySettingsLayout/Sidebar.tsx
@@ -1,8 +1,8 @@
import { makeStyles } from "@material-ui/core/styles"
import Brush from "@material-ui/icons/Brush"
import LaunchOutlined from "@material-ui/icons/LaunchOutlined"
-import LockRounded from "@material-ui/icons/LockRounded"
-import Globe from "@material-ui/icons/Public"
+import LockRounded from "@material-ui/icons/LockOutlined"
+import Globe from "@material-ui/icons/PublicOutlined"
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
import { GitIcon } from "components/Icons/GitIcon"
import { Stack } from "components/Stack/Stack"
@@ -90,9 +90,9 @@ const useStyles = makeStyles((theme) => ({
sidebarNavItem: {
color: "inherit",
display: "block",
- fontSize: 16,
+ fontSize: 14,
textDecoration: "none",
- padding: theme.spacing(1.5, 1.5, 1.5, 3),
+ padding: theme.spacing(1.5, 1.5, 1.5, 2),
borderRadius: theme.shape.borderRadius / 2,
transition: "background-color 0.15s ease-in-out",
marginBottom: 1,
@@ -115,7 +115,8 @@ const useStyles = makeStyles((theme) => ({
left: 0,
top: 0,
backgroundColor: theme.palette.secondary.dark,
- borderRadius: theme.shape.borderRadius,
+ borderTopLeftRadius: theme.shape.borderRadius,
+ borderBottomLeftRadius: theme.shape.borderRadius,
},
},
diff --git a/site/src/components/SettingsLayout/Section.tsx b/site/src/components/SettingsLayout/Section.tsx
new file mode 100644
index 0000000000000..e701dd4d7dc6d
--- /dev/null
+++ b/site/src/components/SettingsLayout/Section.tsx
@@ -0,0 +1,79 @@
+import { makeStyles } from "@material-ui/core/styles"
+import Typography from "@material-ui/core/Typography"
+import { FC } from "react"
+import { SectionAction } from "../SectionAction/SectionAction"
+
+type SectionLayout = "fixed" | "fluid"
+
+export interface SectionProps {
+ title?: React.ReactNode | string
+ description?: React.ReactNode
+ toolbar?: React.ReactNode
+ alert?: React.ReactNode
+ layout?: SectionLayout
+ className?: string
+ children?: React.ReactNode
+}
+
+type SectionFC = FC> & {
+ Action: typeof SectionAction
+}
+
+export const Section: SectionFC = ({
+ title,
+ description,
+ toolbar,
+ alert,
+ className = "",
+ children,
+ layout = "fixed",
+}) => {
+ const styles = useStyles({ layout })
+ return (
+
+
+ {(title || description) && (
+
+
+ {title &&
{title} }
+ {description && typeof description === "string" && (
+
+ {description}
+
+ )}
+ {description && typeof description !== "string" && (
+
{description}
+ )}
+
+ {toolbar &&
{toolbar}
}
+
+ )}
+ {alert &&
{alert}
}
+ {children}
+
+
+ )
+}
+
+// Sub-components
+Section.Action = SectionAction
+
+const useStyles = makeStyles((theme) => ({
+ inner: ({ layout }: { layout: SectionLayout }) => ({
+ maxWidth: layout === "fluid" ? "100%" : 500,
+ }),
+ alert: {
+ marginBottom: theme.spacing(1),
+ },
+ header: {
+ marginBottom: theme.spacing(3),
+ display: "flex",
+ flexDirection: "row",
+ justifyContent: "space-between",
+ },
+ description: {
+ color: theme.palette.text.secondary,
+ fontSize: 16,
+ marginTop: theme.spacing(0.5),
+ },
+}))
diff --git a/site/src/components/SettingsLayout/SettingsLayout.tsx b/site/src/components/SettingsLayout/SettingsLayout.tsx
index 77fb87cc803c6..b22be4ecebcf2 100644
--- a/site/src/components/SettingsLayout/SettingsLayout.tsx
+++ b/site/src/components/SettingsLayout/SettingsLayout.tsx
@@ -1,38 +1,42 @@
-import Box from "@material-ui/core/Box"
-import { FC } from "react"
+import { makeStyles } from "@material-ui/core/styles"
+import { Sidebar } from "./Sidebar"
+import { Stack } from "components/Stack/Stack"
+import { FC, PropsWithChildren, Suspense } from "react"
import { Helmet } from "react-helmet-async"
-import { Outlet } from "react-router-dom"
import { pageTitle } from "../../util/page"
-import { AuthAndFrame } from "../AuthAndFrame/AuthAndFrame"
import { Margins } from "../Margins/Margins"
-import { TabPanel } from "../TabPanel/TabPanel"
+import { useMe } from "hooks/useMe"
+import { Loader } from "components/Loader/Loader"
-export const Language = {
- accountLabel: "Account",
- securityLabel: "Security",
- sshKeysLabel: "SSH keys",
- settingsLabel: "Settings",
-}
-
-const menuItems = [
- { label: Language.accountLabel, path: "/settings/account" },
- { label: Language.securityLabel, path: "/settings/security" },
- { label: Language.sshKeysLabel, path: "/settings/ssh-keys" },
-]
+export const SettingsLayout: FC = ({ children }) => {
+ const styles = useStyles()
+ const me = useMe()
-export const SettingsLayout: FC = () => {
return (
-
-
-
- Codestin Search App
-
-
-
-
-
-
-
-
+ <>
+
+ Codestin Search App
+
+
+
+
+
+ }>
+ {children}
+
+
+
+ >
)
}
+
+const useStyles = makeStyles((theme) => ({
+ wrapper: {
+ padding: theme.spacing(6, 0),
+ },
+
+ content: {
+ maxWidth: 800,
+ width: "100%",
+ },
+}))
diff --git a/site/src/components/SettingsLayout/Sidebar.tsx b/site/src/components/SettingsLayout/Sidebar.tsx
new file mode 100644
index 0000000000000..95fa316c6e6da
--- /dev/null
+++ b/site/src/components/SettingsLayout/Sidebar.tsx
@@ -0,0 +1,133 @@
+import { makeStyles } from "@material-ui/core/styles"
+import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
+import { User } from "api/typesGenerated"
+import { Stack } from "components/Stack/Stack"
+import { UserAvatar } from "components/UserAvatar/UserAvatar"
+import { FC, ElementType, PropsWithChildren, ReactNode } from "react"
+import { NavLink } from "react-router-dom"
+import { combineClasses } from "util/combineClasses"
+import AccountIcon from "@material-ui/icons/Person"
+import SecurityIcon from "@material-ui/icons/LockOutlined"
+
+const SidebarNavItem: FC<
+ PropsWithChildren<{ href: string; icon: ReactNode }>
+> = ({ children, href, icon }) => {
+ const styles = useStyles()
+ return (
+
+ combineClasses([
+ styles.sidebarNavItem,
+ isActive ? styles.sidebarNavItemActive : undefined,
+ ])
+ }
+ >
+
+ {icon}
+ {children}
+
+
+ )
+}
+
+const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({
+ icon: Icon,
+}) => {
+ const styles = useStyles()
+ return
+}
+
+export const Sidebar: React.FC<{ user: User }> = ({ user }) => {
+ const styles = useStyles()
+
+ return (
+
+
+
+
+ {user.username}
+ {user.email}
+
+
+
+ }
+ >
+ Account
+
+ }
+ >
+ Security
+
+ }
+ >
+ SSH Keys
+
+
+ )
+}
+
+const useStyles = makeStyles((theme) => ({
+ sidebar: {
+ width: 245,
+ flexShrink: 0,
+ },
+ sidebarNavItem: {
+ color: "inherit",
+ display: "block",
+ fontSize: 14,
+ textDecoration: "none",
+ padding: theme.spacing(1.5, 1.5, 1.5, 2),
+ borderRadius: theme.shape.borderRadius / 2,
+ transition: "background-color 0.15s ease-in-out",
+ marginBottom: 1,
+ position: "relative",
+
+ "&:hover": {
+ backgroundColor: theme.palette.action.hover,
+ },
+ },
+ sidebarNavItemActive: {
+ backgroundColor: theme.palette.action.hover,
+
+ "&:before": {
+ content: '""',
+ display: "block",
+ width: 3,
+ height: "100%",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ backgroundColor: theme.palette.secondary.dark,
+ borderTopLeftRadius: theme.shape.borderRadius,
+ borderBottomLeftRadius: theme.shape.borderRadius,
+ },
+ },
+ sidebarNavItemIcon: {
+ width: theme.spacing(2),
+ height: theme.spacing(2),
+ },
+ userInfo: {
+ marginBottom: theme.spacing(2),
+ },
+ userData: {
+ overflow: "hidden",
+ },
+ username: {
+ fontWeight: 600,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ },
+ email: {
+ color: theme.palette.text.secondary,
+ fontSize: 12,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ },
+}))
diff --git a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx
index 7eec41bac6f3c..0b09e7ad3f48d 100644
--- a/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx
+++ b/site/src/components/SettingsSecurityForm/SettingsSecurityForm.tsx
@@ -77,6 +77,9 @@ export const SecurityForm: FC = ({
)}
= ({
/>
= ({
/>
{
}
return (
-
+
- The following public key is used to authenticate Git in workspaces. You
- may add it to Git services (such as GitHub) that you need to access from
- your workspace.
-
- Coder configures authentication via $GIT_SSH_COMMAND
.
-
- ),
regenerateDialogTitle: "Regenerate SSH key?",
regenerateDialogMessage:
"You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.",
@@ -41,7 +32,7 @@ export const SSHKeysPage: FC> = () => {
return (
<>
-
+
{
+ const styles = useStyles()
+
if (isLoading) {
return (
@@ -43,7 +46,6 @@ export const SSHKeysPageView: FC<
{/* Regenerating the key is not an option if getSSHKey fails.
Only one of the error messages will exist at a single time */}
-
{Boolean(getSSHKeyError) && (
)}
@@ -57,6 +59,12 @@ export const SSHKeysPageView: FC<
)}
{hasLoaded && sshKey && (
<>
+
+ The following public key is used to authenticate Git in workspaces.
+ You may add it to Git services (such as GitHub) that you need to
+ access from your workspace. Coder configures authentication via{" "}
+ $GIT_SSH_COMMAND
.
+
@@ -68,3 +76,18 @@ export const SSHKeysPageView: FC<
)
}
+
+const useStyles = makeStyles((theme) => ({
+ description: {
+ fontSize: 14,
+ color: theme.palette.text.secondary,
+ margin: 0,
+ },
+ code: {
+ background: theme.palette.divider,
+ fontSize: 12,
+ padding: "2px 4px",
+ color: theme.palette.text.primary,
+ borderRadius: 2,
+ },
+}))
diff --git a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
index bf5905c85c1fc..75fa1290636bd 100644
--- a/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
+++ b/site/src/pages/UserSettingsPage/SecurityPage/SecurityPage.tsx
@@ -2,7 +2,7 @@ import { useMachine } from "@xstate/react"
import { useMe } from "hooks/useMe"
import { FC } from "react"
import { userSecuritySettingsMachine } from "xServices/userSecuritySettings/userSecuritySettingsXService"
-import { Section } from "../../../components/Section/Section"
+import { Section } from "../../../components/SettingsLayout/Section"
import { SecurityForm } from "../../../components/SettingsSecurityForm/SettingsSecurityForm"
export const Language = {
@@ -22,7 +22,7 @@ export const SecurityPage: FC = () => {
const { error } = securityState.context
return (
-