Skip to content

Commit 49de44c

Browse files
presleypKira-Pilot
andauthored
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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy