Skip to content

feat: Add LicenseBanner #3568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 22 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,3 +378,8 @@ export const putWorkspaceExtension = async (
): Promise<void> => {
await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { deadline: newDeadline })
}

export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
const response = await axios.get("/api/v2/entitlements")
return response.data
}
2 changes: 2 additions & 0 deletions site/src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import CssBaseline from "@material-ui/core/CssBaseline"
import ThemeProvider from "@material-ui/styles/ThemeProvider"
import { LicenseBanner } from "components/LicenseBanner/LicenseBanner"
import { FC } from "react"
import { HelmetProvider } from "react-helmet-async"
import { BrowserRouter as Router } from "react-router-dom"
Expand All @@ -18,6 +19,7 @@ export const App: FC = () => {
<CssBaseline />
<ErrorBoundary>
<XServiceProvider>
<LicenseBanner />
<AppRouter />
<GlobalSnackbar />
</XServiceProvider>
Expand Down
16 changes: 10 additions & 6 deletions site/src/components/DropdownArrows/DropdownArrows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ import KeyboardArrowDown from "@material-ui/icons/KeyboardArrowDown"
import KeyboardArrowUp from "@material-ui/icons/KeyboardArrowUp"
import { FC } from "react"

const useStyles = makeStyles((theme: Theme) => ({
const useStyles = makeStyles<Theme, ArrowProps>((theme: Theme) => ({
arrowIcon: {
color: fade(theme.palette.primary.contrastText, 0.7),
marginLeft: theme.spacing(1),
marginLeft: ({ margin }) => (margin ? theme.spacing(1) : 0),
width: 16,
height: 16,
},
Expand All @@ -15,12 +15,16 @@ const useStyles = makeStyles((theme: Theme) => ({
},
}))

export const OpenDropdown: FC = () => {
const styles = useStyles()
interface ArrowProps {
margin?: boolean
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could share this typing with the makeStyles block above: makeStyles<Theme, ArrowProps>

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also wonder if, instead of a margin prop, we add a style prop that we spread in the children. Makes things a bit more flexible, but just a suggestion!

Copy link
Contributor Author

@presleyp presleyp Aug 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to consider keeping link text blue for accessibility reasons.

(This comment ended up in the wrong place, sorry for the confusion!)

@Kira-Pilot Yeah, so I changed this because I thought the color combination was bad and it was also going to pose a contrast issue, but I have been wondering if it's obvious enough. I think the chevron and placement helps. But I also think I'm having trouble making it blue or underlined because it's a not a navigation link. It's really a button in functionality. What do you think is the best way to style that? I guess Zenhub styles "Show 4 more" on this page like a link (bold and blue).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to wait on a style prop here because I think this component will be revisited in future color work and it'll be easier to tell then what to do.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@presleyp That makes sense! I agree with your logic and I don't think it's something we need to change right now. Let's keep it back of mind and look out for link inspo in the future.


export const OpenDropdown: FC<ArrowProps> = ({ margin = true }) => {
const styles = useStyles({ margin })
return <KeyboardArrowDown className={styles.arrowIcon} />
}

export const CloseDropdown: FC = () => {
const styles = useStyles()
export const CloseDropdown: FC<ArrowProps> = ({ margin = true }) => {
const styles = useStyles({ margin })
return <KeyboardArrowUp className={`${styles.arrowIcon} ${styles.arrowIconUp}`} />
}
24 changes: 7 additions & 17 deletions site/src/components/ErrorSummary/ErrorSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import Button from "@material-ui/core/Button"
import Collapse from "@material-ui/core/Collapse"
import IconButton from "@material-ui/core/IconButton"
import Link from "@material-ui/core/Link"
import { darken, lighten, makeStyles, Theme } from "@material-ui/core/styles"
import CloseIcon from "@material-ui/icons/Close"
import RefreshIcon from "@material-ui/icons/Refresh"
import { ApiError, getErrorDetail, getErrorMessage } from "api/errors"
import { Expander } from "components/Expander/Expander"
import { Stack } from "components/Stack/Stack"
import { FC, useState } from "react"

Expand Down Expand Up @@ -36,10 +36,6 @@ export const ErrorSummary: FC<React.PropsWithChildren<ErrorSummaryProps>> = ({

const styles = useStyles({ showDetails })

const toggleShowDetails = () => {
setShowDetails(!showDetails)
}

const closeError = () => {
setOpen(false)
}
Expand All @@ -51,19 +47,10 @@ export const ErrorSummary: FC<React.PropsWithChildren<ErrorSummaryProps>> = ({
return (
<Stack className={styles.root}>
<Stack direction="row" alignItems="center" className={styles.messageBox}>
<div>
<Stack direction="row" spacing={0}>
<span className={styles.errorMessage}>{message}</span>
{!!detail && (
<Link
aria-expanded={showDetails}
onClick={toggleShowDetails}
className={styles.detailsLink}
tabIndex={0}
>
{showDetails ? Language.lessDetails : Language.moreDetails}
</Link>
)}
</div>
{!!detail && <Expander expanded={showDetails} setExpanded={setShowDetails} />}
</Stack>
{dismissible && (
<IconButton onClick={closeError} className={styles.iconButton}>
<CloseIcon className={styles.closeIcon} />
Expand Down Expand Up @@ -101,6 +88,9 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
borderRadius: theme.shape.borderRadius,
gap: 0,
},
flex: {
display: "flex",
},
messageBox: {
justifyContent: "space-between",
},
Expand Down
22 changes: 22 additions & 0 deletions site/src/components/Expander/Expander.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Story } from "@storybook/react"
import { Expander, ExpanderProps } from "./Expander"

export default {
title: "components/Expander",
component: Expander,
argTypes: {
setExpanded: { action: "setExpanded" },
},
}

const Template: Story<ExpanderProps> = (args) => <Expander {...args} />

export const Expanded = Template.bind({})
Expanded.args = {
expanded: true,
}

export const Collapsed = Template.bind({})
Collapsed.args = {
expanded: false,
}
45 changes: 45 additions & 0 deletions site/src/components/Expander/Expander.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import Link from "@material-ui/core/Link"
import makeStyles from "@material-ui/core/styles/makeStyles"
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"

const Language = {
expand: "More",
collapse: "Less",
}

export interface ExpanderProps {
expanded: boolean
setExpanded: (val: boolean) => void
}

export const Expander: React.FC<ExpanderProps> = ({ expanded, setExpanded }) => {
const toggleExpanded = () => setExpanded(!expanded)
const styles = useStyles()
return (
<Link aria-expanded={expanded} onClick={toggleExpanded} className={styles.expandLink}>
{expanded ? (
<span className={styles.text}>
{Language.collapse}
<CloseDropdown margin={false} />{" "}
</span>
) : (
<span className={styles.text}>
{Language.expand}
<OpenDropdown margin={false} />
</span>
)}
</Link>
)
}

const useStyles = makeStyles((theme) => ({
expandLink: {
cursor: "pointer",
color: theme.palette.text.primary,
display: "flex",
},
text: {
display: "flex",
alignItems: "center",
},
}))
27 changes: 27 additions & 0 deletions site/src/components/LicenseBanner/LicenseBanner.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { screen } from "@testing-library/react"
import { rest } from "msw"
import { MockEntitlementsWithWarnings } from "testHelpers/entities"
import { render } from "testHelpers/renderHelpers"
import { server } from "testHelpers/server"
import { LicenseBanner } from "./LicenseBanner"
import { Language } from "./LicenseBannerView"

describe("LicenseBanner", () => {
it("does not show when there are no warnings", async () => {
render(<LicenseBanner />)
const bannerPillSingular = await screen.queryByText(Language.licenseIssue)
const bannerPillPlural = await screen.queryByText(Language.licenseIssues(2))
expect(bannerPillSingular).toBe(null)
expect(bannerPillPlural).toBe(null)
})
it("shows when there are warnings", async () => {
server.use(
rest.get("/api/v2/entitlements", (req, res, ctx) => {
return res(ctx.status(200), ctx.json(MockEntitlementsWithWarnings))
}),
)
render(<LicenseBanner />)
const bannerPill = await screen.findByText(Language.licenseIssues(2))
expect(bannerPill).toBeDefined()
})
})
21 changes: 21 additions & 0 deletions site/src/components/LicenseBanner/LicenseBanner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { useActor } from "@xstate/react"
import { useContext, useEffect } from "react"
import { XServiceContext } from "xServices/StateContext"
import { LicenseBannerView } from "./LicenseBannerView"

export const LicenseBanner: React.FC = () => {
const xServices = useContext(XServiceContext)
const [entitlementsState, entitlementsSend] = useActor(xServices.entitlementsXService)
const { warnings } = entitlementsState.context.entitlements

/** Gets license data on app mount because LicenseBanner is mounted in App */
useEffect(() => {
entitlementsSend("GET_ENTITLEMENTS")
}, [entitlementsSend])

if (warnings.length) {
return <LicenseBannerView warnings={warnings} />
} else {
return null
}
}
22 changes: 22 additions & 0 deletions site/src/components/LicenseBanner/LicenseBannerView.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Story } from "@storybook/react"
import { LicenseBannerView, LicenseBannerViewProps } from "./LicenseBannerView"

export default {
title: "components/LicenseBannerView",
component: LicenseBannerView,
}

const Template: Story<LicenseBannerViewProps> = (args) => <LicenseBannerView {...args} />

export const OneWarning = Template.bind({})
OneWarning.args = {
warnings: ["You have exceeded the number of seats in your license."],
}

export const TwoWarnings = Template.bind({})
TwoWarnings.args = {
warnings: [
"You have exceeded the number of seats in your license.",
"You are flying too close to the sun.",
],
}
87 changes: 87 additions & 0 deletions site/src/components/LicenseBanner/LicenseBannerView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import Collapse from "@material-ui/core/Collapse"
import { makeStyles } from "@material-ui/core/styles"
import { Expander } from "components/Expander/Expander"
import { Pill } from "components/Pill/Pill"
import { useState } from "react"

export const Language = {
licenseIssue: "License Issue",
licenseIssues: (num: number): string => `${num} License Issues`,
upgrade: "Contact us to upgrade your license.",
exceeded: "It looks like you've exceeded some limits of your license.",
lessDetails: "Less",
moreDetails: "More",
}

export interface LicenseBannerViewProps {
warnings: string[]
}

export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({ warnings }) => {
const styles = useStyles()
const [showDetails, setShowDetails] = useState(false)
if (warnings.length === 1) {
return (
<div className={styles.container}>
<Pill text={Language.licenseIssue} type="warning" lightBorder />
<span className={styles.text}>{warnings[0]}</span>
&nbsp;
<a href="mailto:sales@coder.com" className={styles.link}>
{Language.upgrade}
</a>
</div>
)
} else {
return (
<div className={styles.container}>
<div className={styles.flex}>
<div className={styles.leftContent}>
<Pill text={Language.licenseIssues(warnings.length)} type="warning" lightBorder />
<span className={styles.text}>{Language.exceeded}</span>
&nbsp;
<a href="mailto:sales@coder.com" className={styles.link}>
{Language.upgrade}
</a>
</div>
<Expander expanded={showDetails} setExpanded={setShowDetails} />
</div>
<Collapse in={showDetails}>
<ul className={styles.list}>
{warnings.map((warning) => (
<li className={styles.listItem} key={`${warning}`}>
{warning}
</li>
))}
</ul>
</Collapse>
</div>
)
}
}

const useStyles = makeStyles((theme) => ({
container: {
padding: theme.spacing(1.5),
backgroundColor: theme.palette.warning.main,
},
flex: {
display: "flex",
},
leftContent: {
marginRight: theme.spacing(1),
},
text: {
marginLeft: theme.spacing(1),
},
link: {
color: "inherit",
textDecoration: "none",
fontWeight: "bold",
},
list: {
margin: theme.spacing(1.5),
},
listItem: {
margin: theme.spacing(1),
},
}))
Loading