From e5cbcd85b967d3b37cda8a938cc0a7fe2d194206 Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 13 Jun 2022 18:07:34 +0000 Subject: [PATCH 1/7] Add version tooltup --- .../components/HelpTooltip/HelpTooltip.tsx | 38 +++++++++++++++++-- .../WorkspacesPageView.stories.tsx | 7 ++++ .../WorkspacesPage/WorkspacesPageView.tsx | 38 ++++++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx index 8d14452ec2421..15a622ef87970 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -3,9 +3,11 @@ import Popover from "@material-ui/core/Popover" import { makeStyles } from "@material-ui/core/styles" import HelpIcon from "@material-ui/icons/HelpOutline" import OpenInNewIcon from "@material-ui/icons/OpenInNew" -import { useState } from "react" +import React, { useState } from "react" import { Stack } from "../Stack/Stack" +type Icon = typeof HelpIcon + type Size = "small" | "medium" export interface HelpTooltipProps { // Useful to test on storybook @@ -70,6 +72,17 @@ export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href }) ) } +export const HelpTooltipAction: React.FC<{ icon: Icon; onClick: () => void }> = ({ children, icon: Icon, onClick }) => { + const styles = useStyles() + + return ( + + ) +} + export const HelpTooltipLinksGroup: React.FC = ({ children }) => { const styles = useStyles() @@ -110,11 +123,12 @@ const useStyles = makeStyles((theme) => ({ padding: 0, border: 0, background: "transparent", - color: theme.palette.text.secondary, + color: theme.palette.text.primary, + opacity: 0.5, cursor: "pointer", "&:hover": { - color: theme.palette.text.primary, + opacity: 0.75, }, }, @@ -156,4 +170,22 @@ const useStyles = makeStyles((theme) => ({ linksGroup: { marginTop: theme.spacing(2), }, + + action: { + display: "flex", + alignItems: "center", + background: "none", + border: 0, + color: theme.palette.primary.light, + padding: 0, + cursor: "pointer", + fontSize: 14, + }, + + actionIcon: { + color: "inherit", + width: 14, + height: 14, + marginRight: theme.spacing(1), + }, })) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 1b62b533ae30b..99329b21bca08 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -14,9 +14,11 @@ const Template: Story = (args) => { return { ...MockWorkspace, + outdated, latest_build: { ...MockWorkspace.latest_build, transition, @@ -49,6 +51,11 @@ AllStates.args = { ], } +export const Outdated = Template.bind({}) +Outdated.args = { + workspaces: [createWorkspaceWithStatus("running", "stop", true)], +} + export const OwnerHasNoWorkspaces = Template.bind({}) OwnerHasNoWorkspaces.args = { workspaces: [], diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index c3c6b7c0a942a..19996565297f7 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -13,6 +13,7 @@ import TableRow from "@material-ui/core/TableRow" import TextField from "@material-ui/core/TextField" import AddCircleOutline from "@material-ui/icons/AddCircleOutline" import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" +import RefreshIcon from "@material-ui/icons/Refresh" import SearchIcon from "@material-ui/icons/Search" import useTheme from "@material-ui/styles/useTheme" import dayjs from "dayjs" @@ -26,6 +27,7 @@ import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/Dro import { EmptyState } from "../../components/EmptyState/EmptyState" import { HelpTooltip, + HelpTooltipAction, HelpTooltipLink, HelpTooltipLinksGroup, HelpTooltipText, @@ -54,6 +56,11 @@ export const Language = { workspaceTooltipLink1: "Create workspaces", workspaceTooltipLink2: "Connect with SSH", workspaceTooltipLink3: "Editors and IDEs", + outdatedLabel: "Outdated", + upToDateLabel: "Up to date", + versionTooltipText: + "Looks like the version you are using for this workspace is outdated and there is a newest version that you could use.", + updateVersionLabel: "Update version", } const WorkspaceHelpTooltip: React.FC = () => { @@ -76,6 +83,20 @@ const WorkspaceHelpTooltip: React.FC = () => { ) } +const OutdatedHelpTooltip: React.FC<{ onUpdateVersion: () => void }> = ({ onUpdateVersion }) => { + return ( + + {Language.outdatedLabel} + {Language.versionTooltipText} + + + {Language.updateVersionLabel} + + + + ) +} + interface FilterFormValues { query: string } @@ -256,9 +277,16 @@ export const WorkspacesPageView: FC = ({ loading, works {workspace.template_name} {workspace.outdated ? ( - outdated + + {Language.outdatedLabel} + { + console.log("UPDATE!!") + }} + /> + ) : ( - up to date + {Language.upToDateLabel} )} @@ -341,4 +369,10 @@ const useStyles = makeStyles((theme) => ({ arrowCell: { display: "flex", }, + outdatedLabel: { + color: theme.palette.error.main, + display: "flex", + alignItems: "center", + gap: theme.spacing(0.5), + }, })) From 9d92df20f4c7db54023975ef7a42b0dcd460bb5a Mon Sep 17 00:00:00 2001 From: Bruno Date: Mon, 13 Jun 2022 20:20:09 +0000 Subject: [PATCH 2/7] Add update action --- .../components/HelpTooltip/HelpTooltip.tsx | 43 +++++- .../pages/WorkspacesPage/WorkspacesPage.tsx | 3 +- .../WorkspacesPageView.stories.tsx | 14 +- .../WorkspacesPage/WorkspacesPageView.tsx | 137 +++++++++-------- .../workspaces/workspacesXService.ts | 144 +++++++++++++++++- 5 files changed, 254 insertions(+), 87 deletions(-) diff --git a/site/src/components/HelpTooltip/HelpTooltip.tsx b/site/src/components/HelpTooltip/HelpTooltip.tsx index 15a622ef87970..1ed96af999871 100644 --- a/site/src/components/HelpTooltip/HelpTooltip.tsx +++ b/site/src/components/HelpTooltip/HelpTooltip.tsx @@ -3,7 +3,7 @@ import Popover from "@material-ui/core/Popover" import { makeStyles } from "@material-ui/core/styles" import HelpIcon from "@material-ui/icons/HelpOutline" import OpenInNewIcon from "@material-ui/icons/OpenInNew" -import React, { useState } from "react" +import React, { createContext, useContext, useState } from "react" import { Stack } from "../Stack/Stack" type Icon = typeof HelpIcon @@ -15,15 +15,38 @@ export interface HelpTooltipProps { size?: Size } +const HelpTooltipContext = createContext<{ open: boolean; onClose: () => void } | undefined>(undefined) + +const useHelpTooltip = () => { + const helpTooltipContext = useContext(HelpTooltipContext) + + if (!helpTooltipContext) { + throw new Error("This hook should be used in side of the HelpTooltipContext.") + } + + return helpTooltipContext +} + export const HelpTooltip: React.FC = ({ children, open, size = "medium" }) => { const styles = useStyles({ size }) const [anchorEl, setAnchorEl] = useState(null) open = open ?? Boolean(anchorEl) const id = open ? "help-popover" : undefined + const onClose = () => { + setAnchorEl(null) + } + return ( <> - = ({ children, open, size = id={id} open={open} anchorEl={anchorEl} - onClose={() => { - setAnchorEl(null) - }} + onClose={onClose} anchorOrigin={{ vertical: "bottom", horizontal: "left", @@ -43,7 +64,7 @@ export const HelpTooltip: React.FC = ({ children, open, size = horizontal: "left", }} > - {children} + {children} ) @@ -74,9 +95,17 @@ export const HelpTooltipLink: React.FC<{ href: string }> = ({ children, href }) export const HelpTooltipAction: React.FC<{ icon: Icon; onClick: () => void }> = ({ children, icon: Icon, onClick }) => { const styles = useStyles() + const tooltip = useHelpTooltip() return ( - diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index c5f47cc50a537..0c2ad7a8988c0 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -10,6 +10,7 @@ import { WorkspacesPageView } from "./WorkspacesPageView" const WorkspacesPage: FC = () => { const [workspacesState, send] = useMachine(workspacesMachine) const [searchParams, setSearchParams] = useSearchParams() + const { workspaceRefs } = workspacesState.context useEffect(() => { const filter = searchParams.get("filter") @@ -30,7 +31,7 @@ const WorkspacesPage: FC = () => { { searchParams.set("filter", query) setSearchParams(searchParams) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx index 99329b21bca08..f25a33518eb59 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.stories.tsx @@ -1,7 +1,9 @@ import { ComponentMeta, Story } from "@storybook/react" +import { spawn } from "xstate" import { ProvisionerJobStatus, Workspace, WorkspaceTransition } from "../../api/typesGenerated" import { MockWorkspace } from "../../testHelpers/entities" import { workspaceFilterQuery } from "../../util/workspace" +import { workspaceItemMachine } from "../../xServices/workspaces/workspacesXService" import { WorkspacesPageView, WorkspacesPageViewProps } from "./WorkspacesPageView" export default { @@ -43,27 +45,29 @@ const workspaces: { [key in ProvisionerJobStatus]: Workspace } = { export const AllStates = Template.bind({}) AllStates.args = { - workspaces: [ + workspaceRefs: [ ...Object.values(workspaces), createWorkspaceWithStatus("running", "stop"), createWorkspaceWithStatus("succeeded", "stop"), createWorkspaceWithStatus("running", "delete"), - ], + ].map((data) => spawn(workspaceItemMachine.withContext({ data }))), } export const Outdated = Template.bind({}) Outdated.args = { - workspaces: [createWorkspaceWithStatus("running", "stop", true)], + workspaceRefs: [createWorkspaceWithStatus("running", "stop", true)].map((data) => + spawn(workspaceItemMachine.withContext({ data })), + ), } export const OwnerHasNoWorkspaces = Template.bind({}) OwnerHasNoWorkspaces.args = { - workspaces: [], + workspaceRefs: [], filter: workspaceFilterQuery.me, } export const NoResults = Template.bind({}) NoResults.args = { - workspaces: [], + workspaceRefs: [], filter: "searchtearmwithnoresults", } diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 19996565297f7..01e0b7192b6a7 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -16,12 +16,12 @@ import KeyboardArrowRight from "@material-ui/icons/KeyboardArrowRight" import RefreshIcon from "@material-ui/icons/Refresh" import SearchIcon from "@material-ui/icons/Search" import useTheme from "@material-ui/styles/useTheme" +import { useActor } from "@xstate/react" import dayjs from "dayjs" import relativeTime from "dayjs/plugin/relativeTime" import { FormikErrors, useFormik } from "formik" import { FC, useState } from "react" import { Link as RouterLink, useNavigate } from "react-router-dom" -import * as TypesGen from "../../api/typesGenerated" import { AvatarData } from "../../components/AvatarData/AvatarData" import { CloseDropdown, OpenDropdown } from "../../components/DropdownArrows/DropdownArrows" import { EmptyState } from "../../components/EmptyState/EmptyState" @@ -39,6 +39,7 @@ import { Stack } from "../../components/Stack/Stack" import { TableLoader } from "../../components/TableLoader/TableLoader" import { getFormHelpers, onChangeTrimmed } from "../../util/formUtils" import { getDisplayStatus, workspaceFilterQuery } from "../../util/workspace" +import { WorkspaceItemMachineRef } from "../../xServices/workspaces/workspacesXService" dayjs.extend(relativeTime) @@ -97,6 +98,64 @@ const OutdatedHelpTooltip: React.FC<{ onUpdateVersion: () => void }> = ({ onUpda ) } +const WorkspaceRow: React.FC<{ workspaceRef: WorkspaceItemMachineRef }> = ({ workspaceRef }) => { + const styles = useStyles() + const navigate = useNavigate() + const theme: Theme = useTheme() + const [workspaceState, send] = useActor(workspaceRef) + const { data: workspace } = workspaceState.context + const status = getDisplayStatus(theme, workspace.latest_build) + const navigateToWorkspacePage = () => { + navigate(`/@${workspace.owner_name}/${workspace.name}`) + } + return ( + { + if (event.key === "Enter") { + navigateToWorkspacePage() + } + }} + className={styles.clickableTableRow} + > + + + + {workspace.template_name} + + {workspace.outdated ? ( + + {Language.outdatedLabel} + { + send("UPDATE_VERSION") + }} + /> + + ) : ( + {Language.upToDateLabel} + )} + + + + {dayjs().to(dayjs(workspace.latest_build.created_at))} + + + + {status.status} + + +
+ +
+
+
+ ) +} + interface FilterFormValues { query: string } @@ -105,15 +164,13 @@ export type FilterFormErrors = FormikErrors export interface WorkspacesPageViewProps { loading?: boolean - workspaces?: TypesGen.Workspace[] + workspaceRefs?: WorkspaceItemMachineRef[] filter?: string onFilter: (query: string) => void } -export const WorkspacesPageView: FC = ({ loading, workspaces, filter, onFilter }) => { +export const WorkspacesPageView: FC = ({ loading, workspaceRefs, filter, onFilter }) => { const styles = useStyles() - const navigate = useNavigate() - const theme: Theme = useTheme() const form = useFormik({ enableReinitialize: true, @@ -216,17 +273,17 @@ export const WorkspacesPageView: FC = ({ loading, works - Name - Template - Version - Last Built - Status + Name + Template + Version + Last Built + Status - {!workspaces && loading && } - {workspaces && workspaces.length === 0 && ( + {!workspaceRefs && loading && } + {workspaceRefs && workspaceRefs.length === 0 && ( <> {filter === workspaceFilterQuery.me || filter === workspaceFilterQuery.all ? ( @@ -251,60 +308,8 @@ export const WorkspacesPageView: FC = ({ loading, works )} )} - {workspaces && - workspaces.map((workspace) => { - const status = getDisplayStatus(theme, workspace.latest_build) - const navigateToWorkspacePage = () => { - navigate(`/@${workspace.owner_name}/${workspace.name}`) - } - return ( - { - if (event.key === "Enter") { - navigateToWorkspacePage() - } - }} - className={styles.clickableTableRow} - > - - - - {workspace.template_name} - - {workspace.outdated ? ( - - {Language.outdatedLabel} - { - console.log("UPDATE!!") - }} - /> - - ) : ( - {Language.upToDateLabel} - )} - - - - {dayjs().to(dayjs(workspace.latest_build.created_at))} - - - - {status.status} - - -
- -
-
-
- ) - })} + {workspaceRefs && + workspaceRefs.map((workspaceRef) => )}
diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index 842d1108d2b67..d724e699ac7af 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -1,22 +1,135 @@ -import { assign, createMachine } from "xstate" +import { ActorRefFrom, assign, createMachine, spawn } from "xstate" import * as API from "../../api/api" +import { getErrorMessage } from "../../api/errors" import * as TypesGen from "../../api/typesGenerated" +import { displayError, displayMsg } from "../../components/GlobalSnackbar/utils" import { workspaceQueryToFilter } from "../../util/workspace" +interface WorkspaceItemContext { + data: TypesGen.Workspace + updatedTemplate?: TypesGen.Template +} + +type WorkspaceItemEvent = { + type: "UPDATE_VERSION" +} + +export const workspaceItemMachine = createMachine( + { + id: "workspaceItemMachine", + schema: { + context: {} as WorkspaceItemContext, + events: {} as WorkspaceItemEvent, + services: {} as { + getTemplate: { + data: TypesGen.Template + } + startWorkspace: { + data: TypesGen.WorkspaceBuild + } + }, + }, + tsTypes: {} as import("./workspacesXService.typegen").Typegen0, + initial: "idle", + states: { + idle: { + on: { + UPDATE_VERSION: "updatingVersion", + }, + }, + updatingVersion: { + entry: "displayUpdatingVersionMessage", + initial: "gettingUpdatedTemplate", + onDone: "idle", + states: { + gettingUpdatedTemplate: { + invoke: { + id: "getTemplate", + src: "getTemplate", + onDone: { + actions: "assignUpdatedTemplate", + target: "restartingWorkspace", + }, + onError: { + target: "error", + actions: "displayUpdateVersionError", + }, + }, + }, + restartingWorkspace: { + invoke: { + id: "startWorkspace", + src: "startWorkspace", + onDone: { + actions: "assignLatestBuild", + target: "success", + }, + onError: { + target: "error", + actions: "displayUpdateVersionError", + }, + }, + }, + error: { + type: "final", + }, + success: { + type: "final", + }, + }, + }, + }, + }, + { + services: { + getTemplate: (context) => API.getTemplate(context.data.template_id), + startWorkspace: (context) => { + if (!context.updatedTemplate) { + throw new Error("Updated template is not loaded.") + } + + return API.startWorkspace(context.data.id, context.updatedTemplate.active_version_id) + }, + }, + actions: { + assignUpdatedTemplate: assign({ + updatedTemplate: (_, event) => event.data, + }), + assignLatestBuild: assign({ + data: (context, event) => { + return { + ...context.data, + latest_build: event.data, + } + }, + }), + displayUpdateVersionError: (_, event) => { + const message = getErrorMessage(event.data, "Error on update workspace version.") + displayError(message) + }, + displayUpdatingVersionMessage: () => { + displayMsg("Updating workspace version", "When it is done, the workspace will be updated in the list.") + }, + }, + }, +) + +export type WorkspaceItemMachineRef = ActorRefFrom + interface WorkspaceContext { - workspaces?: TypesGen.Workspace[] + workspaceRefs?: WorkspaceItemMachineRef[] filter?: string getWorkspacesError?: Error | unknown } -type WorkspaceEvent = { type: "GET_WORKSPACE"; workspaceId: string } | { type: "SET_FILTER"; query: string } +type WorkspacesEvent = { type: "SET_FILTER"; query: string } | { type: "UPDATE_VERSION"; workspaceId: string } export const workspacesMachine = createMachine( { - tsTypes: {} as import("./workspacesXService.typegen").Typegen0, + tsTypes: {} as import("./workspacesXService.typegen").Typegen1, schema: { context: {} as WorkspaceContext, - events: {} as WorkspaceEvent, + events: {} as WorkspacesEvent, services: {} as { getWorkspaces: { data: TypesGen.Workspace[] @@ -29,6 +142,9 @@ export const workspacesMachine = createMachine( ready: { on: { SET_FILTER: "extractingFilter", + UPDATE_VERSION: { + actions: "triggerUpdateVersion", + }, }, }, extractingFilter: { @@ -44,7 +160,7 @@ export const workspacesMachine = createMachine( id: "getWorkspaces", onDone: { target: "ready", - actions: ["assignWorkspaces", "clearGetWorkspacesError"], + actions: ["assignWorkspaceRefs", "clearGetWorkspacesError"], }, onError: { target: "ready", @@ -57,8 +173,11 @@ export const workspacesMachine = createMachine( }, { actions: { - assignWorkspaces: assign({ - workspaces: (_, event) => event.data, + assignWorkspaceRefs: assign({ + workspaceRefs: (_, event) => + event.data.map((data) => { + return spawn(workspaceItemMachine.withContext({ data }), data.id) + }), }), assignFilter: assign({ filter: (_, event) => event.query, @@ -68,6 +187,15 @@ export const workspacesMachine = createMachine( }), clearGetWorkspacesError: (context) => assign({ ...context, getWorkspacesError: undefined }), clearWorkspaces: (context) => assign({ ...context, workspaces: undefined }), + triggerUpdateVersion: (context, event) => { + const workspaceRef = context.workspaceRefs?.find((ref) => ref.id === event.workspaceId) + + if (!workspaceRef) { + throw new Error(`No workspace ref found for ${event.workspaceId}.`) + } + + workspaceRef.send("UPDATE_VERSION") + }, }, services: { getWorkspaces: (context) => API.getWorkspaces(workspaceQueryToFilter(context.filter)), From 58d6a6a69903265abc9bcd0c2b26ae8081e56663 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 14 Jun 2022 14:44:16 +0000 Subject: [PATCH 3/7] Add pooling --- .../workspaces/workspacesXService.ts | 87 +++++++++++++++---- 1 file changed, 72 insertions(+), 15 deletions(-) diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index d724e699ac7af..e6fc5c9649425 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -2,9 +2,15 @@ import { ActorRefFrom, assign, createMachine, spawn } from "xstate" import * as API from "../../api/api" import { getErrorMessage } from "../../api/errors" import * as TypesGen from "../../api/typesGenerated" -import { displayError, displayMsg } from "../../components/GlobalSnackbar/utils" +import { displayError, displayMsg, displaySuccess } from "../../components/GlobalSnackbar/utils" import { workspaceQueryToFilter } from "../../util/workspace" +/** + * Workspace item machine + * + * It is used to control the state and actions of each workspace in the + * workspaces page view + **/ interface WorkspaceItemContext { data: TypesGen.Workspace updatedTemplate?: TypesGen.Template @@ -30,18 +36,22 @@ export const workspaceItemMachine = createMachine( }, }, tsTypes: {} as import("./workspacesXService.typegen").Typegen0, - initial: "idle", + type: "parallel", states: { - idle: { - on: { - UPDATE_VERSION: "updatingVersion", - }, - }, - updatingVersion: { - entry: "displayUpdatingVersionMessage", - initial: "gettingUpdatedTemplate", - onDone: "idle", + updateVersion: { + initial: "idle", states: { + idle: { + on: { + UPDATE_VERSION: { + target: "gettingUpdatedTemplate", + // We can improve the UI by optimistically updating the workspace status + // to "Queued" so the UI can display the updated state right away and we + // don't need to display an extra spinner. + actions: ["assignQueuedStatus", "displayUpdatingVersionMessage"], + }, + }, + }, gettingUpdatedTemplate: { invoke: { id: "getTemplate", @@ -62,7 +72,7 @@ export const workspaceItemMachine = createMachine( src: "startWorkspace", onDone: { actions: "assignLatestBuild", - target: "success", + target: "waitingToBeUpdated", }, onError: { target: "error", @@ -70,6 +80,27 @@ export const workspaceItemMachine = createMachine( }, }, }, + waitingToBeUpdated: { + after: { + 5000: "gettingUpdatedWorkspaceData", + }, + }, + gettingUpdatedWorkspaceData: { + invoke: { + id: "getWorkspace", + src: "getWorkspace", + onDone: [ + { + target: "waitingToBeUpdated", + cond: "isOutdated", + }, + { + target: "success", + actions: "displayUpdatedSuccessMessage", + }, + ], + }, + }, error: { type: "final", }, @@ -81,6 +112,9 @@ export const workspaceItemMachine = createMachine( }, }, { + guards: { + isOutdated: (ctx) => !ctx.data.outdated, + }, services: { getTemplate: (context) => API.getTemplate(context.data.template_id), startWorkspace: (context) => { @@ -108,15 +142,38 @@ export const workspaceItemMachine = createMachine( displayError(message) }, displayUpdatingVersionMessage: () => { - displayMsg("Updating workspace version", "When it is done, the workspace will be updated in the list.") + displayMsg("Updating your workspace", "It will be running in a few seconds.") + }, + assignQueuedStatus: assign({ + data: (ctx) => { + return { + ...ctx.data, + latest_build: { + ...ctx.data.latest_build, + job: { + ...ctx.data.latest_build.job, + status: "pending" as TypesGen.ProvisionerJobStatus, + }, + }, + } + }, + }), + displayUpdatedSuccessMessage: () => { + displaySuccess("Workspace updated successfully.") }, }, }, ) +/** + * Workspaces machine + * + * It is used to control the state of the workspace list + **/ + export type WorkspaceItemMachineRef = ActorRefFrom -interface WorkspaceContext { +interface WorkspacesContext { workspaceRefs?: WorkspaceItemMachineRef[] filter?: string getWorkspacesError?: Error | unknown @@ -128,7 +185,7 @@ export const workspacesMachine = createMachine( { tsTypes: {} as import("./workspacesXService.typegen").Typegen1, schema: { - context: {} as WorkspaceContext, + context: {} as WorkspacesContext, events: {} as WorkspacesEvent, services: {} as { getWorkspaces: { From b4bc684e4c99c179a2cd75647007315f282c6455 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 14 Jun 2022 14:58:58 +0000 Subject: [PATCH 4/7] Fix pooling --- .../components/GlobalSnackbar/GlobalSnackbar.tsx | 1 + .../src/xServices/workspaces/workspacesXService.ts | 14 +++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/site/src/components/GlobalSnackbar/GlobalSnackbar.tsx b/site/src/components/GlobalSnackbar/GlobalSnackbar.tsx index e2dd795f806ce..b2dc8eae74619 100644 --- a/site/src/components/GlobalSnackbar/GlobalSnackbar.tsx +++ b/site/src/components/GlobalSnackbar/GlobalSnackbar.tsx @@ -72,6 +72,7 @@ export const GlobalSnackbar: React.FC = () => { return ( !ctx.data.outdated, + isOutdated: (_, event) => event.data.outdated, }, services: { getTemplate: (context) => API.getTemplate(context.data.template_id), @@ -124,6 +128,7 @@ export const workspaceItemMachine = createMachine( return API.startWorkspace(context.data.id, context.updatedTemplate.active_version_id) }, + getWorkspace: (context) => API.getWorkspace(context.data.id), }, actions: { assignUpdatedTemplate: assign({ @@ -142,7 +147,7 @@ export const workspaceItemMachine = createMachine( displayError(message) }, displayUpdatingVersionMessage: () => { - displayMsg("Updating your workspace", "It will be running in a few seconds.") + displayMsg("Updating workspace...") }, assignQueuedStatus: assign({ data: (ctx) => { @@ -161,6 +166,9 @@ export const workspaceItemMachine = createMachine( displayUpdatedSuccessMessage: () => { displaySuccess("Workspace updated successfully.") }, + assignUpdatedData: assign({ + data: (_, event) => event.data, + }), }, }, ) From ca18646c6630826cbc1bea0e5c4e6d797b4919b1 Mon Sep 17 00:00:00 2001 From: Bruno Date: Tue, 14 Jun 2022 17:05:03 +0000 Subject: [PATCH 5/7] Fix pulling and workspace data update --- .../pages/WorkspacesPage/WorkspacesPage.tsx | 2 +- .../workspaces/workspacesXService.ts | 124 +++++++++++++----- 2 files changed, 89 insertions(+), 37 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx index 0c2ad7a8988c0..1f742967e5310 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPage.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPage.tsx @@ -17,7 +17,7 @@ const WorkspacesPage: FC = () => { const query = filter !== null ? filter : workspaceFilterQuery.me send({ - type: "SET_FILTER", + type: "GET_WORKSPACES", query, }) }, [searchParams, send]) diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index f87195fc69748..6bfa7758b79d3 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -16,9 +16,14 @@ interface WorkspaceItemContext { updatedTemplate?: TypesGen.Template } -type WorkspaceItemEvent = { - type: "UPDATE_VERSION" -} +type WorkspaceItemEvent = + | { + type: "UPDATE_VERSION" + } + | { + type: "UPDATE_DATA" + data: TypesGen.Workspace + } export const workspaceItemMachine = createMachine( { @@ -40,6 +45,7 @@ export const workspaceItemMachine = createMachine( }, tsTypes: {} as import("./workspacesXService.typegen").Typegen0, type: "parallel", + states: { updateVersion: { initial: "idle", @@ -53,6 +59,9 @@ export const workspaceItemMachine = createMachine( // don't need to display an extra spinner. actions: ["assignQueuedStatus", "displayUpdatingVersionMessage"], }, + UPDATE_DATA: { + actions: "assignUpdatedData", + }, }, }, gettingUpdatedTemplate: { @@ -64,7 +73,7 @@ export const workspaceItemMachine = createMachine( target: "restartingWorkspace", }, onError: { - target: "error", + target: "idle", actions: "displayUpdateVersionError", }, }, @@ -78,7 +87,7 @@ export const workspaceItemMachine = createMachine( target: "waitingToBeUpdated", }, onError: { - target: "error", + target: "idle", actions: "displayUpdateVersionError", }, }, @@ -99,18 +108,12 @@ export const workspaceItemMachine = createMachine( actions: ["assignUpdatedData"], }, { - target: "success", + target: "idle", actions: ["assignUpdatedData", "displayUpdatedSuccessMessage"], }, ], }, }, - error: { - type: "final", - }, - success: { - type: "final", - }, }, }, }, @@ -187,7 +190,7 @@ interface WorkspacesContext { getWorkspacesError?: Error | unknown } -type WorkspacesEvent = { type: "SET_FILTER"; query: string } | { type: "UPDATE_VERSION"; workspaceId: string } +type WorkspacesEvent = { type: "GET_WORKSPACES"; query: string } | { type: "UPDATE_VERSION"; workspaceId: string } export const workspacesMachine = createMachine( { @@ -201,42 +204,54 @@ export const workspacesMachine = createMachine( } }, }, - id: "workspaceState", - initial: "ready", - states: { - ready: { - on: { - SET_FILTER: "extractingFilter", - UPDATE_VERSION: { - actions: "triggerUpdateVersion", - }, - }, + id: "workspacesState", + on: { + GET_WORKSPACES: { + actions: "assignFilter", + target: "gettingWorkspaces", }, - extractingFilter: { - entry: "assignFilter", - always: { - target: "gettingWorkspaces", - }, + UPDATE_VERSION: { + actions: "triggerUpdateVersion", + }, + }, + initial: "idle", + states: { + idle: { + tags: ["loading"], }, gettingWorkspaces: { entry: "clearGetWorkspacesError", invoke: { src: "getWorkspaces", id: "getWorkspaces", - onDone: { - target: "ready", - actions: ["assignWorkspaceRefs", "clearGetWorkspacesError"], - }, + onDone: [ + { + target: "waitToRefreshWorkspaces", + actions: ["assignWorkspaceRefs"], + cond: "isEmpty", + }, + { + target: "waitToRefreshWorkspaces", + actions: ["updateWorkspaceRefs"], + }, + ], onError: { - target: "ready", - actions: ["assignGetWorkspacesError", "clearWorkspaces"], + target: "waitToRefreshWorkspaces", + actions: ["assignGetWorkspacesError"], }, }, - tags: "loading", + }, + waitToRefreshWorkspaces: { + after: { + 5000: "gettingWorkspaces", + }, }, }, }, { + guards: { + isEmpty: (context) => !context.workspaceRefs, + }, actions: { assignWorkspaceRefs: assign({ workspaceRefs: (_, event) => @@ -251,7 +266,6 @@ export const workspacesMachine = createMachine( getWorkspacesError: (_, event) => event.data, }), clearGetWorkspacesError: (context) => assign({ ...context, getWorkspacesError: undefined }), - clearWorkspaces: (context) => assign({ ...context, workspaces: undefined }), triggerUpdateVersion: (context, event) => { const workspaceRef = context.workspaceRefs?.find((ref) => ref.id === event.workspaceId) @@ -261,6 +275,44 @@ export const workspacesMachine = createMachine( workspaceRef.send("UPDATE_VERSION") }, + updateWorkspaceRefs: assign({ + workspaceRefs: (context, event) => { + let workspaceRefs = context.workspaceRefs + + if (!workspaceRefs) { + throw new Error("No workspaces loaded.") + } + + // Update the existent workspaces or create the new ones + for (const data of event.data) { + const ref = workspaceRefs.find((ref) => ref.id === data.id) + + if (!ref) { + workspaceRefs.push(spawn(workspaceItemMachine.withContext({ data }), data.id)) + } else { + ref.send({ type: "UPDATE_DATA", data }) + } + } + + // Remove workspaces that were deleted + for (const ref of workspaceRefs) { + const refData = event.data.find((workspaceData) => workspaceData.id === ref.id) + + // If there is no refData, it is because the workspace was deleted + if (!refData) { + // Stop the actor before remove it from the array + if (ref.stop) { + ref.stop() + } + + // Remove ref from the array + workspaceRefs = workspaceRefs.filter((oldRef) => oldRef.id === ref.id) + } + } + + return workspaceRefs + }, + }), }, services: { getWorkspaces: (context) => API.getWorkspaces(workspaceQueryToFilter(context.filter)), From d982d558561ce0105f4fa9e8fed2f823ba2f0776 Mon Sep 17 00:00:00 2001 From: Bruno Quaresma Date: Wed, 15 Jun 2022 10:20:46 -0300 Subject: [PATCH 6/7] Update site/src/pages/WorkspacesPage/WorkspacesPageView.tsx Co-authored-by: Kira Pilot --- site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 01e0b7192b6a7..7d0477d488811 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -60,7 +60,7 @@ export const Language = { outdatedLabel: "Outdated", upToDateLabel: "Up to date", versionTooltipText: - "Looks like the version you are using for this workspace is outdated and there is a newest version that you could use.", + "This workspace version is outdated and a newer version is available.", updateVersionLabel: "Update version", } From e6145bdc151a682fbd850ea0aa732b73c536e0e1 Mon Sep 17 00:00:00 2001 From: Bruno Date: Wed, 15 Jun 2022 13:25:42 +0000 Subject: [PATCH 7/7] Fix formatting and add more context --- site/src/pages/WorkspacesPage/WorkspacesPageView.tsx | 3 +-- site/src/xServices/workspaces/workspacesXService.ts | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx index 7d0477d488811..d09b7b47ac73c 100644 --- a/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx +++ b/site/src/pages/WorkspacesPage/WorkspacesPageView.tsx @@ -59,8 +59,7 @@ export const Language = { workspaceTooltipLink3: "Editors and IDEs", outdatedLabel: "Outdated", upToDateLabel: "Up to date", - versionTooltipText: - "This workspace version is outdated and a newer version is available.", + versionTooltipText: "This workspace version is outdated and a newer version is available.", updateVersionLabel: "Update version", } diff --git a/site/src/xServices/workspaces/workspacesXService.ts b/site/src/xServices/workspaces/workspacesXService.ts index 6bfa7758b79d3..1fff46c0341e2 100644 --- a/site/src/xServices/workspaces/workspacesXService.ts +++ b/site/src/xServices/workspaces/workspacesXService.ts @@ -275,6 +275,7 @@ export const workspacesMachine = createMachine( workspaceRef.send("UPDATE_VERSION") }, + // Opened discussion on XState https://github.com/statelyai/xstate/discussions/3406 updateWorkspaceRefs: assign({ workspaceRefs: (context, event) => { let workspaceRefs = context.workspaceRefs