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 ( + + ) +} + +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. +