Skip to content

Commit a83a5e6

Browse files
committed
Finish rendering
1 parent 6540746 commit a83a5e6

File tree

12 files changed

+252
-0
lines changed

12 files changed

+252
-0
lines changed

codersdk/servicebanner.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package codersdk
2+
3+
type ServiceBanner struct {
4+
Enabled bool `json:"enabled"`
5+
Message string `json:"message,omitempty"`
6+
BackgroundColor string `json:"background_color,omitempty"`
7+
}

docs/admin/high-availability.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ env:
5757
5858
Then, increase the number of pods.
5959
60+
## Service Banners
61+
62+
Support for Service Banners is licensed alongside High Availability. You may
63+
enable a Service Banner by navigating to `Deployment -> Service Banner` in
64+
the Coder dashboard.
65+
6066
## Up next
6167

6268
- [Networking](../networking.md)

enterprise/coderd/coderd.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ func New(ctx context.Context, options *Options) (*API, error) {
127127
r.Get("/", api.workspaceQuota)
128128
})
129129
})
130+
r.Route("/service-banner", func(r chi.Router) {
131+
r.Use(
132+
apiKeyMiddleware,
133+
)
134+
r.Get("/", api.serviceBanner)
135+
})
130136
})
131137

132138
if len(options.SCIMAPIKey) != 0 {

enterprise/coderd/servicebanner.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package coderd
2+
3+
import (
4+
"net/http"
5+
6+
"github.com/coder/coder/coderd/httpapi"
7+
"github.com/coder/coder/codersdk"
8+
)
9+
10+
func (api *API) serviceBanner(rw http.ResponseWriter, r *http.Request) {
11+
httpapi.Write(r.Context(), rw, http.StatusOK, codersdk.ServiceBanner{
12+
Enabled: true,
13+
Message: "Testing!",
14+
BackgroundColor: "#FF00FF",
15+
})
16+
}

site/src/api/api.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -663,3 +663,8 @@ export const getFile = async (fileId: string): Promise<ArrayBuffer> => {
663663
})
664664
return response.data
665665
}
666+
667+
export const getServiceBanner = async (): Promise<TypesGen.ServiceBanner> => {
668+
const response = await axios.get(`/api/v2/service-banner`)
669+
return response.data
670+
}

site/src/api/typesGenerated.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,13 @@ export interface ServerSentEvent {
604604
readonly data: any
605605
}
606606

607+
// From codersdk/servicebanner.go
608+
export interface ServiceBanner {
609+
readonly enabled?: boolean
610+
readonly message?: string
611+
readonly background_color?: string
612+
}
613+
607614
// From codersdk/deploymentconfig.go
608615
export interface TLSConfig {
609616
readonly enable: DeploymentConfigField<boolean>

site/src/app.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import CssBaseline from "@material-ui/core/CssBaseline"
22
import ThemeProvider from "@material-ui/styles/ThemeProvider"
33
import { LicenseBanner } from "components/LicenseBanner/LicenseBanner"
4+
import { ServiceBanner } from "components/ServiceBanner/ServiceBanner"
45
import { FC } from "react"
56
import { HelmetProvider } from "react-helmet-async"
67
import { BrowserRouter as Router } from "react-router-dom"
@@ -19,6 +20,7 @@ export const App: FC = () => {
1920
<CssBaseline />
2021
<ErrorBoundary>
2122
<XServiceProvider>
23+
<ServiceBanner />
2224
<LicenseBanner />
2325
<AppRouter />
2426
<GlobalSnackbar />
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { useActor } from "@xstate/react"
2+
import { useContext, useEffect } from "react"
3+
import { XServiceContext } from "xServices/StateContext"
4+
import { ServiceBannerView } from "./ServiceBannerView"
5+
6+
export const ServiceBanner: React.FC = () => {
7+
const xServices = useContext(XServiceContext)
8+
const [serviceBannerState, serviceBannerSend] = useActor(
9+
xServices.serviceBannerXService,
10+
)
11+
12+
const { message, background_color, enabled } =
13+
serviceBannerState.context.serviceBanner
14+
15+
/** Gets license data on app mount because LicenseBanner is mounted in App */
16+
useEffect(() => {
17+
serviceBannerSend("GET_BANNER")
18+
}, [serviceBannerSend])
19+
20+
if (!enabled) {
21+
return null
22+
}
23+
24+
if (enabled && message !== undefined && background_color !== undefined) {
25+
return (
26+
<ServiceBannerView message={message} backgroundColor={background_color} />
27+
)
28+
} else {
29+
return null
30+
}
31+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { Story } from "@storybook/react"
2+
import { ServiceBannerView, ServiceBannerViewProps } from "./ServiceBannerView"
3+
4+
export default {
5+
title: "components/LicenseBannerView",
6+
component: ServiceBannerView,
7+
}
8+
9+
const Template: Story<ServiceBannerViewProps> = (args) => (
10+
<ServiceBannerView {...args} />
11+
)
12+
13+
export const GoodColor = Template.bind({})
14+
GoodColor.args = {
15+
message: "weeeee",
16+
backgroundColor: "#00FF00",
17+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { makeStyles } from "@material-ui/core/styles"
2+
import ReactMarkdown from "react-markdown"
3+
import { colors } from "theme/colors"
4+
5+
export interface ServiceBannerViewProps {
6+
message: string
7+
backgroundColor: string
8+
}
9+
10+
export const ServiceBannerView: React.FC<ServiceBannerViewProps> = ({
11+
message,
12+
backgroundColor,
13+
}) => {
14+
const styles = useStyles()
15+
const markdownElementsAllowed = [
16+
"text",
17+
"strong",
18+
"delete",
19+
"emphasis",
20+
"link",
21+
]
22+
return (
23+
<div
24+
className={`${styles.container}`}
25+
style={{ backgroundColor: backgroundColor }}
26+
>
27+
<div className={styles.centerContent}>
28+
<ReactMarkdown
29+
allowedElements={markdownElementsAllowed}
30+
unwrapDisallowed
31+
>
32+
{message}
33+
</ReactMarkdown>
34+
</div>
35+
</div>
36+
)
37+
}
38+
39+
const useStyles = makeStyles((theme) => ({
40+
container: {
41+
padding: theme.spacing(1.5),
42+
backgroundColor: theme.palette.warning.main,
43+
display: "flex",
44+
alignItems: "center",
45+
46+
"&.error": {
47+
backgroundColor: colors.red[12],
48+
},
49+
},
50+
flex: {
51+
display: "column",
52+
},
53+
centerContent: {
54+
marginRight: "auto",
55+
marginLeft: "auto",
56+
// Automatically pick high-contrast foreground text.
57+
// "difference" is the most correct way of implementing this
58+
// but "exclusion" looks prettier for most colors.
59+
mixBlendMode: "exclusion",
60+
},
61+
link: {
62+
color: "inherit",
63+
textDecoration: "none",
64+
fontWeight: "bold",
65+
},
66+
list: {
67+
padding: theme.spacing(1),
68+
margin: 0,
69+
},
70+
listItem: {
71+
margin: theme.spacing(0.5),
72+
},
73+
}))

site/src/xServices/StateContext.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import { updateCheckMachine } from "./updateCheck/updateCheckXService"
77
import { deploymentConfigMachine } from "./deploymentConfig/deploymentConfigMachine"
88
import { entitlementsMachine } from "./entitlements/entitlementsXService"
99
import { siteRolesMachine } from "./roles/siteRolesXService"
10+
import { serviceBannerMachine } from "./serviceBanner/serviceBannerXService"
1011

1112
interface XServiceContextType {
1213
authXService: ActorRefFrom<typeof authMachine>
1314
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
1415
entitlementsXService: ActorRefFrom<typeof entitlementsMachine>
16+
serviceBannerXService: ActorRefFrom<typeof serviceBannerMachine>
1517
siteRolesXService: ActorRefFrom<typeof siteRolesMachine>
1618
// Since the info here is used by multiple deployment settings page and we don't want to refetch them every time
1719
deploymentConfigXService: ActorRefFrom<typeof deploymentConfigMachine>
@@ -35,6 +37,7 @@ export const XServiceProvider: FC<{ children: ReactNode }> = ({ children }) => {
3537
authXService: useInterpret(authMachine),
3638
buildInfoXService: useInterpret(buildInfoMachine),
3739
entitlementsXService: useInterpret(entitlementsMachine),
40+
serviceBannerXService: useInterpret(serviceBannerMachine),
3841
siteRolesXService: useInterpret(siteRolesMachine),
3942
deploymentConfigXService: useInterpret(deploymentConfigMachine),
4043
updateCheckXService: useInterpret(updateCheckMachine),
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { assign, createMachine } from "xstate"
2+
import * as API from "../../api/api"
3+
import { ServiceBanner } from "../../api/typesGenerated"
4+
5+
export const Language = {
6+
getServiceBannerError: "Error getting service banner.",
7+
}
8+
9+
export type ServiceBannerContext = {
10+
serviceBanner: ServiceBanner
11+
getServiceBannerError?: Error | unknown
12+
}
13+
14+
export type ServiceBannerEvent = {
15+
type: "GET_BANNER"
16+
}
17+
18+
const emptyBanner = {
19+
enabled: false,
20+
}
21+
22+
export const serviceBannerMachine = createMachine(
23+
{
24+
id: "serviceBannerMachine",
25+
predictableActionArguments: true,
26+
tsTypes: {} as import("./serviceBannerXService.typegen").Typegen0,
27+
schema: {
28+
context: {} as ServiceBannerContext,
29+
events: {} as ServiceBannerEvent,
30+
services: {
31+
getServiceBanner: {
32+
data: {} as ServiceBanner,
33+
},
34+
},
35+
},
36+
context: {
37+
serviceBanner: emptyBanner,
38+
},
39+
initial: "idle",
40+
states: {
41+
idle: {
42+
on: {
43+
GET_BANNER: "gettingBanner",
44+
},
45+
},
46+
gettingBanner: {
47+
entry: "clearGetBannerError",
48+
invoke: {
49+
id: "getBanner",
50+
src: "getBanner",
51+
onDone: {
52+
target: "idle",
53+
actions: ["assignBanner"],
54+
},
55+
onError: {
56+
target: "idle",
57+
actions: ["assignGetBannerError"],
58+
},
59+
},
60+
},
61+
},
62+
},
63+
{
64+
actions: {
65+
assignBanner: assign({
66+
serviceBanner: (_, event) => event.data as ServiceBanner,
67+
}),
68+
assignGetBannerError: assign({
69+
getServiceBannerError: (_, event) => event.data,
70+
}),
71+
clearGetBannerError: assign({
72+
getServiceBannerError: (_) => undefined,
73+
}),
74+
},
75+
services: {
76+
getBanner: API.getServiceBanner,
77+
},
78+
},
79+
)

0 commit comments

Comments
 (0)