Skip to content

Commit 1859579

Browse files
authored
feat: add version to footer (#882)
* Add endpoint for getting build info * Add build info XService * Add version with link to page footer Partially addresses #376. * Lift buildinfo package
1 parent 2e5859f commit 1859579

File tree

14 files changed

+204
-17
lines changed

14 files changed

+204
-17
lines changed
File renamed without changes.

cli/buildinfo/buildinfo_test.go renamed to buildinfo/buildinfo_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"github.com/stretchr/testify/require"
77
"golang.org/x/mod/semver"
88

9-
"github.com/coder/coder/cli/buildinfo"
9+
"github.com/coder/coder/buildinfo"
1010
)
1111

1212
func TestBuildInfo(t *testing.T) {

cli/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import (
1111
"github.com/mattn/go-isatty"
1212
"github.com/spf13/cobra"
1313

14-
"github.com/coder/coder/cli/buildinfo"
14+
"github.com/coder/coder/buildinfo"
1515
"github.com/coder/coder/cli/cliui"
1616
"github.com/coder/coder/cli/config"
1717
"github.com/coder/coder/codersdk"

coderd/coderd.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,19 @@ import (
77
"time"
88

99
"github.com/go-chi/chi/v5"
10+
"github.com/go-chi/render"
1011
"google.golang.org/api/idtoken"
1112

1213
chitrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go-chi/chi.v5"
1314

1415
"cdr.dev/slog"
16+
"github.com/coder/coder/buildinfo"
1517
"github.com/coder/coder/coderd/awsidentity"
1618
"github.com/coder/coder/coderd/database"
1719
"github.com/coder/coder/coderd/gitsshkey"
1820
"github.com/coder/coder/coderd/httpapi"
1921
"github.com/coder/coder/coderd/httpmw"
22+
"github.com/coder/coder/codersdk"
2023
"github.com/coder/coder/site"
2124
)
2225

@@ -59,6 +62,15 @@ func New(options *Options) (http.Handler, func()) {
5962
Message: "👋",
6063
})
6164
})
65+
r.Route("/buildinfo", func(r chi.Router) {
66+
r.Get("/", func(rw http.ResponseWriter, r *http.Request) {
67+
render.Status(r, http.StatusOK)
68+
render.JSON(rw, r, codersdk.BuildInfoResponse{
69+
ExternalURL: buildinfo.ExternalURL(),
70+
Version: buildinfo.Version(),
71+
})
72+
})
73+
})
6274
r.Route("/files", func(r chi.Router) {
6375
r.Use(
6476
httpmw.ExtractAPIKey(options.Database, nil),

coderd/coderd_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,26 @@
11
package coderd_test
22

33
import (
4+
"context"
45
"testing"
56

67
"go.uber.org/goleak"
8+
9+
"github.com/stretchr/testify/require"
10+
11+
"github.com/coder/coder/buildinfo"
12+
"github.com/coder/coder/coderd/coderdtest"
713
)
814

915
func TestMain(m *testing.M) {
1016
goleak.VerifyTestMain(m)
1117
}
18+
19+
func TestBuildInfo(t *testing.T) {
20+
t.Parallel()
21+
client := coderdtest.New(t, nil)
22+
buildInfo, err := client.BuildInfo(context.Background())
23+
require.NoError(t, err)
24+
require.Equal(t, buildinfo.ExternalURL(), buildInfo.ExternalURL, "external URL")
25+
require.Equal(t, buildinfo.Version(), buildInfo.Version, "version")
26+
}

codersdk/buildinfo.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package codersdk
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"net/http"
7+
)
8+
9+
// BuildInfoResponse contains build information for this instance of Coder.
10+
type BuildInfoResponse struct {
11+
// ExternalURL is a URL referencing the current Coder version. For production
12+
// builds, this will link directly to a release. For development builds, this
13+
// will link to a commit.
14+
ExternalURL string `json:"external_url"`
15+
// Version returns the semantic version of the build.
16+
Version string `json:"version"`
17+
}
18+
19+
// BuildInfo returns build information for this instance of Coder.
20+
func (c *Client) BuildInfo(ctx context.Context) (BuildInfoResponse, error) {
21+
res, err := c.request(ctx, http.MethodGet, "/api/v2/buildinfo", nil)
22+
if err != nil {
23+
return BuildInfoResponse{}, err
24+
}
25+
defer res.Body.Close()
26+
27+
if res.StatusCode != http.StatusOK {
28+
return BuildInfoResponse{}, readBodyAsError(res)
29+
}
30+
31+
var buildInfo BuildInfoResponse
32+
return buildInfo, json.NewDecoder(res.Body).Decode(&buildInfo)
33+
}

site/src/api/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,8 @@ export const getApiKey = async (): Promise<Types.APIKeyResponse> => {
6868
const response = await axios.post<Types.APIKeyResponse>("/api/v2/users/me/keys")
6969
return response.data
7070
}
71+
72+
export const getBuildInfo = async (): Promise<Types.BuildInfoResponse> => {
73+
const response = await axios.get("/api/v2/buildinfo")
74+
return response.data
75+
}

site/src/api/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
/**
2+
* `BuildInfoResponse` must be kept in sync with the go struct in buildinfo.go.
3+
*/
4+
export interface BuildInfoResponse {
5+
external_url: string
6+
version: string
7+
}
8+
19
export interface LoginResponse {
210
session_token: string
311
}
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { screen } from "@testing-library/react"
22
import React from "react"
3-
import { render } from "../../test_helpers"
4-
import { Footer } from "./Footer"
3+
import { MockBuildInfo, render } from "../../test_helpers"
4+
import { Footer, Language } from "./Footer"
55

66
describe("Footer", () => {
77
it("renders content", async () => {
@@ -10,5 +10,6 @@ describe("Footer", () => {
1010

1111
// Then
1212
await screen.findByText("Copyright", { exact: false })
13+
await screen.findByText(Language.buildInfoText(MockBuildInfo))
1314
})
1415
})

site/src/components/Page/Footer.tsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
import Link from "@material-ui/core/Link"
12
import { makeStyles } from "@material-ui/core/styles"
23
import Typography from "@material-ui/core/Typography"
3-
import React from "react"
4+
import { useActor } from "@xstate/react"
5+
import React, { useContext } from "react"
6+
import { BuildInfoResponse } from "../../api/types"
7+
import { XServiceContext } from "../../xServices/StateContext"
8+
9+
export const Language = {
10+
buildInfoText: (buildInfo: BuildInfoResponse): string => {
11+
return `Coder ${buildInfo.version}`
12+
},
13+
}
414

515
export const Footer: React.FC = ({ children }) => {
616
const styles = useFooterStyles()
17+
const xServices = useContext(XServiceContext)
18+
const [buildInfoState] = useActor(xServices.buildInfoXService)
719

820
return (
921
<div className={styles.root}>
@@ -13,11 +25,13 @@ export const Footer: React.FC = ({ children }) => {
1325
{`Copyright \u00a9 ${new Date().getFullYear()} Coder Technologies, Inc. All rights reserved.`}
1426
</Typography>
1527
</div>
16-
<div className={styles.version}>
17-
<Typography color="textSecondary" variant="caption">
18-
v2 0.0.0-prototype
19-
</Typography>
20-
</div>
28+
{buildInfoState.context.buildInfo && (
29+
<div className={styles.buildInfo}>
30+
<Link variant="caption" href={buildInfoState.context.buildInfo.external_url}>
31+
{Language.buildInfoText(buildInfoState.context.buildInfo)}
32+
</Link>
33+
</div>
34+
)}
2135
</div>
2236
)
2337
}
@@ -29,11 +43,9 @@ const useFooterStyles = makeStyles((theme) => ({
2943
flex: "0",
3044
},
3145
copyRight: {
32-
backgroundColor: theme.palette.background.default,
3346
margin: theme.spacing(0.25),
3447
},
35-
version: {
36-
backgroundColor: theme.palette.background.default,
48+
buildInfo: {
3749
margin: theme.spacing(0.25),
3850
},
3951
}))

site/src/test_helpers/entities.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,22 @@
1-
import { Organization, Provisioner, Template, UserAgent, UserResponse, Workspace } from "../api/types"
1+
import {
2+
BuildInfoResponse,
3+
Organization,
4+
Provisioner,
5+
Template,
6+
UserAgent,
7+
UserResponse,
8+
Workspace,
9+
} from "../api/types"
210

311
export const MockSessionToken = { session_token: "my-session-token" }
412

513
export const MockAPIKey = { key: "my-api-key" }
614

15+
export const MockBuildInfo: BuildInfoResponse = {
16+
external_url: "file:///mock-url",
17+
version: "v99.999.9999+c9cdf14",
18+
}
19+
720
export const MockUser: UserResponse = {
821
id: "test-user",
922
username: "TestUser",

site/src/test_helpers/handlers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import { rest } from "msw"
22
import * as M from "./entities"
33

44
export const handlers = [
5+
// build info
6+
rest.get("/api/v2/buildinfo", async (req, res, ctx) => {
7+
return res(ctx.status(200), ctx.json(M.MockBuildInfo))
8+
}),
9+
510
// organizations
611
rest.get("/api/v2/organizations/:organizationId", async (req, res, ctx) => {
712
return res(ctx.status(200), ctx.json(M.MockOrganization))

site/src/xServices/StateContext.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { useInterpret } from "@xstate/react"
22
import React, { createContext } from "react"
33
import { ActorRefFrom } from "xstate"
4+
import { buildInfoMachine } from "./buildInfo/buildInfoXService"
45
import { userMachine } from "./user/userXService"
56

67
interface XServiceContextType {
8+
buildInfoXService: ActorRefFrom<typeof buildInfoMachine>
79
userXService: ActorRefFrom<typeof userMachine>
810
}
911

@@ -18,7 +20,14 @@ interface XServiceContextType {
1820
export const XServiceContext = createContext({} as XServiceContextType)
1921

2022
export const XServiceProvider: React.FC = ({ children }) => {
21-
const userXService = useInterpret(userMachine)
22-
23-
return <XServiceContext.Provider value={{ userXService }}>{children}</XServiceContext.Provider>
23+
return (
24+
<XServiceContext.Provider
25+
value={{
26+
buildInfoXService: useInterpret(buildInfoMachine),
27+
userXService: useInterpret(userMachine),
28+
}}
29+
>
30+
{children}
31+
</XServiceContext.Provider>
32+
)
2433
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { assign, createMachine } from "xstate"
2+
import * as API from "../../api"
3+
import * as Types from "../../api/types"
4+
5+
export interface BuildInfoContext {
6+
getBuildInfoError?: Error | unknown
7+
buildInfo?: Types.BuildInfoResponse
8+
}
9+
10+
export const buildInfoMachine = createMachine(
11+
{
12+
tsTypes: {} as import("./buildInfoXService.typegen").Typegen0,
13+
schema: {
14+
context: {} as BuildInfoContext,
15+
services: {} as {
16+
getBuildInfo: {
17+
data: Types.BuildInfoResponse
18+
}
19+
},
20+
},
21+
context: {
22+
buildInfo: undefined,
23+
},
24+
id: "buildInfoState",
25+
initial: "gettingBuildInfo",
26+
states: {
27+
gettingBuildInfo: {
28+
invoke: {
29+
src: "getBuildInfo",
30+
id: "getBuildInfo",
31+
onDone: [
32+
{
33+
actions: ["assignBuildInfo", "clearGetBuildInfoError"],
34+
target: "#buildInfoState.success",
35+
},
36+
],
37+
onError: [
38+
{
39+
actions: ["assignGetBuildInfoError", "clearBuildInfo"],
40+
target: "#buildInfoState.failure",
41+
},
42+
],
43+
},
44+
},
45+
success: {
46+
type: "final",
47+
},
48+
failure: {
49+
type: "final",
50+
},
51+
},
52+
},
53+
{
54+
services: {
55+
getBuildInfo: API.getBuildInfo,
56+
},
57+
actions: {
58+
assignBuildInfo: assign({
59+
buildInfo: (_, event) => event.data,
60+
}),
61+
clearBuildInfo: assign((context: BuildInfoContext) => ({
62+
...context,
63+
buildInfo: undefined,
64+
})),
65+
assignGetBuildInfoError: assign({
66+
getBuildInfoError: (_, event) => event.data,
67+
}),
68+
clearGetBuildInfoError: assign((context: BuildInfoContext) => ({
69+
...context,
70+
getBuildInfoError: undefined,
71+
})),
72+
},
73+
},
74+
)

0 commit comments

Comments
 (0)