Skip to content

Commit 49de44c

Browse files
presleypKira-Pilot
andauthoredAug 23, 2022
feat: Add LicenseBanner (#3568)
* Extract reusable Pill component * Make icon optional * Get pills in place * Rough styling * Extract Expander component * Fix alignment * Put it in action - type error * Hide banner by default * Use generated type * Move PaletteIndex type * Tweak colors * Format, another color tweak * Add stories * Add tests * Update site/src/components/Pill/Pill.tsx Co-authored-by: Kira Pilot <kira@coder.com> * Update site/src/components/Pill/Pill.tsx Co-authored-by: Kira Pilot <kira@coder.com> * Comments * Remove empty story, improve empty test * Lint Co-authored-by: Kira Pilot <kira@coder.com>
1 parent f7ccfa2 commit 49de44c

File tree

18 files changed

+509
-97
lines changed

18 files changed

+509
-97
lines changed
 

‎site/src/api/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,3 +378,8 @@ export const putWorkspaceExtension = async (
378378
): Promise<void> => {
379379
await axios.put(`/api/v2/workspaces/${workspaceId}/extend`, { deadline: newDeadline })
380380
}
381+
382+
export const getEntitlements = async (): Promise<TypesGen.Entitlements> => {
383+
const response = await axios.get("/api/v2/entitlements")
384+
return response.data
385+
}

‎site/src/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import CssBaseline from "@material-ui/core/CssBaseline"
22
import ThemeProvider from "@material-ui/styles/ThemeProvider"
3+
import { LicenseBanner } from "components/LicenseBanner/LicenseBanner"
34
import { FC } from "react"
45
import { HelmetProvider } from "react-helmet-async"
56
import { BrowserRouter as Router } from "react-router-dom"
@@ -18,6 +19,7 @@ export const App: FC = () => {
1819
<CssBaseline />
1920
<ErrorBoundary>
2021
<XServiceProvider>
22+
<LicenseBanner />
2123
<AppRouter />
2224
<GlobalSnackbar />
2325
</XServiceProvider>

‎site/src/components/DropdownArrows/DropdownArrows.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@ import KeyboardArrowDown from "@material-ui/icons/KeyboardArrowDown"
33
import KeyboardArrowUp from "@material-ui/icons/KeyboardArrowUp"
44
import { FC } from "react"
55

6-
const useStyles = makeStyles((theme: Theme) => ({
6+
const useStyles = makeStyles<Theme, ArrowProps>((theme: Theme) => ({
77
arrowIcon: {
88
color: fade(theme.palette.primary.contrastText, 0.7),
9-
marginLeft: theme.spacing(1),
9+
marginLeft: ({ margin }) => (margin ? theme.spacing(1) : 0),
1010
width: 16,
1111
height: 16,
1212
},
@@ -15,12 +15,16 @@ const useStyles = makeStyles((theme: Theme) => ({
1515
},
1616
}))
1717

18-
export const OpenDropdown: FC = () => {
19-
const styles = useStyles()
18+
interface ArrowProps {
19+
margin?: boolean
20+
}
21+
22+
export const OpenDropdown: FC<ArrowProps> = ({ margin = true }) => {
23+
const styles = useStyles({ margin })
2024
return <KeyboardArrowDown className={styles.arrowIcon} />
2125
}
2226

23-
export const CloseDropdown: FC = () => {
24-
const styles = useStyles()
27+
export const CloseDropdown: FC<ArrowProps> = ({ margin = true }) => {
28+
const styles = useStyles({ margin })
2529
return <KeyboardArrowUp className={`${styles.arrowIcon} ${styles.arrowIconUp}`} />
2630
}

‎site/src/components/ErrorSummary/ErrorSummary.tsx

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import Button from "@material-ui/core/Button"
22
import Collapse from "@material-ui/core/Collapse"
33
import IconButton from "@material-ui/core/IconButton"
4-
import Link from "@material-ui/core/Link"
54
import { darken, lighten, makeStyles, Theme } from "@material-ui/core/styles"
65
import CloseIcon from "@material-ui/icons/Close"
76
import RefreshIcon from "@material-ui/icons/Refresh"
87
import { ApiError, getErrorDetail, getErrorMessage } from "api/errors"
8+
import { Expander } from "components/Expander/Expander"
99
import { Stack } from "components/Stack/Stack"
1010
import { FC, useState } from "react"
1111

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

3737
const styles = useStyles({ showDetails })
3838

39-
const toggleShowDetails = () => {
40-
setShowDetails(!showDetails)
41-
}
42-
4339
const closeError = () => {
4440
setOpen(false)
4541
}
@@ -51,19 +47,10 @@ export const ErrorSummary: FC<React.PropsWithChildren<ErrorSummaryProps>> = ({
5147
return (
5248
<Stack className={styles.root}>
5349
<Stack direction="row" alignItems="center" className={styles.messageBox}>
54-
<div>
50+
<Stack direction="row" spacing={0}>
5551
<span className={styles.errorMessage}>{message}</span>
56-
{!!detail && (
57-
<Link
58-
aria-expanded={showDetails}
59-
onClick={toggleShowDetails}
60-
className={styles.detailsLink}
61-
tabIndex={0}
62-
>
63-
{showDetails ? Language.lessDetails : Language.moreDetails}
64-
</Link>
65-
)}
66-
</div>
52+
{!!detail && <Expander expanded={showDetails} setExpanded={setShowDetails} />}
53+
</Stack>
6754
{dismissible && (
6855
<IconButton onClick={closeError} className={styles.iconButton}>
6956
<CloseIcon className={styles.closeIcon} />
@@ -101,6 +88,9 @@ const useStyles = makeStyles<Theme, StyleProps>((theme) => ({
10188
borderRadius: theme.shape.borderRadius,
10289
gap: 0,
10390
},
91+
flex: {
92+
display: "flex",
93+
},
10494
messageBox: {
10595
justifyContent: "space-between",
10696
},
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Story } from "@storybook/react"
2+
import { Expander, ExpanderProps } from "./Expander"
3+
4+
export default {
5+
title: "components/Expander",
6+
component: Expander,
7+
argTypes: {
8+
setExpanded: { action: "setExpanded" },
9+
},
10+
}
11+
12+
const Template: Story<ExpanderProps> = (args) => <Expander {...args} />
13+
14+
export const Expanded = Template.bind({})
15+
Expanded.args = {
16+
expanded: true,
17+
}
18+
19+
export const Collapsed = Template.bind({})
20+
Collapsed.args = {
21+
expanded: false,
22+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import Link from "@material-ui/core/Link"
2+
import makeStyles from "@material-ui/core/styles/makeStyles"
3+
import { CloseDropdown, OpenDropdown } from "components/DropdownArrows/DropdownArrows"
4+
5+
const Language = {
6+
expand: "More",
7+
collapse: "Less",
8+
}
9+
10+
export interface ExpanderProps {
11+
expanded: boolean
12+
setExpanded: (val: boolean) => void
13+
}
14+
15+
export const Expander: React.FC<ExpanderProps> = ({ expanded, setExpanded }) => {
16+
const toggleExpanded = () => setExpanded(!expanded)
17+
const styles = useStyles()
18+
return (
19+
<Link aria-expanded={expanded} onClick={toggleExpanded} className={styles.expandLink}>
20+
{expanded ? (
21+
<span className={styles.text}>
22+
{Language.collapse}
23+
<CloseDropdown margin={false} />{" "}
24+
</span>
25+
) : (
26+
<span className={styles.text}>
27+
{Language.expand}
28+
<OpenDropdown margin={false} />
29+
</span>
30+
)}
31+
</Link>
32+
)
33+
}
34+
35+
const useStyles = makeStyles((theme) => ({
36+
expandLink: {
37+
cursor: "pointer",
38+
color: theme.palette.text.primary,
39+
display: "flex",
40+
},
41+
text: {
42+
display: "flex",
43+
alignItems: "center",
44+
},
45+
}))
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { screen } from "@testing-library/react"
2+
import { rest } from "msw"
3+
import { MockEntitlementsWithWarnings } from "testHelpers/entities"
4+
import { render } from "testHelpers/renderHelpers"
5+
import { server } from "testHelpers/server"
6+
import { LicenseBanner } from "./LicenseBanner"
7+
import { Language } from "./LicenseBannerView"
8+
9+
describe("LicenseBanner", () => {
10+
it("does not show when there are no warnings", async () => {
11+
render(<LicenseBanner />)
12+
const bannerPillSingular = await screen.queryByText(Language.licenseIssue)
13+
const bannerPillPlural = await screen.queryByText(Language.licenseIssues(2))
14+
expect(bannerPillSingular).toBe(null)
15+
expect(bannerPillPlural).toBe(null)
16+
})
17+
it("shows when there are warnings", async () => {
18+
server.use(
19+
rest.get("/api/v2/entitlements", (req, res, ctx) => {
20+
return res(ctx.status(200), ctx.json(MockEntitlementsWithWarnings))
21+
}),
22+
)
23+
render(<LicenseBanner />)
24+
const bannerPill = await screen.findByText(Language.licenseIssues(2))
25+
expect(bannerPill).toBeDefined()
26+
})
27+
})
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { useActor } from "@xstate/react"
2+
import { useContext, useEffect } from "react"
3+
import { XServiceContext } from "xServices/StateContext"
4+
import { LicenseBannerView } from "./LicenseBannerView"
5+
6+
export const LicenseBanner: React.FC = () => {
7+
const xServices = useContext(XServiceContext)
8+
const [entitlementsState, entitlementsSend] = useActor(xServices.entitlementsXService)
9+
const { warnings } = entitlementsState.context.entitlements
10+
11+
/** Gets license data on app mount because LicenseBanner is mounted in App */
12+
useEffect(() => {
13+
entitlementsSend("GET_ENTITLEMENTS")
14+
}, [entitlementsSend])
15+
16+
if (warnings.length) {
17+
return <LicenseBannerView warnings={warnings} />
18+
} else {
19+
return null
20+
}
21+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import { Story } from "@storybook/react"
2+
import { LicenseBannerView, LicenseBannerViewProps } from "./LicenseBannerView"
3+
4+
export default {
5+
title: "components/LicenseBannerView",
6+
component: LicenseBannerView,
7+
}
8+
9+
const Template: Story<LicenseBannerViewProps> = (args) => <LicenseBannerView {...args} />
10+
11+
export const OneWarning = Template.bind({})
12+
OneWarning.args = {
13+
warnings: ["You have exceeded the number of seats in your license."],
14+
}
15+
16+
export const TwoWarnings = Template.bind({})
17+
TwoWarnings.args = {
18+
warnings: [
19+
"You have exceeded the number of seats in your license.",
20+
"You are flying too close to the sun.",
21+
],
22+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import Collapse from "@material-ui/core/Collapse"
2+
import { makeStyles } from "@material-ui/core/styles"
3+
import { Expander } from "components/Expander/Expander"
4+
import { Pill } from "components/Pill/Pill"
5+
import { useState } from "react"
6+
7+
export const Language = {
8+
licenseIssue: "License Issue",
9+
licenseIssues: (num: number): string => `${num} License Issues`,
10+
upgrade: "Contact us to upgrade your license.",
11+
exceeded: "It looks like you've exceeded some limits of your license.",
12+
lessDetails: "Less",
13+
moreDetails: "More",
14+
}
15+
16+
export interface LicenseBannerViewProps {
17+
warnings: string[]
18+
}
19+
20+
export const LicenseBannerView: React.FC<LicenseBannerViewProps> = ({ warnings }) => {
21+
const styles = useStyles()
22+
const [showDetails, setShowDetails] = useState(false)
23+
if (warnings.length === 1) {
24+
return (
25+
<div className={styles.container}>
26+
<Pill text={Language.licenseIssue} type="warning" lightBorder />
27+
<span className={styles.text}>{warnings[0]}</span>
28+
&nbsp;
29+
<a href="mailto:sales@coder.com" className={styles.link}>
30+
{Language.upgrade}
31+
</a>
32+
</div>
33+
)
34+
} else {
35+
return (
36+
<div className={styles.container}>
37+
<div className={styles.flex}>
38+
<div className={styles.leftContent}>
39+
<Pill text={Language.licenseIssues(warnings.length)} type="warning" lightBorder />
40+
<span className={styles.text}>{Language.exceeded}</span>
41+
&nbsp;
42+
<a href="mailto:sales@coder.com" className={styles.link}>
43+
{Language.upgrade}
44+
</a>
45+
</div>
46+
<Expander expanded={showDetails} setExpanded={setShowDetails} />
47+
</div>
48+
<Collapse in={showDetails}>
49+
<ul className={styles.list}>
50+
{warnings.map((warning) => (
51+
<li className={styles.listItem} key={`${warning}`}>
52+
{warning}
53+
</li>
54+
))}
55+
</ul>
56+
</Collapse>
57+
</div>
58+
)
59+
}
60+
}
61+
62+
const useStyles = makeStyles((theme) => ({
63+
container: {
64+
padding: theme.spacing(1.5),
65+
backgroundColor: theme.palette.warning.main,
66+
},
67+
flex: {
68+
display: "flex",
69+
},
70+
leftContent: {
71+
marginRight: theme.spacing(1),
72+
},
73+
text: {
74+
marginLeft: theme.spacing(1),
75+
},
76+
link: {
77+
color: "inherit",
78+
textDecoration: "none",
79+
fontWeight: "bold",
80+
},
81+
list: {
82+
margin: theme.spacing(1.5),
83+
},
84+
listItem: {
85+
margin: theme.spacing(1),
86+
},
87+
}))

0 commit comments

Comments
 (0)