Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions site/src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,17 @@ export const getWorkspace = async (
return response.data
}

/**
*
* @param workspaceId
* @returns An EventSource that emits workspace event objects (ServerSentEvent)
*/
export const watchWorkspace = (workspaceId: string): EventSource => {
return new EventSource(`${location.protocol}//${location.host}/api/v2/workspaces/${workspaceId}/watch`,
{ withCredentials: true }
)
}

export const getURLWithSearchParams = (
basePath: string,
filter?: TypesGen.WorkspaceFilter | TypesGen.UsersRequest,
Expand Down
11 changes: 5 additions & 6 deletions site/src/pages/WorkspacePage/WorkspacePage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ export const WorkspacePage: FC = () => {
workspace,
getWorkspaceError,
template,
refreshTemplateError,
resources,
getResourcesError,
refreshTemplateWarning,
refreshWorkspaceWarning,
builds,
getBuildsError,
permissions,
Expand Down Expand Up @@ -70,7 +69,7 @@ export const WorkspacePage: FC = () => {
return (
<div className={styles.error}>
{Boolean(getWorkspaceError) && <ErrorSummary error={getWorkspaceError} />}
{Boolean(refreshTemplateError) && <ErrorSummary error={refreshTemplateError} />}
{Boolean(refreshTemplateWarning) && <ErrorSummary error={refreshTemplateWarning} />}
{Boolean(checkPermissionsError) && <ErrorSummary error={checkPermissionsError} />}
</div>
)
Expand Down Expand Up @@ -128,11 +127,11 @@ export const WorkspacePage: FC = () => {
handleDelete={() => workspaceSend("ASK_DELETE")}
handleUpdate={() => workspaceSend("UPDATE")}
handleCancel={() => workspaceSend("CANCEL")}
resources={resources}
resources={workspace.latest_build.resources}
builds={builds}
canUpdateWorkspace={canUpdateWorkspace}
workspaceErrors={{
[WorkspaceErrors.GET_RESOURCES_ERROR]: getResourcesError,
[WorkspaceErrors.GET_RESOURCES_ERROR]: refreshWorkspaceWarning,
[WorkspaceErrors.GET_BUILDS_ERROR]: getBuildsError,
[WorkspaceErrors.BUILD_ERROR]: buildError,
[WorkspaceErrors.CANCELLATION_ERROR]: cancellationError,
Expand Down
201 changes: 87 additions & 114 deletions site/src/xServices/workspace/workspaceXService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,39 @@ const latestBuild = (builds: TypesGen.WorkspaceBuild[]) => {
}

const Language = {
refreshTemplateError: "Error updating workspace: latest template could not be fetched.",
refreshTemplateWarning: "Error updating workspace: latest template could not be fetched.",
buildError: "Workspace action failed.",
}

type Permissions = Record<keyof ReturnType<typeof permissionsToCheck>, boolean>

export interface WorkspaceContext {
// our server side events instance
eventSource?: EventSource,
workspace?: TypesGen.Workspace
template?: TypesGen.Template
build?: TypesGen.WorkspaceBuild
resources?: TypesGen.WorkspaceResource[]
getWorkspaceError?: Error | unknown
// error creating a new WorkspaceBuild
buildError?: Error | unknown
// these are separate from getX errors because they don't make the page unusable
refreshWorkspaceError: Error | unknown
refreshTemplateError: Error | unknown
getResourcesError: Error | unknown
// these are labeled as warnings because they don't make the page unusable
refreshWorkspaceWarning?: Error | unknown
refreshTemplateWarning: Error | unknown
// Builds
builds?: TypesGen.WorkspaceBuild[]
getBuildsError?: Error | unknown
loadMoreBuildsError?: Error | unknown
// error creating a new WorkspaceBuild
buildError?: Error | unknown
cancellationMessage?: Types.Message
cancellationError?: Error | unknown
// permissions
permissions?: Permissions
checkPermissionsError?: Error | unknown
userId?: string
userId?: string,
}

export type WorkspaceEvent =
| { type: "GET_WORKSPACE"; workspaceName: string; username: string }
| { type: "REFRESH_WORKSPACE", data: TypesGen.ServerSentEvent["data"] }
| { type: "START" }
| { type: "STOP" }
| { type: "ASK_DELETE" }
Expand All @@ -53,7 +54,9 @@ export type WorkspaceEvent =
| { type: "UPDATE" }
| { type: "CANCEL" }
| { type: "LOAD_MORE_BUILDS" }
| { type: "CHECK_REFRESH_TIMELINE", data: TypesGen.ServerSentEvent["data"] }
| { type: "REFRESH_TIMELINE" }
| { type: "EVENT_SOURCE_ERROR", error: Error | unknown }

export const checks = {
readWorkspace: "readWorkspace",
Expand Down Expand Up @@ -109,12 +112,9 @@ export const workspaceMachine = createMachine(
cancelWorkspace: {
data: Types.Message
}
refreshWorkspace: {
data: TypesGen.Workspace | undefined
}
getResources: {
data: TypesGen.WorkspaceResource[]
}
listening: {
data: TypesGen.ServerSentEvent
},
getBuilds: {
data: TypesGen.WorkspaceBuild[]
}
Expand Down Expand Up @@ -158,7 +158,7 @@ export const workspaceMachine = createMachine(
tags: "loading",
},
refreshingTemplate: {
entry: "clearRefreshTemplateError",
entry: "clearRefreshTemplateWarning",
invoke: {
src: "getTemplate",
id: "refreshTemplate",
Expand All @@ -170,7 +170,7 @@ export const workspaceMachine = createMachine(
],
onError: [
{
actions: ["assignRefreshTemplateError", "displayRefreshTemplateError"],
actions: ["assignRefreshTemplateWarning", "displayRefreshTemplateWarning"],
target: "error",
},
],
Expand Down Expand Up @@ -199,36 +199,36 @@ export const workspaceMachine = createMachine(
ready: {
type: "parallel",
states: {
pollingWorkspace: {
initial: "refreshingWorkspace",
listening: {
initial: "gettingEvents",
states: {
refreshingWorkspace: {
entry: "clearRefreshWorkspaceError",
gettingEvents: {
entry: ['clearRefreshWorkspaceWarning', 'initializeEventSource'],
exit: "closeEventSource",
invoke: {
src: "refreshWorkspace",
id: "refreshWorkspace",
onDone: [
{
actions: ["refreshTimeline", "assignWorkspace"],
target: "waiting",
},
],
onError: [
{
actions: "assignRefreshWorkspaceError",
target: "waiting",
},
],
src: "listening",
},
},
waiting: {
after: {
"1000": {
target: "refreshingWorkspace",
on: {
REFRESH_WORKSPACE: {
actions: ["refreshWorkspace"]
},
EVENT_SOURCE_ERROR: {
target: "error"
},
CHECK_REFRESH_TIMELINE: {
actions: ["refreshTimeline"]
}
},
},
},
error: {
entry: "assignRefreshWorkspaceWarning",
after: {
"1000": {
target: 'gettingEvents'
}
}
}
}
},
build: {
initial: "idle",
Expand Down Expand Up @@ -348,7 +348,7 @@ export const workspaceMachine = createMachine(
},
},
refreshingTemplate: {
entry: "clearRefreshTemplateError",
entry: "clearRefreshTemplateWarning",
invoke: {
src: "getTemplate",
id: "refreshTemplate",
Expand All @@ -360,45 +360,14 @@ export const workspaceMachine = createMachine(
],
onError: [
{
actions: ["assignRefreshTemplateError", "displayRefreshTemplateError"],
actions: ["assignRefreshTemplateWarning", "displayRefreshTemplateWarning"],
target: "idle",
},
],
},
},
},
},
pollingResources: {
initial: "gettingResources",
states: {
gettingResources: {
entry: "clearGetResourcesError",
invoke: {
src: "getResources",
id: "getResources",
onDone: [
{
actions: "assignResources",
target: "waiting",
},
],
onError: [
{
actions: "assignGetResourcesError",
target: "waiting",
},
],
},
},
waiting: {
after: {
"5000": {
target: "gettingResources",
},
},
},
},
},
timeline: {
initial: "gettingBuilds",
states: {
Expand Down Expand Up @@ -477,6 +446,7 @@ export const workspaceMachine = createMachine(
template: undefined,
build: undefined,
permissions: undefined,
eventSource: undefined,
}),
assignWorkspace: assign({
workspace: (_, event) => event.data,
Expand Down Expand Up @@ -525,30 +495,29 @@ export const workspaceMachine = createMachine(
clearCancellationError: assign({
cancellationError: (_) => undefined,
}),
assignRefreshWorkspaceError: assign({
refreshWorkspaceError: (_, event) => event.data,
}),
clearRefreshWorkspaceError: assign({
refreshWorkspaceError: (_) => undefined,
// SSE related actions
// open a new EventSource so we can stream SSE
initializeEventSource: assign({
eventSource: (context) => context.workspace && API.watchWorkspace(context.workspace.id)
}),
assignRefreshTemplateError: assign({
refreshTemplateError: (_, event) => event.data,
closeEventSource: (context) => context.eventSource && context.eventSource.close(),
refreshWorkspace: assign({
workspace: (_, event) => event.data,
}),
displayRefreshTemplateError: () => {
displayError(Language.refreshTemplateError)
},
clearRefreshTemplateError: assign({
refreshTemplateError: (_) => undefined,
assignRefreshWorkspaceWarning: assign({
refreshWorkspaceWarning: (_, event) => event,
}),
// Resources
assignResources: assign({
resources: (_, event) => event.data,
clearRefreshWorkspaceWarning: assign({
refreshWorkspaceWarning: (_) => undefined,
}),
assignGetResourcesError: assign({
getResourcesError: (_, event) => event.data,
assignRefreshTemplateWarning: assign({
refreshTemplateWarning: (_, event) => event.data,
}),
clearGetResourcesError: assign({
getResourcesError: (_) => undefined,
displayRefreshTemplateWarning: () => {
displayError(Language.refreshTemplateWarning)
},
clearRefreshTemplateWarning: assign({
refreshTemplateWarning: (_) => undefined,
}),
// Timeline
assignBuilds: assign({
Expand Down Expand Up @@ -583,9 +552,10 @@ export const workspaceMachine = createMachine(
if (!context.builds) {
return
}
// When it is a refresh workspace event, we want to check if the latest

// When it is a CHECK_REFRESH_TIMELINE workspace event, we want to check if the latest
// build was updated to not over fetch the builds
if (event.type === "done.invoke.refreshWorkspace") {
if (event.type === "CHECK_REFRESH_TIMELINE") {
const latestBuildInTimeline = latestBuild(context.builds)
const isUpdated = event.data?.latest_build.updated_at !== latestBuildInTimeline.updated_at
if (isUpdated) {
Expand Down Expand Up @@ -650,27 +620,30 @@ export const workspaceMachine = createMachine(
throw Error("Cannot cancel workspace without build id")
}
},
refreshWorkspace: async (context) => {
if (context.workspace) {
return await API.getWorkspaceByOwnerAndName(
context.workspace.owner_name,
context.workspace.name,
{
include_deleted: true,
},
)
} else {
throw Error("Cannot refresh workspace without id")
listening: (context) => (send) => {
if (!context.eventSource) {
send({ type: "EVENT_SOURCE_ERROR", error: "error initializing sse" })
return
}
},
getResources: async (context) => {
// If the job hasn't completed, fetching resources will result
// in an unfriendly error for the user.
if (!context.workspace?.latest_build.job.completed_at) {
return []

context.eventSource.addEventListener("data", (event) => {
// refresh our workspace with each SSE
send({ type: "REFRESH_WORKSPACE", data: JSON.parse(event.data) }) // i wonder if this is problematic
// refresh our timeline
send({ type: "CHECK_REFRESH_TIMELINE", data: JSON.parse(event.data) })
})

// handle any error events returned by our sse
context.eventSource.addEventListener("error", (event) => {
send({ type: "EVENT_SOURCE_ERROR", error: event })
})

// handle any sse implementation exceptions
context.eventSource.onerror = () => {
context.eventSource && context.eventSource.close();
send({ type: "EVENT_SOURCE_ERROR", error: "sse error" })
}
const resources = await API.getWorkspaceResources(context.workspace.latest_build.id)
return resources

},
getBuilds: async (context) => {
if (context.workspace) {
Expand Down
2 changes: 2 additions & 0 deletions site/webpack.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const config: Configuration = {
secure: false,
},
},
// We must disable compression to get SSEs to work (in workspaceXService.ts)
compress: false,
static: ["./static"],
},

Expand Down