Skip to content

Commit 0cf36a4

Browse files
committed
Merge remote-tracking branch 'origin/main' into add-error-boundary/kira-pilot
2 parents b9fce75 + 3be3560 commit 0cf36a4

34 files changed

+1499
-421
lines changed

.vscode/settings.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"cSpell.words": [
3+
"buildname",
34
"circbuf",
45
"cliflag",
56
"cliui",
@@ -56,6 +57,7 @@
5657
"TCGETS",
5758
"tcpip",
5859
"TCSETS",
60+
"testid",
5961
"tfexec",
6062
"tfjson",
6163
"tfstate",

docker-compose.yaml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ services:
66
- "7080:7080"
77
environment:
88
CODER_PG_CONNECTION_URL: "postgresql://${POSTGRES_USER:-username}:${POSTGRES_PASSWORD:-password}@database/${POSTGRES_DB:-coder}?sslmode=disable"
9+
# You'll need to set CODER_ACCESS_URL to an IP or domain
10+
# that workspaces can reach. This cannot be localhost
11+
# or 127.0.0.1 for non-Docker templates!
912
CODER_ADDRESS: "0.0.0.0:7080"
10-
# You'll need to set CODER_ACCESS_URL to an
11-
# externally-reachable IP to use non-Docker examples!
12-
CODER_ACCESS_URL: "${CODER_ACCESS_URL:-http://host.docker.internal:7080}"
13+
CODER_ACCESS_URL: "${CODER_ACCESS_URL}"
1314
volumes:
1415
- /var/run/docker.sock:/var/run/docker.sock
1516
depends_on:

examples/docker-image-builds/main.tf

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,9 @@ resource "docker_container" "workspace" {
9494
# Hostname makes the shell more user friendly: coder@my-workspace:~$
9595
hostname = lower(data.coder_workspace.me.name)
9696
dns = ["1.1.1.1"]
97-
command = ["sh", "-c", coder_agent.dev.init_script]
98-
env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"]
97+
# Use the docker gateway if the access URL is 127.0.0.1
98+
command = ["sh", "-c", replace(coder_agent.dev.init_script, "127.0.0.1", "host.docker.internal")]
99+
env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"]
99100
host {
100101
host = "host.docker.internal"
101102
ip = "host-gateway"

examples/docker/main.tf

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ variable "step2_arch" {
4747
provider "docker" {
4848
host = "unix:///var/run/docker.sock"
4949
}
50-
5150
provider "coder" {
5251
}
5352

@@ -82,8 +81,9 @@ resource "docker_container" "workspace" {
8281
# Hostname makes the shell more user friendly: coder@my-workspace:~$
8382
hostname = lower(data.coder_workspace.me.name)
8483
dns = ["1.1.1.1"]
85-
command = ["sh", "-c", coder_agent.dev.init_script]
86-
env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"]
84+
# Use the docker gateway if the access URL is 127.0.0.1
85+
command = ["sh", "-c", replace(coder_agent.dev.init_script, "127.0.0.1", "host.docker.internal")]
86+
env = ["CODER_AGENT_TOKEN=${coder_agent.dev.token}"]
8787
host {
8888
host = "host.docker.internal"
8989
ip = "host-gateway"

site/src/AppRouter.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ import { SSHKeysPage } from "./pages/SettingsPages/SSHKeysPage/SSHKeysPage"
1515
import TemplatesPage from "./pages/TemplatesPage/TemplatesPage"
1616
import { CreateUserPage } from "./pages/UsersPage/CreateUserPage/CreateUserPage"
1717
import { UsersPage } from "./pages/UsersPage/UsersPage"
18+
import { WorkspaceBuildPage } from "./pages/WorkspaceBuildPage/WorkspaceBuildPage"
1819
import { WorkspacePage } from "./pages/WorkspacePage/WorkspacePage"
1920
import { WorkspaceSettingsPage } from "./pages/WorkspaceSettingsPage/WorkspaceSettingsPage"
2021

2122
const TerminalPage = React.lazy(() => import("./pages/TerminalPage/TerminalPage"))
2223
const WorkspacesPage = React.lazy(() => import("./pages/WorkspacesPage/WorkspacesPage"))
24+
const CreateWorkspacePage = React.lazy(() => import("./pages/CreateWorkspacePage/CreateWorkspacePage"))
2325

2426
export const AppRouter: React.FC = () => (
2527
<React.Suspense fallback={<></>}>
@@ -83,6 +85,17 @@ export const AppRouter: React.FC = () => (
8385
</AuthAndFrame>
8486
}
8587
/>
88+
89+
<Route path=":template">
90+
<Route
91+
path="new"
92+
element={
93+
<RequireAuth>
94+
<CreateWorkspacePage />
95+
</RequireAuth>
96+
}
97+
/>
98+
</Route>
8699
</Route>
87100

88101
<Route path="users">
@@ -138,6 +151,15 @@ export const AppRouter: React.FC = () => (
138151
</Route>
139152
</Route>
140153

154+
<Route
155+
path="builds/:buildId"
156+
element={
157+
<AuthAndFrame>
158+
<WorkspaceBuildPage />
159+
</AuthAndFrame>
160+
}
161+
/>
162+
141163
{/* Using path="*"" means "match anything", so this route
142164
acts like a catch-all for URLs that we don't have explicit
143165
routes for. */}

site/src/api/api.ts

Lines changed: 33 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import axios, { AxiosRequestHeaders } from "axios"
2-
import { mutate } from "swr"
32
import { WorkspaceBuildTransition } from "./types"
43
import * as TypesGen from "./typesGenerated"
54

@@ -22,34 +21,6 @@ export const provisioners: TypesGen.ProvisionerDaemon[] = [
2221
},
2322
]
2423

25-
export namespace Workspace {
26-
export const create = async (
27-
organizationId: string,
28-
request: TypesGen.CreateWorkspaceRequest,
29-
): Promise<TypesGen.Workspace> => {
30-
const response = await fetch(`/api/v2/organizations/${organizationId}/workspaces`, {
31-
method: "POST",
32-
headers: {
33-
"Content-Type": "application/json",
34-
},
35-
body: JSON.stringify(request),
36-
})
37-
38-
const body = await response.json()
39-
if (!response.ok) {
40-
throw new Error(body.message)
41-
}
42-
43-
// Let SWR know that both the /api/v2/workspaces/* and /api/v2/templates/*
44-
// endpoints will need to fetch new data.
45-
const mutateWorkspacesPromise = mutate("/api/v2/workspaces")
46-
const mutateTemplatesPromise = mutate("/api/v2/templates")
47-
await Promise.all([mutateWorkspacesPromise, mutateTemplatesPromise])
48-
49-
return body
50-
}
51-
}
52-
5324
export const login = async (email: string, password: string): Promise<TypesGen.LoginWithPasswordResponse> => {
5425
const payload = JSON.stringify({
5526
email,
@@ -115,6 +86,21 @@ export const getTemplates = async (organizationId: string): Promise<TypesGen.Tem
11586
return response.data
11687
}
11788

89+
export const getTemplateByName = async (organizationId: string, name: string): Promise<TypesGen.Template> => {
90+
const response = await axios.get<TypesGen.Template>(`/api/v2/organizations/${organizationId}/templates/${name}`)
91+
return response.data
92+
}
93+
94+
export const getTemplateVersion = async (versionId: string): Promise<TypesGen.TemplateVersion> => {
95+
const response = await axios.get<TypesGen.TemplateVersion>(`/api/v2/templateversions/${versionId}`)
96+
return response.data
97+
}
98+
99+
export const getTemplateVersionSchema = async (versionId: string): Promise<TypesGen.ParameterSchema[]> => {
100+
const response = await axios.get<TypesGen.ParameterSchema[]>(`/api/v2/templateversions/${versionId}/schema`)
101+
return response.data
102+
}
103+
118104
export const getWorkspace = async (workspaceId: string): Promise<TypesGen.Workspace> => {
119105
const response = await axios.get<TypesGen.Workspace>(`/api/v2/workspaces/${workspaceId}`)
120106
return response.data
@@ -180,6 +166,14 @@ export const createUser = async (user: TypesGen.CreateUserRequest): Promise<Type
180166
return response.data
181167
}
182168

169+
export const createWorkspace = async (
170+
organizationId: string,
171+
workspace: TypesGen.CreateWorkspaceRequest,
172+
): Promise<TypesGen.Workspace> => {
173+
const response = await axios.post<TypesGen.Workspace>(`/api/v2/organizations/${organizationId}/workspaces`, workspace)
174+
return response.data
175+
}
176+
183177
export const getBuildInfo = async (): Promise<TypesGen.BuildInfoResponse> => {
184178
const response = await axios.get("/api/v2/buildinfo")
185179
return response.data
@@ -248,3 +242,13 @@ export const getWorkspaceBuilds = async (workspaceId: string): Promise<TypesGen.
248242
const response = await axios.get<TypesGen.WorkspaceBuild[]>(`/api/v2/workspaces/${workspaceId}/builds`)
249243
return response.data
250244
}
245+
246+
export const getWorkspaceBuild = async (workspaceId: string): Promise<TypesGen.WorkspaceBuild> => {
247+
const response = await axios.get<TypesGen.WorkspaceBuild>(`/api/v2/workspacebuilds/${workspaceId}`)
248+
return response.data
249+
}
250+
251+
export const getWorkspaceBuildLogs = async (buildname: string): Promise<TypesGen.ProvisionerJobLog[]> => {
252+
const response = await axios.get<TypesGen.ProvisionerJobLog[]>(`/api/v2/workspacebuilds/${buildname}/logs`)
253+
return response.data
254+
}
Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,18 @@
11
import Box from "@material-ui/core/Box"
2-
import { Theme } from "@material-ui/core/styles"
2+
import { makeStyles, Theme } from "@material-ui/core/styles"
33
import Table from "@material-ui/core/Table"
44
import TableBody from "@material-ui/core/TableBody"
55
import TableCell from "@material-ui/core/TableCell"
66
import TableHead from "@material-ui/core/TableHead"
77
import TableRow from "@material-ui/core/TableRow"
88
import useTheme from "@material-ui/styles/useTheme"
9-
import dayjs from "dayjs"
10-
import duration from "dayjs/plugin/duration"
11-
import relativeTime from "dayjs/plugin/relativeTime"
129
import React from "react"
10+
import { useNavigate } from "react-router-dom"
1311
import * as TypesGen from "../../api/typesGenerated"
14-
import { getDisplayStatus } from "../../util/workspace"
12+
import { displayWorkspaceBuildDuration, getDisplayStatus } from "../../util/workspace"
1513
import { EmptyState } from "../EmptyState/EmptyState"
1614
import { TableLoader } from "../TableLoader/TableLoader"
1715

18-
dayjs.extend(relativeTime)
19-
dayjs.extend(duration)
20-
2116
export const Language = {
2217
emptyMessage: "No builds found",
2318
inProgressLabel: "In progress",
@@ -27,19 +22,6 @@ export const Language = {
2722
statusLabel: "Status",
2823
}
2924

30-
const getDurationInSeconds = (build: TypesGen.WorkspaceBuild) => {
31-
let display = Language.inProgressLabel
32-
33-
if (build.job.started_at && build.job.completed_at) {
34-
const startedAt = dayjs(build.job.started_at)
35-
const completedAt = dayjs(build.job.completed_at)
36-
const diff = completedAt.diff(startedAt, "seconds")
37-
display = `${diff} seconds`
38-
}
39-
40-
return display
41-
}
42-
4325
export interface BuildsTableProps {
4426
builds?: TypesGen.WorkspaceBuild[]
4527
className?: string
@@ -48,6 +30,8 @@ export interface BuildsTableProps {
4830
export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) => {
4931
const isLoading = !builds
5032
const theme: Theme = useTheme()
33+
const navigate = useNavigate()
34+
const styles = useStyles()
5135

5236
return (
5337
<Table className={className}>
@@ -62,18 +46,35 @@ export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) =
6246
<TableBody>
6347
{isLoading && <TableLoader />}
6448
{builds &&
65-
builds.map((b) => {
66-
const status = getDisplayStatus(theme, b)
67-
const duration = getDurationInSeconds(b)
49+
builds.map((build) => {
50+
const status = getDisplayStatus(theme, build)
51+
52+
const navigateToBuildPage = () => {
53+
navigate(`/builds/${build.id}`)
54+
}
6855

6956
return (
70-
<TableRow key={b.id} data-testid={`build-${b.id}`}>
71-
<TableCell>{b.transition}</TableCell>
57+
<TableRow
58+
hover
59+
key={build.id}
60+
data-testid={`build-${build.id}`}
61+
tabIndex={0}
62+
onClick={navigateToBuildPage}
63+
onKeyDown={(event) => {
64+
if (event.key === "Enter") {
65+
navigateToBuildPage()
66+
}
67+
}}
68+
className={styles.clickableTableRow}
69+
>
70+
<TableCell>{build.transition}</TableCell>
7271
<TableCell>
73-
<span style={{ color: theme.palette.text.secondary }}>{duration}</span>
72+
<span style={{ color: theme.palette.text.secondary }}>{displayWorkspaceBuildDuration(build)}</span>
7473
</TableCell>
7574
<TableCell>
76-
<span style={{ color: theme.palette.text.secondary }}>{new Date(b.created_at).toLocaleString()}</span>
75+
<span style={{ color: theme.palette.text.secondary }}>
76+
{new Date(build.created_at).toLocaleString()}
77+
</span>
7778
</TableCell>
7879
<TableCell>
7980
<span style={{ color: status.color }}>{status.status}</span>
@@ -95,3 +96,17 @@ export const BuildsTable: React.FC<BuildsTableProps> = ({ builds, className }) =
9596
</Table>
9697
)
9798
}
99+
100+
const useStyles = makeStyles((theme) => ({
101+
clickableTableRow: {
102+
cursor: "pointer",
103+
104+
"&:hover td": {
105+
backgroundColor: theme.palette.background.default,
106+
},
107+
108+
"&:focus": {
109+
outline: `1px solid ${theme.palette.primary.dark}`,
110+
},
111+
},
112+
}))

site/src/components/Loader/Loader.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import Box from "@material-ui/core/Box"
2+
import CircularProgress from "@material-ui/core/CircularProgress"
3+
import React from "react"
4+
5+
export const Loader: React.FC<{ size?: number }> = ({ size = 26 }) => {
6+
return (
7+
<Box p={4} width="100%" display="flex" alignItems="center" justifyContent="center">
8+
<CircularProgress size={size} />
9+
</Box>
10+
)
11+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { ComponentMeta, Story } from "@storybook/react"
2+
import React from "react"
3+
import { MockWorkspaceBuildLogs } from "../../testHelpers/entities"
4+
import { Logs, LogsProps } from "./Logs"
5+
6+
export default {
7+
title: "components/Logs",
8+
component: Logs,
9+
} as ComponentMeta<typeof Logs>
10+
11+
const Template: Story<LogsProps> = (args) => <Logs {...args} />
12+
13+
const lines = MockWorkspaceBuildLogs.map((log) => ({
14+
time: log.created_at,
15+
output: log.output,
16+
}))
17+
export const Example = Template.bind({})
18+
Example.args = {
19+
lines,
20+
}

0 commit comments

Comments
 (0)