mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: User settings page (#5661)
This commit is contained in:
+31
-4
@@ -420,10 +420,37 @@ export const AppRouter: FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="settings" element={<SettingsLayout />}>
|
<Route path="settings">
|
||||||
<Route path="account" element={<AccountPage />} />
|
<Route
|
||||||
<Route path="security" element={<SecurityPage />} />
|
path="account"
|
||||||
<Route path="ssh-keys" element={<SSHKeysPage />} />
|
element={
|
||||||
|
<AuthAndFrame>
|
||||||
|
<SettingsLayout>
|
||||||
|
<AccountPage />
|
||||||
|
</SettingsLayout>
|
||||||
|
</AuthAndFrame>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="security"
|
||||||
|
element={
|
||||||
|
<AuthAndFrame>
|
||||||
|
<SettingsLayout>
|
||||||
|
<SecurityPage />
|
||||||
|
</SettingsLayout>
|
||||||
|
</AuthAndFrame>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="ssh-keys"
|
||||||
|
element={
|
||||||
|
<AuthAndFrame>
|
||||||
|
<SettingsLayout>
|
||||||
|
<SSHKeysPage />
|
||||||
|
</SettingsLayout>
|
||||||
|
</AuthAndFrame>
|
||||||
|
}
|
||||||
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route path="/@:username">
|
<Route path="/@:username">
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const DeploySettingsLayout: FC<PropsWithChildren> = ({ children }) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Margins>
|
<Margins>
|
||||||
<Stack className={styles.wrapper} direction="row" spacing={5}>
|
<Stack className={styles.wrapper} direction="row" spacing={6}>
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
<main className={styles.content}>
|
<main className={styles.content}>
|
||||||
{deploymentConfig ? (
|
{deploymentConfig ? (
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { makeStyles } from "@material-ui/core/styles"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import Brush from "@material-ui/icons/Brush"
|
import Brush from "@material-ui/icons/Brush"
|
||||||
import LaunchOutlined from "@material-ui/icons/LaunchOutlined"
|
import LaunchOutlined from "@material-ui/icons/LaunchOutlined"
|
||||||
import LockRounded from "@material-ui/icons/LockRounded"
|
import LockRounded from "@material-ui/icons/LockOutlined"
|
||||||
import Globe from "@material-ui/icons/Public"
|
import Globe from "@material-ui/icons/PublicOutlined"
|
||||||
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
|
import VpnKeyOutlined from "@material-ui/icons/VpnKeyOutlined"
|
||||||
import { GitIcon } from "components/Icons/GitIcon"
|
import { GitIcon } from "components/Icons/GitIcon"
|
||||||
import { Stack } from "components/Stack/Stack"
|
import { Stack } from "components/Stack/Stack"
|
||||||
@@ -90,9 +90,9 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
sidebarNavItem: {
|
sidebarNavItem: {
|
||||||
color: "inherit",
|
color: "inherit",
|
||||||
display: "block",
|
display: "block",
|
||||||
fontSize: 16,
|
fontSize: 14,
|
||||||
textDecoration: "none",
|
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,
|
borderRadius: theme.shape.borderRadius / 2,
|
||||||
transition: "background-color 0.15s ease-in-out",
|
transition: "background-color 0.15s ease-in-out",
|
||||||
marginBottom: 1,
|
marginBottom: 1,
|
||||||
@@ -115,7 +115,8 @@ const useStyles = makeStyles((theme) => ({
|
|||||||
left: 0,
|
left: 0,
|
||||||
top: 0,
|
top: 0,
|
||||||
backgroundColor: theme.palette.secondary.dark,
|
backgroundColor: theme.palette.secondary.dark,
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderTopLeftRadius: theme.shape.borderRadius,
|
||||||
|
borderBottomLeftRadius: theme.shape.borderRadius,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -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<React.PropsWithChildren<SectionProps>> & {
|
||||||
|
Action: typeof SectionAction
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Section: SectionFC = ({
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
toolbar,
|
||||||
|
alert,
|
||||||
|
className = "",
|
||||||
|
children,
|
||||||
|
layout = "fixed",
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles({ layout })
|
||||||
|
return (
|
||||||
|
<section className={className}>
|
||||||
|
<div className={styles.inner}>
|
||||||
|
{(title || description) && (
|
||||||
|
<div className={styles.header}>
|
||||||
|
<div>
|
||||||
|
{title && <Typography variant="h4">{title}</Typography>}
|
||||||
|
{description && typeof description === "string" && (
|
||||||
|
<Typography className={styles.description}>
|
||||||
|
{description}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{description && typeof description !== "string" && (
|
||||||
|
<div className={styles.description}>{description}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{toolbar && <div>{toolbar}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{alert && <div className={styles.alert}>{alert}</div>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -1,38 +1,42 @@
|
|||||||
import Box from "@material-ui/core/Box"
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import { FC } from "react"
|
import { Sidebar } from "./Sidebar"
|
||||||
|
import { Stack } from "components/Stack/Stack"
|
||||||
|
import { FC, PropsWithChildren, Suspense } from "react"
|
||||||
import { Helmet } from "react-helmet-async"
|
import { Helmet } from "react-helmet-async"
|
||||||
import { Outlet } from "react-router-dom"
|
|
||||||
import { pageTitle } from "../../util/page"
|
import { pageTitle } from "../../util/page"
|
||||||
import { AuthAndFrame } from "../AuthAndFrame/AuthAndFrame"
|
|
||||||
import { Margins } from "../Margins/Margins"
|
import { Margins } from "../Margins/Margins"
|
||||||
import { TabPanel } from "../TabPanel/TabPanel"
|
import { useMe } from "hooks/useMe"
|
||||||
|
import { Loader } from "components/Loader/Loader"
|
||||||
|
|
||||||
export const Language = {
|
export const SettingsLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||||
accountLabel: "Account",
|
const styles = useStyles()
|
||||||
securityLabel: "Security",
|
const me = useMe()
|
||||||
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 = () => {
|
|
||||||
return (
|
return (
|
||||||
<AuthAndFrame>
|
<>
|
||||||
<Box display="flex" flexDirection="column">
|
<Helmet>
|
||||||
<Helmet>
|
<title>{pageTitle("Settings")}</title>
|
||||||
<title>{pageTitle("Settings")}</title>
|
</Helmet>
|
||||||
</Helmet>
|
|
||||||
<Margins>
|
<Margins>
|
||||||
<TabPanel title={Language.settingsLabel} menuItems={menuItems}>
|
<Stack className={styles.wrapper} direction="row" spacing={6}>
|
||||||
<Outlet />
|
<Sidebar user={me} />
|
||||||
</TabPanel>
|
<Suspense fallback={<Loader />}>
|
||||||
</Margins>
|
<main className={styles.content}>{children}</main>
|
||||||
</Box>
|
</Suspense>
|
||||||
</AuthAndFrame>
|
</Stack>
|
||||||
|
</Margins>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useStyles = makeStyles((theme) => ({
|
||||||
|
wrapper: {
|
||||||
|
padding: theme.spacing(6, 0),
|
||||||
|
},
|
||||||
|
|
||||||
|
content: {
|
||||||
|
maxWidth: 800,
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<NavLink
|
||||||
|
to={href}
|
||||||
|
className={({ isActive }) =>
|
||||||
|
combineClasses([
|
||||||
|
styles.sidebarNavItem,
|
||||||
|
isActive ? styles.sidebarNavItemActive : undefined,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Stack alignItems="center" spacing={1.5} direction="row">
|
||||||
|
{icon}
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</NavLink>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({
|
||||||
|
icon: Icon,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles()
|
||||||
|
return <Icon className={styles.sidebarNavItemIcon} />
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar: React.FC<{ user: User }> = ({ user }) => {
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={styles.sidebar}>
|
||||||
|
<Stack direction="row" alignItems="center" className={styles.userInfo}>
|
||||||
|
<UserAvatar username={user.username} avatarURL={user.avatar_url} />
|
||||||
|
<Stack spacing={0} className={styles.userData}>
|
||||||
|
<span className={styles.username}>{user.username}</span>
|
||||||
|
<span className={styles.email}>{user.email}</span>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<SidebarNavItem
|
||||||
|
href="../account"
|
||||||
|
icon={<SidebarNavItemIcon icon={AccountIcon} />}
|
||||||
|
>
|
||||||
|
Account
|
||||||
|
</SidebarNavItem>
|
||||||
|
<SidebarNavItem
|
||||||
|
href="../security"
|
||||||
|
icon={<SidebarNavItemIcon icon={SecurityIcon} />}
|
||||||
|
>
|
||||||
|
Security
|
||||||
|
</SidebarNavItem>
|
||||||
|
<SidebarNavItem
|
||||||
|
href="../ssh-keys"
|
||||||
|
icon={<SidebarNavItemIcon icon={VpnKeyOutlined} />}
|
||||||
|
>
|
||||||
|
SSH Keys
|
||||||
|
</SidebarNavItem>
|
||||||
|
</nav>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
}))
|
||||||
@@ -77,6 +77,9 @@ export const SecurityForm: FC<SecurityFormProps> = ({
|
|||||||
)}
|
)}
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("old_password")}
|
{...getFieldHelpers("old_password")}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
autoComplete="old_password"
|
autoComplete="old_password"
|
||||||
fullWidth
|
fullWidth
|
||||||
label={Language.oldPasswordLabel}
|
label={Language.oldPasswordLabel}
|
||||||
@@ -85,6 +88,9 @@ export const SecurityForm: FC<SecurityFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("password")}
|
{...getFieldHelpers("password")}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
autoComplete="password"
|
autoComplete="password"
|
||||||
fullWidth
|
fullWidth
|
||||||
label={Language.newPasswordLabel}
|
label={Language.newPasswordLabel}
|
||||||
@@ -93,6 +99,9 @@ export const SecurityForm: FC<SecurityFormProps> = ({
|
|||||||
/>
|
/>
|
||||||
<TextField
|
<TextField
|
||||||
{...getFieldHelpers("confirm_password")}
|
{...getFieldHelpers("confirm_password")}
|
||||||
|
InputLabelProps={{
|
||||||
|
shrink: true,
|
||||||
|
}}
|
||||||
autoComplete="confirm_password"
|
autoComplete="confirm_password"
|
||||||
fullWidth
|
fullWidth
|
||||||
label={Language.confirmPasswordLabel}
|
label={Language.confirmPasswordLabel}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useActor } from "@xstate/react"
|
import { useActor } from "@xstate/react"
|
||||||
import { useContext, FC } from "react"
|
import { FC, useContext } from "react"
|
||||||
import { Section } from "../../../components/Section/Section"
|
import { Section } from "../../../components/SettingsLayout/Section"
|
||||||
import { AccountForm } from "../../../components/SettingsAccountForm/SettingsAccountForm"
|
import { AccountForm } from "../../../components/SettingsAccountForm/SettingsAccountForm"
|
||||||
import { XServiceContext } from "../../../xServices/StateContext"
|
import { XServiceContext } from "../../../xServices/StateContext"
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ export const AccountPage: FC = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title={Language.title}>
|
<Section title={Language.title} description="Update your account info">
|
||||||
<AccountForm
|
<AccountForm
|
||||||
editable={Boolean(canEditUsers)}
|
editable={Boolean(canEditUsers)}
|
||||||
email={me.email}
|
email={me.email}
|
||||||
|
|||||||
@@ -1,21 +1,12 @@
|
|||||||
import { useActor } from "@xstate/react"
|
import { useActor } from "@xstate/react"
|
||||||
import { useContext, useEffect, PropsWithChildren, FC } from "react"
|
import { useContext, useEffect, PropsWithChildren, FC } from "react"
|
||||||
import { ConfirmDialog } from "../../../components/Dialogs/ConfirmDialog/ConfirmDialog"
|
import { ConfirmDialog } from "../../../components/Dialogs/ConfirmDialog/ConfirmDialog"
|
||||||
import { Section } from "../../../components/Section/Section"
|
import { Section } from "../../../components/SettingsLayout/Section"
|
||||||
import { XServiceContext } from "../../../xServices/StateContext"
|
import { XServiceContext } from "../../../xServices/StateContext"
|
||||||
import { SSHKeysPageView } from "./SSHKeysPageView"
|
import { SSHKeysPageView } from "./SSHKeysPageView"
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
title: "SSH keys",
|
title: "SSH keys",
|
||||||
description: (
|
|
||||||
<p>
|
|
||||||
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. <br />
|
|
||||||
<br />
|
|
||||||
Coder configures authentication via <code>$GIT_SSH_COMMAND</code>.
|
|
||||||
</p>
|
|
||||||
),
|
|
||||||
regenerateDialogTitle: "Regenerate SSH key?",
|
regenerateDialogTitle: "Regenerate SSH key?",
|
||||||
regenerateDialogMessage:
|
regenerateDialogMessage:
|
||||||
"You will need to replace the public SSH key on services you use it with, and you'll need to rebuild existing workspaces.",
|
"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<PropsWithChildren<unknown>> = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Section title={Language.title} description={Language.description}>
|
<Section title={Language.title}>
|
||||||
<SSHKeysPageView
|
<SSHKeysPageView
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
hasLoaded={hasLoaded}
|
hasLoaded={hasLoaded}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { makeStyles } from "@material-ui/core/styles"
|
||||||
import Box from "@material-ui/core/Box"
|
import Box from "@material-ui/core/Box"
|
||||||
import Button from "@material-ui/core/Button"
|
import Button from "@material-ui/core/Button"
|
||||||
import CircularProgress from "@material-ui/core/CircularProgress"
|
import CircularProgress from "@material-ui/core/CircularProgress"
|
||||||
@@ -31,6 +32,8 @@ export const SSHKeysPageView: FC<
|
|||||||
sshKey,
|
sshKey,
|
||||||
onRegenerateClick,
|
onRegenerateClick,
|
||||||
}) => {
|
}) => {
|
||||||
|
const styles = useStyles()
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<Box p={4}>
|
<Box p={4}>
|
||||||
@@ -43,7 +46,6 @@ export const SSHKeysPageView: FC<
|
|||||||
<Stack>
|
<Stack>
|
||||||
{/* Regenerating the key is not an option if getSSHKey fails.
|
{/* Regenerating the key is not an option if getSSHKey fails.
|
||||||
Only one of the error messages will exist at a single time */}
|
Only one of the error messages will exist at a single time */}
|
||||||
|
|
||||||
{Boolean(getSSHKeyError) && (
|
{Boolean(getSSHKeyError) && (
|
||||||
<AlertBanner severity="error" error={getSSHKeyError} />
|
<AlertBanner severity="error" error={getSSHKeyError} />
|
||||||
)}
|
)}
|
||||||
@@ -57,6 +59,12 @@ export const SSHKeysPageView: FC<
|
|||||||
)}
|
)}
|
||||||
{hasLoaded && sshKey && (
|
{hasLoaded && sshKey && (
|
||||||
<>
|
<>
|
||||||
|
<p className={styles.description}>
|
||||||
|
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{" "}
|
||||||
|
<code className={styles.code}>$GIT_SSH_COMMAND</code>.
|
||||||
|
</p>
|
||||||
<CodeExample code={sshKey.public_key.trim()} />
|
<CodeExample code={sshKey.public_key.trim()} />
|
||||||
<div>
|
<div>
|
||||||
<Button onClick={onRegenerateClick}>
|
<Button onClick={onRegenerateClick}>
|
||||||
@@ -68,3 +76,18 @@ export const SSHKeysPageView: FC<
|
|||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useMachine } from "@xstate/react"
|
|||||||
import { useMe } from "hooks/useMe"
|
import { useMe } from "hooks/useMe"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { userSecuritySettingsMachine } from "xServices/userSecuritySettings/userSecuritySettingsXService"
|
import { userSecuritySettingsMachine } from "xServices/userSecuritySettings/userSecuritySettingsXService"
|
||||||
import { Section } from "../../../components/Section/Section"
|
import { Section } from "../../../components/SettingsLayout/Section"
|
||||||
import { SecurityForm } from "../../../components/SettingsSecurityForm/SettingsSecurityForm"
|
import { SecurityForm } from "../../../components/SettingsSecurityForm/SettingsSecurityForm"
|
||||||
|
|
||||||
export const Language = {
|
export const Language = {
|
||||||
@@ -22,7 +22,7 @@ export const SecurityPage: FC = () => {
|
|||||||
const { error } = securityState.context
|
const { error } = securityState.context
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section title={Language.title}>
|
<Section title={Language.title} description="Update your account password">
|
||||||
<SecurityForm
|
<SecurityForm
|
||||||
updateSecurityError={error}
|
updateSecurityError={error}
|
||||||
isLoading={securityState.matches("updatingSecurity")}
|
isLoading={securityState.matches("updatingSecurity")}
|
||||||
|
|||||||
Reference in New Issue
Block a user