mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
refactor: move footer items into the user dropdown (#5562)
* refactor: move footer items into the user dropdown The items at the bottom looked unprofessional. Users don't always need to be prompted to join our Discord or see the active version of Coder. This moves the items in the user dropdown which looks better. * Update site/src/components/UserDropdownContent/UserDropdownContent.tsx Co-authored-by: Asher <ash@coder.com> * Fix import order Co-authored-by: Asher <ash@coder.com>
This commit is contained in:
@@ -3,7 +3,6 @@ import { useActor } from "@xstate/react"
|
||||
import { Loader } from "components/Loader/Loader"
|
||||
import { FC, Suspense, useContext, useEffect } from "react"
|
||||
import { XServiceContext } from "../../xServices/StateContext"
|
||||
import { Footer } from "../Footer/Footer"
|
||||
import { Navbar } from "../Navbar/Navbar"
|
||||
import { RequireAuth } from "../RequireAuth/RequireAuth"
|
||||
import { UpdateCheckBanner } from "components/UpdateCheckBanner/UpdateCheckBanner"
|
||||
@@ -20,7 +19,6 @@ export const AuthAndFrame: FC<AuthAndFrameProps> = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
const xServices = useContext(XServiceContext)
|
||||
const [authState] = useActor(xServices.authXService)
|
||||
const [buildInfoState] = useActor(xServices.buildInfoXService)
|
||||
const [updateCheckState, updateCheckSend] = useActor(
|
||||
xServices.updateCheckXService,
|
||||
)
|
||||
@@ -51,7 +49,6 @@ export const AuthAndFrame: FC<AuthAndFrameProps> = ({ children }) => {
|
||||
<div className={styles.siteContent}>
|
||||
<Suspense fallback={<Loader />}>{children}</Suspense>
|
||||
</div>
|
||||
<Footer buildInfo={buildInfoState.context.buildInfo} />
|
||||
</div>
|
||||
</RequireAuth>
|
||||
)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import React, { PropsWithChildren } from "react"
|
||||
import { MONOSPACE_FONT_FAMILY } from "theme/constants"
|
||||
import { combineClasses } from "util/combineClasses"
|
||||
|
||||
export const EnabledBadge: React.FC = () => {
|
||||
@@ -39,6 +40,17 @@ export const EnterpriseBadge: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export const VersionBadge: React.FC<{
|
||||
version: string
|
||||
}> = ({ version }) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
<span className={combineClasses([styles.badge, styles.versionBadge])}>
|
||||
Version: {version}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
export const Badges: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
const styles = useStyles()
|
||||
return (
|
||||
@@ -76,6 +88,16 @@ const useStyles = makeStyles((theme) => ({
|
||||
border: `1px solid ${theme.palette.info.light}`,
|
||||
},
|
||||
|
||||
versionBadge: {
|
||||
border: `1px solid ${theme.palette.success.light}`,
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
textTransform: "none",
|
||||
color: "white",
|
||||
fontFamily: MONOSPACE_FONT_FAMILY,
|
||||
textDecoration: "none",
|
||||
fontSize: 12,
|
||||
},
|
||||
|
||||
enabledBadge: {
|
||||
border: `1px solid ${theme.palette.success.light}`,
|
||||
backgroundColor: theme.palette.success.dark,
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Story } from "@storybook/react"
|
||||
import { Footer, FooterProps } from "./Footer"
|
||||
|
||||
export default {
|
||||
title: "components/Footer",
|
||||
component: Footer,
|
||||
}
|
||||
|
||||
const Template: Story<FooterProps> = (args) => <Footer {...args} />
|
||||
|
||||
export const Example = Template.bind({})
|
||||
Example.args = {
|
||||
buildInfo: {
|
||||
external_url: "",
|
||||
version: "test-1.2.3",
|
||||
},
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import { MockBuildInfo, render } from "../../testHelpers/renderHelpers"
|
||||
import { Footer, Language } from "./Footer"
|
||||
|
||||
describe("Footer", () => {
|
||||
it("renders content", async () => {
|
||||
// When
|
||||
render(<Footer buildInfo={MockBuildInfo} />)
|
||||
|
||||
// Then
|
||||
await screen.findByText("Copyright", { exact: false })
|
||||
await screen.findByText(Language.buildInfoText(MockBuildInfo))
|
||||
const reportBugLink = screen
|
||||
.getByText(Language.reportBugLink, { exact: false })
|
||||
.closest("a")
|
||||
if (!reportBugLink) {
|
||||
throw new Error("Bug report link not found in footer")
|
||||
}
|
||||
|
||||
expect(reportBugLink.getAttribute("href")?.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
@@ -1,98 +0,0 @@
|
||||
import Link from "@material-ui/core/Link"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import AccountTreeIcon from "@material-ui/icons/AccountTree"
|
||||
import AssistantIcon from "@material-ui/icons/Assistant"
|
||||
import ChatIcon from "@material-ui/icons/Chat"
|
||||
import { colors } from "theme/colors"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
|
||||
export const Language = {
|
||||
buildInfoText: (buildInfo: TypesGen.BuildInfoResponse): string => {
|
||||
return `Coder ${buildInfo.version}`
|
||||
},
|
||||
copyrightText: `Copyright \u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`,
|
||||
reportBugLink: "Report an issue or share feedback",
|
||||
discordLink: "Join Coder on Discord",
|
||||
}
|
||||
|
||||
export interface FooterProps {
|
||||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
}
|
||||
|
||||
export const Footer: React.FC<React.PropsWithChildren<FooterProps>> = ({
|
||||
buildInfo,
|
||||
}) => {
|
||||
const styles = useFooterStyles()
|
||||
|
||||
const githubUrl = `https://github.com/coder/coder/issues/new?labels=needs+grooming&body=${encodeURIComponent(`Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})
|
||||
|
||||
<!--- Ask a question or leave feedback! -->`)}`
|
||||
const discordUrl = `https://coder.com/chat?utm_source=coder&utm_medium=coder&utm_campaign=server-footer`
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.copyRight}>{Language.copyrightText}</div>
|
||||
{buildInfo && (
|
||||
<div className={styles.buildInfo}>
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="caption"
|
||||
target="_blank"
|
||||
href={buildInfo.external_url}
|
||||
>
|
||||
<AccountTreeIcon className={styles.icon} />{" "}
|
||||
{Language.buildInfoText(buildInfo)}
|
||||
</Link>
|
||||
|
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="caption"
|
||||
target="_blank"
|
||||
href={githubUrl}
|
||||
>
|
||||
<AssistantIcon className={styles.icon} /> {Language.reportBugLink}
|
||||
</Link>
|
||||
|
|
||||
<Link
|
||||
className={styles.link}
|
||||
variant="caption"
|
||||
target="_blank"
|
||||
href={discordUrl}
|
||||
>
|
||||
<ChatIcon className={styles.icon} /> {Language.discordLink}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const useFooterStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
color: colors.gray[7],
|
||||
textAlign: "center",
|
||||
flex: "0",
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingBottom: theme.spacing(2),
|
||||
marginTop: theme.spacing(8),
|
||||
},
|
||||
copyRight: {
|
||||
margin: theme.spacing(0.25),
|
||||
},
|
||||
buildInfo: {
|
||||
margin: theme.spacing(0.25),
|
||||
display: "inline-flex",
|
||||
},
|
||||
link: {
|
||||
color: theme.palette.text.secondary,
|
||||
fontWeight: 600,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
},
|
||||
icon: {
|
||||
fontSize: 12,
|
||||
color: theme.palette.secondary.dark,
|
||||
marginRight: theme.spacing(0.5),
|
||||
},
|
||||
}))
|
||||
@@ -8,6 +8,7 @@ import { NavbarView } from "../NavbarView/NavbarView"
|
||||
export const Navbar: React.FC = () => {
|
||||
const xServices = useContext(XServiceContext)
|
||||
const [authState, authSend] = useActor(xServices.authXService)
|
||||
const [buildInfoState] = useActor(xServices.buildInfoXService)
|
||||
const { me, permissions } = authState.context
|
||||
const featureVisibility = useSelector(
|
||||
xServices.entitlementsXService,
|
||||
@@ -23,6 +24,7 @@ export const Navbar: React.FC = () => {
|
||||
return (
|
||||
<NavbarView
|
||||
user={me}
|
||||
buildInfo={buildInfoState.context.buildInfo}
|
||||
onSignOut={onSignOut}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewDeployment={canViewDeployment}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { UserDropdown } from "../UserDropdown/UsersDropdown"
|
||||
|
||||
export interface NavbarViewProps {
|
||||
user?: TypesGen.User
|
||||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
onSignOut: () => void
|
||||
canViewAuditLog: boolean
|
||||
canViewDeployment: boolean
|
||||
@@ -83,6 +84,7 @@ const NavItems: React.FC<
|
||||
}
|
||||
export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
user,
|
||||
buildInfo,
|
||||
onSignOut,
|
||||
canViewAuditLog,
|
||||
canViewDeployment,
|
||||
@@ -130,7 +132,13 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
/>
|
||||
|
||||
<div className={styles.profileButton}>
|
||||
{user && <UserDropdown user={user} onSignOut={onSignOut} />}
|
||||
{user && (
|
||||
<UserDropdown
|
||||
user={user}
|
||||
buildInfo={buildInfo}
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import { FC, ReactNode } from "react"
|
||||
import { Footer } from "../../components/Footer/Footer"
|
||||
|
||||
export const useStyles = makeStyles((theme) => ({
|
||||
root: {
|
||||
@@ -28,7 +27,7 @@ export const SignInLayout: FC<{ children: ReactNode }> = ({ children }) => {
|
||||
<div className={styles.root}>
|
||||
<div className={styles.layout}>
|
||||
<div className={styles.container}>{children}</div>
|
||||
<Footer />
|
||||
{`\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -12,12 +12,13 @@ import { UserDropdownContent } from "../UserDropdownContent/UserDropdownContent"
|
||||
|
||||
export interface UserDropdownProps {
|
||||
user: TypesGen.User
|
||||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
onSignOut: () => void
|
||||
}
|
||||
|
||||
export const UserDropdown: React.FC<
|
||||
React.PropsWithChildren<UserDropdownProps>
|
||||
> = ({ user, onSignOut }: UserDropdownProps) => {
|
||||
> = ({ buildInfo, user, onSignOut }: UserDropdownProps) => {
|
||||
const styles = useStyles()
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLElement | undefined>()
|
||||
|
||||
@@ -65,6 +66,7 @@ export const UserDropdown: React.FC<
|
||||
>
|
||||
<UserDropdownContent
|
||||
user={user}
|
||||
buildInfo={buildInfo}
|
||||
onPopoverClose={onPopoverClose}
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import { MockUser } from "../../testHelpers/entities"
|
||||
import { MockBuildInfo, MockUser } from "../../testHelpers/entities"
|
||||
import { render } from "../../testHelpers/renderHelpers"
|
||||
import { Language, UserDropdownContent } from "./UserDropdownContent"
|
||||
|
||||
@@ -20,6 +20,7 @@ describe("UserDropdownContent", () => {
|
||||
render(
|
||||
<UserDropdownContent
|
||||
user={MockUser}
|
||||
buildInfo={MockBuildInfo}
|
||||
onSignOut={jest.fn()}
|
||||
onPopoverClose={jest.fn()}
|
||||
/>,
|
||||
@@ -27,6 +28,10 @@ describe("UserDropdownContent", () => {
|
||||
expect(screen.getByText(Language.accountLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.docsLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.bugLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.discordLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.copyrightText)).toBeDefined()
|
||||
expect(screen.getByText(MockBuildInfo.version)).toBeDefined()
|
||||
})
|
||||
|
||||
it("has the correct link for the account item", () => {
|
||||
|
||||
@@ -3,8 +3,12 @@ import ListItemIcon from "@material-ui/core/ListItemIcon"
|
||||
import ListItemText from "@material-ui/core/ListItemText"
|
||||
import MenuItem from "@material-ui/core/MenuItem"
|
||||
import { makeStyles } from "@material-ui/core/styles"
|
||||
import Tooltip from "@material-ui/core/Tooltip"
|
||||
import Typography from "@material-ui/core/Typography"
|
||||
import AccountIcon from "@material-ui/icons/AccountCircleOutlined"
|
||||
import BugIcon from "@material-ui/icons/BugReportOutlined"
|
||||
import ChatIcon from "@material-ui/icons/Chat"
|
||||
import LaunchIcon from "@material-ui/icons/Launch"
|
||||
import { FC } from "react"
|
||||
import { Link } from "react-router-dom"
|
||||
import * as TypesGen from "../../api/typesGenerated"
|
||||
@@ -17,20 +21,29 @@ export const Language = {
|
||||
accountLabel: "Account",
|
||||
docsLabel: "Documentation",
|
||||
signOutLabel: "Sign Out",
|
||||
bugLabel: "Report a Bug",
|
||||
discordLabel: "Join the Coder Discord",
|
||||
copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`,
|
||||
}
|
||||
|
||||
export interface UserDropdownContentProps {
|
||||
user: TypesGen.User
|
||||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
onPopoverClose: () => void
|
||||
onSignOut: () => void
|
||||
}
|
||||
|
||||
export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||
buildInfo,
|
||||
user,
|
||||
onPopoverClose,
|
||||
onSignOut,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const githubUrl = `https://github.com/coder/coder/issues/new?labels=needs+grooming&body=${encodeURIComponent(`Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})
|
||||
|
||||
<!--- Ask a question or leave feedback! -->`)}`
|
||||
const discordUrl = `https://coder.com/chat?utm_source=coder&utm_medium=coder&utm_campaign=server-footer`
|
||||
|
||||
return (
|
||||
<div className={styles.userInfo}>
|
||||
@@ -57,6 +70,13 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||
</MenuItem>
|
||||
</Link>
|
||||
|
||||
<MenuItem className={styles.menuItem} onClick={onSignOut}>
|
||||
<ListItemIcon className={styles.icon}>
|
||||
<LogoutIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={Language.signOutLabel} />
|
||||
</MenuItem>
|
||||
|
||||
<Divider />
|
||||
|
||||
<a
|
||||
@@ -73,12 +93,48 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||
</MenuItem>
|
||||
</a>
|
||||
|
||||
<MenuItem className={styles.menuItem} onClick={onSignOut}>
|
||||
<ListItemIcon className={styles.icon}>
|
||||
<LogoutIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={Language.signOutLabel} />
|
||||
</MenuItem>
|
||||
<a
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
|
||||
<ListItemIcon className={styles.icon}>
|
||||
<BugIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={Language.bugLabel} />
|
||||
</MenuItem>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
|
||||
<ListItemIcon className={styles.icon}>
|
||||
<ChatIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary={Language.discordLabel} />
|
||||
</MenuItem>
|
||||
</a>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Tooltip title="Browse Source Code">
|
||||
<a
|
||||
className={styles.footerText}
|
||||
href={buildInfo?.external_url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
{buildInfo?.version} <LaunchIcon />
|
||||
</a>
|
||||
</Tooltip>
|
||||
|
||||
<div className={styles.footerText}>{Language.copyrightText}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -133,4 +189,18 @@ const useStyles = makeStyles((theme) => ({
|
||||
icon: {
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
footerText: {
|
||||
textDecoration: "none",
|
||||
color: theme.palette.text.secondary,
|
||||
marginTop: theme.spacing(1.5),
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
|
||||
"& svg": {
|
||||
width: 14,
|
||||
height: 14,
|
||||
marginLeft: theme.spacing(0.5),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
@@ -13,13 +13,11 @@ const GeneralSettingsPage: React.FC = () => {
|
||||
<Helmet>
|
||||
<title>{pageTitle("General Settings")}</title>
|
||||
</Helmet>
|
||||
|
||||
<Header
|
||||
title="General"
|
||||
description="Information about your Coder deployment."
|
||||
docsHref="https://coder.com/docs/coder-oss/latest/admin/configure"
|
||||
/>
|
||||
|
||||
<OptionsTable
|
||||
options={{
|
||||
access_url: deploymentConfig.access_url,
|
||||
|
||||
Reference in New Issue
Block a user