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:
Kyle Carberry
2023-01-04 12:36:25 -06:00
committed by GitHub
co-authored by Asher
parent 925b29836c
commit a36cd0bd7b
12 changed files with 119 additions and 153 deletions
@@ -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)
})
})
-98
View File
@@ -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>
&nbsp;|&nbsp;
<Link
className={styles.link}
variant="caption"
target="_blank"
href={githubUrl}
>
<AssistantIcon className={styles.icon} /> {Language.reportBugLink}
</Link>
&nbsp;|&nbsp;
<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),
},
}))
+2
View File
@@ -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,