diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx
index d979a37f4d616..235061621f711 100644
--- a/site/src/AppRouter.tsx
+++ b/site/src/AppRouter.tsx
@@ -17,6 +17,7 @@ import { RequireAuth } from "./components/RequireAuth/RequireAuth"
import { SettingsLayout } from "./components/SettingsLayout/SettingsLayout"
import { DeploySettingsLayout } from "components/DeploySettingsLayout/DeploySettingsLayout"
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"
+import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"
// Lazy load pages
// - Pages that are secondary, not in the main navigation or not usually accessed
@@ -45,7 +46,10 @@ const WorkspaceBuildPage = lazy(
)
const WorkspacePage = lazy(() => import("./pages/WorkspacePage/WorkspacePage"))
const WorkspaceSchedulePage = lazy(
- () => import("./pages/WorkspaceSchedulePage/WorkspaceSchedulePage"),
+ () =>
+ import(
+ "./pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage"
+ ),
)
const TerminalPage = lazy(() => import("./pages/TerminalPage/TerminalPage"))
const TemplatePermissionsPage = lazy(
@@ -260,12 +264,17 @@ export const AppRouter: FC = () => {
} />
- } />
}
/>
- } />
+ }>
+ } />
+ }
+ />
+
diff --git a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx
index 082b555318bda..d9ed3bcc37562 100644
--- a/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx
+++ b/site/src/components/WorkspaceSchedule/WorkspaceSchedule.tsx
@@ -74,7 +74,7 @@ export const WorkspaceSchedule: FC<
{Language.editScheduleLink}
diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx
index 3a43c8edce029..47972cef3110e 100644
--- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx
+++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.stories.tsx
@@ -6,8 +6,8 @@ import utc from "dayjs/plugin/utc"
import {
defaultSchedule,
emptySchedule,
-} from "pages/WorkspaceSchedulePage/schedule"
-import { emptyTTL } from "pages/WorkspaceSchedulePage/ttl"
+} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
+import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
import { makeMockApiError } from "testHelpers/entities"
import {
WorkspaceScheduleForm,
diff --git a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx
index ed3fad4d5cdb9..0ede4239f0a72 100644
--- a/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx
+++ b/site/src/components/WorkspaceScheduleForm/WorkspaceScheduleForm.tsx
@@ -8,8 +8,13 @@ import MenuItem from "@material-ui/core/MenuItem"
import makeStyles from "@material-ui/core/styles/makeStyles"
import Switch from "@material-ui/core/Switch"
import TextField from "@material-ui/core/TextField"
-import { AlertBanner } from "components/AlertBanner/AlertBanner"
-import { Section } from "components/Section/Section"
+import {
+ HorizontalForm,
+ FormFooter,
+ FormSection,
+ FormFields,
+} from "components/Form/Form"
+import { Stack } from "components/Stack/Stack"
import dayjs from "dayjs"
import advancedFormat from "dayjs/plugin/advancedFormat"
import duration from "dayjs/plugin/duration"
@@ -20,13 +25,10 @@ import { FormikTouched, useFormik } from "formik"
import {
defaultSchedule,
emptySchedule,
-} from "pages/WorkspaceSchedulePage/schedule"
+} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
import { ChangeEvent, FC } from "react"
import * as Yup from "yup"
import { getFormHelpers } from "../../util/formUtils"
-import { FormFooter } from "../FormFooter/FormFooter"
-import { FullPageForm } from "../FullPageForm/FullPageForm"
-import { Stack } from "../Stack/Stack"
import { zones } from "./zones"
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
@@ -48,16 +50,14 @@ export const Language = {
errorTtlMax:
"Please enter a limit that is less than or equal to 168 hours (7 days).",
daysOfWeekLabel: "Days of Week",
- daySundayLabel: "Sunday",
- dayMondayLabel: "Monday",
- dayTuesdayLabel: "Tuesday",
- dayWednesdayLabel: "Wednesday",
- dayThursdayLabel: "Thursday",
- dayFridayLabel: "Friday",
- daySaturdayLabel: "Saturday",
+ daySundayLabel: "Sun",
+ dayMondayLabel: "Mon",
+ dayTuesdayLabel: "Tue",
+ dayWednesdayLabel: "Wed",
+ dayThursdayLabel: "Thu",
+ dayFridayLabel: "Fri",
+ daySaturdayLabel: "Sat",
startTimeLabel: "Start time",
- startTimeHelperText: "Your workspace will automatically start at this time.",
- noStartTimeHelperText: "Your workspace will not automatically start.",
timezoneLabel: "Timezone",
ttlLabel: "Time until shutdown (hours)",
ttlCausesShutdownHelperText: "Your workspace will shut down",
@@ -67,13 +67,13 @@ export const Language = {
"Your workspace will not automatically shut down.",
formTitle: "Workspace schedule",
startSection: "Start",
- startSwitch: "Autostart",
+ startSwitch: "Enable Autostart",
stopSection: "Stop",
- stopSwitch: "Autostop",
+ stopSwitch: "Enable Autostop",
}
export interface WorkspaceScheduleFormProps {
- submitScheduleError?: Error | unknown
+ submitScheduleError?: unknown
initialValues: WorkspaceScheduleFormValues
isLoading: boolean
onCancel: () => void
@@ -280,31 +280,26 @@ export const WorkspaceScheduleForm: FC<
}
return (
-
-
-
+
+
+
+ }
+ label={Language.stopSwitch}
+ />
+
+
+
+
+
)
}
@@ -405,13 +400,14 @@ export const ttlShutdownAt = (formTTL: number): string => {
}
}
-const useStyles = makeStyles({
- form: {
- "& input": {
- colorScheme: "dark",
- },
- },
+const useStyles = makeStyles((theme) => ({
daysOfWeekLabel: {
fontSize: 12,
},
-})
+ daysOfWeekOptions: {
+ display: "flex",
+ flexDirection: "row",
+ flexWrap: "wrap",
+ paddingTop: theme.spacing(0.5),
+ },
+}))
diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
deleted file mode 100644
index ed1150c0c74bb..0000000000000
--- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import { makeStyles } from "@material-ui/core/styles"
-import { useMachine } from "@xstate/react"
-import { AlertBanner } from "components/AlertBanner/AlertBanner"
-import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
-import { Loader } from "components/Loader/Loader"
-import { Margins } from "components/Margins/Margins"
-import dayjs from "dayjs"
-import { scheduleToAutostart } from "pages/WorkspaceSchedulePage/schedule"
-import { ttlMsToAutostop } from "pages/WorkspaceSchedulePage/ttl"
-import { useEffect, FC } from "react"
-import { useTranslation } from "react-i18next"
-import { Navigate, useNavigate, useParams } from "react-router-dom"
-import { scheduleChanged } from "util/schedule"
-import * as TypesGen from "../../api/typesGenerated"
-import { WorkspaceScheduleForm } from "../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
-import { firstOrItem } from "../../util/array"
-import { workspaceSchedule } from "../../xServices/workspaceSchedule/workspaceScheduleXService"
-import {
- formValuesToAutostartRequest,
- formValuesToTTLRequest,
-} from "./formToRequest"
-
-const getAutostart = (workspace?: TypesGen.Workspace) =>
- scheduleToAutostart(workspace?.autostart_schedule)
-const getAutostop = (workspace?: TypesGen.Workspace) =>
- ttlMsToAutostop(workspace?.ttl_ms)
-
-const useStyles = makeStyles((theme) => ({
- topMargin: {
- marginTop: `${theme.spacing(3)}px`,
- },
-}))
-
-export const WorkspaceSchedulePage: FC = () => {
- const { t } = useTranslation("workspaceSchedulePage")
- const styles = useStyles()
- const { username: usernameQueryParam, workspace: workspaceQueryParam } =
- useParams()
- const navigate = useNavigate()
- const username = firstOrItem(usernameQueryParam, null)
- const workspaceName = firstOrItem(workspaceQueryParam, null)
- const [scheduleState, scheduleSend] = useMachine(workspaceSchedule)
- const {
- checkPermissionsError,
- submitScheduleError,
- getWorkspaceError,
- getTemplateError,
- permissions,
- workspace,
- template,
- } = scheduleState.context
-
- // Get workspace on mount and whenever the args for getting a workspace change.
- // scheduleSend should not change.
- useEffect(() => {
- username &&
- workspaceName &&
- scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })
- }, [username, workspaceName, scheduleSend])
-
- if (!username || !workspaceName) {
- return
- }
-
- if (scheduleState.hasTag("loading") || !template) {
- return
- }
-
- if (scheduleState.matches("error")) {
- return (
-
-
-
- scheduleSend({ type: "GET_WORKSPACE", username, workspaceName })
- }
- />
-
-
- )
- }
-
- if (!permissions?.updateWorkspace) {
- return (
-
-
-
- )
- }
-
- if (
- scheduleState.matches("presentForm") ||
- scheduleState.matches("submittingSchedule")
- ) {
- return (
- {
- navigate(`/@${username}/${workspaceName}`)
- }}
- onSubmit={(values) => {
- scheduleSend({
- type: "SUBMIT_SCHEDULE",
- autostart: formValuesToAutostartRequest(values),
- ttl: formValuesToTTLRequest(values),
- autostartChanged: scheduleChanged(getAutostart(workspace), values),
- autostopChanged: scheduleChanged(getAutostop(workspace), values),
- })
- }}
- />
- )
- }
-
- if (scheduleState.matches("showingRestartDialog")) {
- return (
- {
- scheduleSend("RESTART_WORKSPACE")
- }}
- onClose={() => {
- scheduleSend("APPLY_LATER")
- }}
- />
- )
- }
-
- if (scheduleState.matches("done")) {
- return
- }
-
- // Theoretically impossible - log and bail
- console.error("WorkspaceSchedulePage: unknown state :: ", scheduleState)
- return
-}
-
-export default WorkspaceSchedulePage
diff --git a/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx
new file mode 100644
index 0000000000000..328b0abc24632
--- /dev/null
+++ b/site/src/pages/WorkspaceSettingsPage/Sidebar.tsx
@@ -0,0 +1,138 @@
+import { makeStyles } from "@material-ui/core/styles"
+import ScheduleIcon from "@material-ui/icons/TimerOutlined"
+import { Workspace } from "api/typesGenerated"
+import { Stack } from "components/Stack/Stack"
+import { FC, ElementType, PropsWithChildren, ReactNode } from "react"
+import { Link, NavLink } from "react-router-dom"
+import { combineClasses } from "util/combineClasses"
+import GeneralIcon from "@material-ui/icons/SettingsOutlined"
+import { Avatar } from "components/Avatar/Avatar"
+
+const SidebarNavItem: FC<
+ PropsWithChildren<{ href: string; icon: ReactNode }>
+> = ({ children, href, icon }) => {
+ const styles = useStyles()
+ return (
+
+ combineClasses([
+ styles.sidebarNavItem,
+ isActive ? styles.sidebarNavItemActive : undefined,
+ ])
+ }
+ >
+
+ {icon}
+ {children}
+
+
+ )
+}
+
+const SidebarNavItemIcon: React.FC<{ icon: ElementType }> = ({
+ icon: Icon,
+}) => {
+ const styles = useStyles()
+ return
+}
+
+export const Sidebar: React.FC<{ username: string; workspace: Workspace }> = ({
+ username,
+ workspace,
+}) => {
+ const styles = useStyles()
+
+ return (
+
+ )
+}
+
+const useStyles = makeStyles((theme) => ({
+ sidebar: {
+ width: 245,
+ flexShrink: 0,
+ },
+ sidebarNavItem: {
+ color: "inherit",
+ display: "block",
+ fontSize: 14,
+ textDecoration: "none",
+ padding: theme.spacing(1.5, 1.5, 1.5, 2),
+ borderRadius: theme.shape.borderRadius / 2,
+ transition: "background-color 0.15s ease-in-out",
+ marginBottom: 1,
+ position: "relative",
+
+ "&:hover": {
+ backgroundColor: theme.palette.action.hover,
+ },
+ },
+ sidebarNavItemActive: {
+ backgroundColor: theme.palette.action.hover,
+
+ "&:before": {
+ content: '""',
+ display: "block",
+ width: 3,
+ height: "100%",
+ position: "absolute",
+ left: 0,
+ top: 0,
+ backgroundColor: theme.palette.secondary.dark,
+ borderTopLeftRadius: theme.shape.borderRadius,
+ borderBottomLeftRadius: theme.shape.borderRadius,
+ },
+ },
+ sidebarNavItemIcon: {
+ width: theme.spacing(2),
+ height: theme.spacing(2),
+ },
+ workspaceInfo: {
+ marginBottom: theme.spacing(2),
+ },
+ workspaceData: {
+ overflow: "hidden",
+ },
+ name: {
+ fontWeight: 600,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ whiteSpace: "nowrap",
+ color: theme.palette.text.primary,
+ textDecoration: "none",
+ },
+ secondary: {
+ color: theme.palette.text.secondary,
+ fontSize: 12,
+ overflow: "hidden",
+ textOverflow: "ellipsis",
+ },
+}))
diff --git a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx
similarity index 93%
rename from site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx
rename to site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx
index d9a8af829dc1d..5e4b36a02ce9e 100644
--- a/site/src/pages/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.test.tsx
@@ -1,16 +1,19 @@
-import { renderWithAuth } from "testHelpers/renderHelpers"
+import { renderWithWorkspaceSettingsLayout } from "testHelpers/renderHelpers"
import userEvent from "@testing-library/user-event"
import { screen } from "@testing-library/react"
import {
formValuesToAutostartRequest,
formValuesToTTLRequest,
-} from "pages/WorkspaceSchedulePage/formToRequest"
+} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest"
import {
Autostart,
scheduleToAutostart,
-} from "pages/WorkspaceSchedulePage/schedule"
-import { Autostop, ttlMsToAutostop } from "pages/WorkspaceSchedulePage/ttl"
-import * as TypesGen from "../../api/typesGenerated"
+} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
+import {
+ Autostop,
+ ttlMsToAutostop,
+} from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
+import * as TypesGen from "../../../api/typesGenerated"
import {
WorkspaceScheduleFormValues,
Language as FormLanguage,
@@ -257,7 +260,7 @@ describe("WorkspaceSchedulePage", () => {
describe("autostop change dialog", () => {
it("shows if autostop is changed", async () => {
- renderWithAuth(, {
+ renderWithWorkspaceSettingsLayout(, {
route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`,
path: "/@:username/:workspace/schedule",
})
@@ -276,7 +279,7 @@ describe("WorkspaceSchedulePage", () => {
})
it("doesn't show if autostop is not changed", async () => {
- renderWithAuth(, {
+ renderWithWorkspaceSettingsLayout(, {
route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`,
path: "/@:username/:workspace/schedule",
})
@@ -309,7 +312,7 @@ describe("WorkspaceSchedulePage", () => {
},
),
)
- renderWithAuth(, {
+ renderWithWorkspaceSettingsLayout(, {
route: `/@${MockUser.username}/${MockWorkspace.name}/schedule`,
path: "/@:username/:workspace/schedule",
})
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
new file mode 100644
index 0000000000000..abd49bc217848
--- /dev/null
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/WorkspaceSchedulePage.tsx
@@ -0,0 +1,136 @@
+import { makeStyles } from "@material-ui/core/styles"
+import { useMachine } from "@xstate/react"
+import { AlertBanner } from "components/AlertBanner/AlertBanner"
+import { ConfirmDialog } from "components/Dialogs/ConfirmDialog/ConfirmDialog"
+import { Loader } from "components/Loader/Loader"
+import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
+import dayjs from "dayjs"
+import { scheduleToAutostart } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
+import { ttlMsToAutostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
+import { useWorkspaceSettingsContext } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"
+import { FC } from "react"
+import { Helmet } from "react-helmet-async"
+import { useTranslation } from "react-i18next"
+import { Navigate, useNavigate, useParams } from "react-router-dom"
+import { pageTitle } from "util/page"
+import { scheduleChanged } from "util/schedule"
+import * as TypesGen from "../../../api/typesGenerated"
+import { WorkspaceScheduleForm } from "../../../components/WorkspaceScheduleForm/WorkspaceScheduleForm"
+import { firstOrItem } from "../../../util/array"
+import { workspaceSchedule } from "../../../xServices/workspaceSchedule/workspaceScheduleXService"
+import {
+ formValuesToAutostartRequest,
+ formValuesToTTLRequest,
+} from "./formToRequest"
+
+const getAutostart = (workspace: TypesGen.Workspace) =>
+ scheduleToAutostart(workspace.autostart_schedule)
+const getAutostop = (workspace: TypesGen.Workspace) =>
+ ttlMsToAutostop(workspace.ttl_ms)
+
+const useStyles = makeStyles((theme) => ({
+ topMargin: {
+ marginTop: `${theme.spacing(3)}px`,
+ },
+ pageHeader: {
+ paddingTop: 0,
+ },
+}))
+
+export const WorkspaceSchedulePage: FC = () => {
+ const { t } = useTranslation("workspaceSchedulePage")
+ const styles = useStyles()
+ const { username: usernameQueryParam, workspace: workspaceQueryParam } =
+ useParams()
+ const navigate = useNavigate()
+ const username = firstOrItem(usernameQueryParam, null)
+ const workspaceName = firstOrItem(workspaceQueryParam, null)
+ const { workspace } = useWorkspaceSettingsContext()
+ const [scheduleState, scheduleSend] = useMachine(workspaceSchedule, {
+ context: { workspace },
+ })
+ const {
+ checkPermissionsError,
+ submitScheduleError,
+ getTemplateError,
+ permissions,
+ template,
+ } = scheduleState.context
+
+ if (!username || !workspaceName) {
+ return
+ }
+
+ if (scheduleState.matches("done")) {
+ return
+ }
+
+ return (
+ <>
+
+ {pageTitle([workspaceName, "Schedule"])}
+
+
+ Workspace Schedule
+
+ {(scheduleState.hasTag("loading") || !template) && }
+ {scheduleState.matches("error") && (
+
+ )}
+ {permissions && !permissions.updateWorkspace && (
+
+ )}
+ {template &&
+ workspace &&
+ (scheduleState.matches("presentForm") ||
+ scheduleState.matches("submittingSchedule")) && (
+ {
+ navigate(`/@${username}/${workspaceName}`)
+ }}
+ onSubmit={(values) => {
+ scheduleSend({
+ type: "SUBMIT_SCHEDULE",
+ autostart: formValuesToAutostartRequest(values),
+ ttl: formValuesToTTLRequest(values),
+ autostartChanged: scheduleChanged(
+ getAutostart(workspace),
+ values,
+ ),
+ autostopChanged: scheduleChanged(
+ getAutostop(workspace),
+ values,
+ ),
+ })
+ }}
+ />
+ )}
+ {
+ scheduleSend("RESTART_WORKSPACE")
+ }}
+ onClose={() => {
+ scheduleSend("APPLY_LATER")
+ }}
+ />
+ >
+ )
+}
+
+export default WorkspaceSchedulePage
diff --git a/site/src/pages/WorkspaceSchedulePage/formToRequest.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest.ts
similarity index 100%
rename from site/src/pages/WorkspaceSchedulePage/formToRequest.ts
rename to site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/formToRequest.ts
diff --git a/site/src/pages/WorkspaceSchedulePage/schedule.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.ts
similarity index 96%
rename from site/src/pages/WorkspaceSchedulePage/schedule.ts
rename to site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.ts
index 7925b44bd5b9c..7c8ff365969c8 100644
--- a/site/src/pages/WorkspaceSchedulePage/schedule.ts
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule.ts
@@ -2,7 +2,7 @@ import * as cronParser from "cron-parser"
import dayjs from "dayjs"
import timezone from "dayjs/plugin/timezone"
import utc from "dayjs/plugin/utc"
-import { extractTimezone, stripTimezone } from "../../util/schedule"
+import { extractTimezone, stripTimezone } from "../../../util/schedule"
// REMARK: timezone plugin depends on UTC
//
diff --git a/site/src/pages/WorkspaceSchedulePage/ttl.ts b/site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl.ts
similarity index 100%
rename from site/src/pages/WorkspaceSchedulePage/ttl.ts
rename to site/src/pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl.ts
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx
new file mode 100644
index 0000000000000..c1582fe564519
--- /dev/null
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsLayout.tsx
@@ -0,0 +1,86 @@
+import { makeStyles } from "@material-ui/core/styles"
+import { Sidebar } from "./Sidebar"
+import { Stack } from "components/Stack/Stack"
+import { createContext, FC, Suspense, useContext } from "react"
+import { Helmet } from "react-helmet-async"
+import { pageTitle } from "../../util/page"
+import { Loader } from "components/Loader/Loader"
+import { Outlet, useParams } from "react-router-dom"
+import { Margins } from "components/Margins/Margins"
+import { getWorkspaceByOwnerAndName } from "api/api"
+import { useQuery } from "@tanstack/react-query"
+
+const fetchWorkspaceSettings = async (owner: string, name: string) => {
+ const workspace = await getWorkspaceByOwnerAndName(owner, name)
+
+ return {
+ workspace,
+ }
+}
+
+const useWorkspace = (owner: string, name: string) => {
+ return useQuery({
+ queryKey: ["workspace", name, "settings"],
+ queryFn: () => fetchWorkspaceSettings(owner, name),
+ })
+}
+
+const WorkspaceSettingsContext = createContext<
+ Awaited> | undefined
+>(undefined)
+
+export const useWorkspaceSettingsContext = () => {
+ const context = useContext(WorkspaceSettingsContext)
+
+ if (!context) {
+ throw new Error(
+ "useWorkspaceSettingsContext must be used within a WorkspaceSettingsContext.Provider",
+ )
+ }
+
+ return context
+}
+
+export const WorkspaceSettingsLayout: FC = () => {
+ const styles = useStyles()
+ const { workspace: workspaceName, username } = useParams() as {
+ workspace: string
+ username: string
+ }
+ const { data: settings } = useWorkspace(username, workspaceName)
+
+ return (
+ <>
+
+ {pageTitle([workspaceName, "Settings"])}
+
+
+ {settings ? (
+
+
+
+
+ }>
+
+
+
+
+
+
+
+ ) : (
+
+ )}
+ >
+ )
+}
+
+const useStyles = makeStyles((theme) => ({
+ wrapper: {
+ padding: theme.spacing(6, 0),
+ },
+
+ content: {
+ width: "100%",
+ },
+}))
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
index a2ca353ec3374..39c48363fbf63 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.test.tsx
@@ -1,6 +1,6 @@
import userEvent from "@testing-library/user-event"
import {
- renderWithAuth,
+ renderWithWorkspaceSettingsLayout,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers"
import WorkspaceSettingsPage from "./WorkspaceSettingsPage"
@@ -41,7 +41,7 @@ test("Submit the workspace settings page successfully", async () => {
.mockResolvedValue(MockWorkspaceBuild)
// Setup event and rendering
const user = userEvent.setup()
- renderWithAuth(, {
+ renderWithWorkspaceSettingsLayout(, {
route: "/@test-user/test-workspace/settings",
path: "/@:username/:workspace/settings",
// Need this because after submit the user is redirected
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
index 87d8cff54e5e3..5f4ba99c3f529 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPage.tsx
@@ -5,6 +5,7 @@ import { useTranslation } from "react-i18next"
import { useNavigate, useParams } from "react-router-dom"
import { pageTitle } from "util/page"
import { useUpdateWorkspaceSettings, useWorkspaceSettings } from "./data"
+import { useWorkspaceSettingsContext } from "./WorkspaceSettingsLayout"
import { WorkspaceSettingsPageView } from "./WorkspaceSettingsPageView"
const WorkspaceSettingsPage = () => {
@@ -13,13 +14,10 @@ const WorkspaceSettingsPage = () => {
username: string
workspace: string
}
- const {
- data: settings,
- error,
- isLoading,
- } = useWorkspaceSettings(username, workspaceName)
+ const { workspace } = useWorkspaceSettingsContext()
+ const { data: settings, error, isLoading } = useWorkspaceSettings(workspace)
const navigate = useNavigate()
- const updateSettings = useUpdateWorkspaceSettings(settings?.workspace.id, {
+ const updateSettings = useUpdateWorkspaceSettings(workspace.id, {
onSuccess: ({ name }) => {
navigate(`/@${username}/${name}`)
},
@@ -30,7 +28,7 @@ const WorkspaceSettingsPage = () => {
return (
<>
- {pageTitle(t("title"))}
+ {pageTitle([workspaceName, "Settings"])}
{
isLoading={isLoading}
isSubmitting={updateSettings.isLoading}
settings={settings}
- onCancel={() => navigate(-1)}
+ onCancel={() => navigate(`/@${username}/${workspaceName}`)}
onSubmit={updateSettings.mutate}
/>
>
diff --git a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx
index ab94200fa9f09..99bf6342ab885 100644
--- a/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx
+++ b/site/src/pages/WorkspaceSettingsPage/WorkspaceSettingsPageView.tsx
@@ -1,6 +1,7 @@
+import { makeStyles } from "@material-ui/core/styles"
import { AlertBanner } from "components/AlertBanner/AlertBanner"
-import { FullPageHorizontalForm } from "components/FullPageForm/FullPageHorizontalForm"
import { Loader } from "components/Loader/Loader"
+import { PageHeader, PageHeaderTitle } from "components/PageHeader/PageHeader"
import { FC } from "react"
import { useTranslation } from "react-i18next"
import { WorkspaceSettings, WorkspaceSettingsFormValue } from "./data"
@@ -26,22 +27,31 @@ export const WorkspaceSettingsPageView: FC = ({
loadingError,
}) => {
const { t } = useTranslation("workspaceSettingsPage")
+ const styles = useStyles()
return (
-
- <>
- {loadingError && }
- {isLoading && }
- {settings && (
-
- )}
- >
-
+ <>
+
+ {t("title")}
+
+
+ {loadingError && }
+ {isLoading && }
+ {settings && (
+
+ )}
+ >
)
}
+
+const useStyles = makeStyles(() => ({
+ pageHeader: {
+ paddingTop: 0,
+ },
+}))
diff --git a/site/src/pages/WorkspaceSettingsPage/data.ts b/site/src/pages/WorkspaceSettingsPage/data.ts
index cf64abc9a189e..46e72b5b33616 100644
--- a/site/src/pages/WorkspaceSettingsPage/data.ts
+++ b/site/src/pages/WorkspaceSettingsPage/data.ts
@@ -1,15 +1,13 @@
import { useMutation, useQuery } from "@tanstack/react-query"
import {
- getWorkspaceByOwnerAndName,
getWorkspaceBuildParameters,
getTemplateVersionRichParameters,
patchWorkspace,
postWorkspaceBuild,
} from "api/api"
-import { WorkspaceBuildParameter } from "api/typesGenerated"
+import { Workspace, WorkspaceBuildParameter } from "api/typesGenerated"
-const getWorkspaceSettings = async (owner: string, name: string) => {
- const workspace = await getWorkspaceByOwnerAndName(owner, name)
+const getWorkspaceSettings = async (workspace: Workspace) => {
const latestBuild = workspace.latest_build
const [templateVersionRichParameters, buildParameters] = await Promise.all([
getTemplateVersionRichParameters(latestBuild.template_version_id),
@@ -22,10 +20,10 @@ const getWorkspaceSettings = async (owner: string, name: string) => {
}
}
-export const useWorkspaceSettings = (owner: string, workspace: string) => {
+export const useWorkspaceSettings = (workspace: Workspace) => {
return useQuery({
- queryKey: ["workspaceSettings", owner, workspace],
- queryFn: () => getWorkspaceSettings(owner, workspace),
+ queryKey: ["workspaceSettings", workspace.id],
+ queryFn: () => getWorkspaceSettings(workspace),
})
}
diff --git a/site/src/testHelpers/renderHelpers.tsx b/site/src/testHelpers/renderHelpers.tsx
index 51287b0118b16..d7e1cc728468a 100644
--- a/site/src/testHelpers/renderHelpers.tsx
+++ b/site/src/testHelpers/renderHelpers.tsx
@@ -9,6 +9,7 @@ import { DashboardLayout } from "components/Dashboard/DashboardLayout"
import { createMemoryHistory } from "history"
import { i18n } from "i18n"
import { TemplateSettingsLayout } from "pages/TemplateSettingsPage/TemplateSettingsLayout"
+import { WorkspaceSettingsLayout } from "pages/WorkspaceSettingsPage/WorkspaceSettingsLayout"
import { FC, ReactElement } from "react"
import { I18nextProvider } from "react-i18next"
import {
@@ -131,6 +132,50 @@ export function renderWithTemplateSettingsLayout(
}
}
+export function renderWithWorkspaceSettingsLayout(
+ element: JSX.Element,
+ {
+ path = "/",
+ route = "/",
+ extraRoutes = [],
+ nonAuthenticatedRoutes = [],
+ }: RenderWithAuthOptions = {},
+) {
+ const routes: RouteObject[] = [
+ {
+ element: ,
+ children: [
+ {
+ element: ,
+ children: [
+ {
+ element: ,
+ children: [{ path, element }, ...extraRoutes],
+ },
+ ],
+ },
+ ],
+ },
+ ...nonAuthenticatedRoutes,
+ ]
+
+ const router = createMemoryRouter(routes, { initialEntries: [route] })
+
+ const renderResult = wrappedRender(
+
+
+
+
+ ,
+ )
+
+ return {
+ user: MockUser,
+ router,
+ ...renderResult,
+ }
+}
+
export const waitForLoaderToBeRemoved = (): Promise =>
// Sometimes, we have pages that are doing a lot of requests to get done, so the
// default timeout of 1_000 is not enough. We should revisit this when we unify
diff --git a/site/src/util/schedule.test.ts b/site/src/util/schedule.test.ts
index 87c31935dee81..29d8aec1912f3 100644
--- a/site/src/util/schedule.test.ts
+++ b/site/src/util/schedule.test.ts
@@ -1,7 +1,7 @@
import dayjs from "dayjs"
import duration from "dayjs/plugin/duration"
-import { emptySchedule } from "pages/WorkspaceSchedulePage/schedule"
-import { emptyTTL } from "pages/WorkspaceSchedulePage/ttl"
+import { emptySchedule } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
+import { emptyTTL } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
import { Workspace } from "../api/typesGenerated"
import * as Mocks from "../testHelpers/entities"
import {
diff --git a/site/src/util/schedule.ts b/site/src/util/schedule.ts
index 494183ce67f1d..02e37516389b9 100644
--- a/site/src/util/schedule.ts
+++ b/site/src/util/schedule.ts
@@ -10,8 +10,8 @@ import utc from "dayjs/plugin/utc"
import { Workspace } from "../api/typesGenerated"
import { isWorkspaceOn } from "./workspace"
import { WorkspaceScheduleFormValues } from "components/WorkspaceScheduleForm/WorkspaceScheduleForm"
-import { Autostop } from "pages/WorkspaceSchedulePage/ttl"
-import { Autostart } from "pages/WorkspaceSchedulePage/schedule"
+import { Autostop } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/ttl"
+import { Autostart } from "pages/WorkspaceSettingsPage/WorkspaceSchedulePage/schedule"
// REMARK: some plugins depend on utc, so it's listed first. Otherwise they're
// sorted alphabetically.
diff --git a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts
index a45eed1ec139b..21fcc3141e7bf 100644
--- a/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts
+++ b/site/src/xServices/workspaceSchedule/workspaceScheduleXService.ts
@@ -15,7 +15,7 @@ export interface WorkspaceScheduleContext {
* re-fetch the workspace to ensure we're up-to-date. As a result, this
* machine is partially influenced by workspaceXService.
*/
- workspace?: TypesGen.Workspace
+ workspace: TypesGen.Workspace
template?: TypesGen.Template
getTemplateError?: Error | unknown
permissions?: Permissions
@@ -41,7 +41,6 @@ const permissionsToCheck = (workspace: TypesGen.Workspace) => ({
})
export type WorkspaceScheduleEvent =
- | { type: "GET_WORKSPACE"; username: string; workspaceName: string }
| {
type: "SUBMIT_SCHEDULE"
autostart: TypesGen.UpdateWorkspaceAutostartRequest
@@ -63,38 +62,13 @@ export const workspaceSchedule =
context: {} as WorkspaceScheduleContext,
events: {} as WorkspaceScheduleEvent,
services: {} as {
- getWorkspace: {
- data: TypesGen.Workspace
- }
getTemplate: {
data: TypesGen.Template
}
},
},
- initial: "idle",
- on: {
- GET_WORKSPACE: "gettingWorkspace",
- },
+ initial: "gettingPermissions",
states: {
- idle: {
- tags: "loading",
- },
- gettingWorkspace: {
- entry: ["clearGetWorkspaceError", "clearContext"],
- invoke: {
- src: "getWorkspace",
- id: "getWorkspace",
- onDone: {
- target: "gettingPermissions",
- actions: ["assignWorkspace"],
- },
- onError: {
- target: "error",
- actions: ["assignGetWorkspaceError"],
- },
- },
- tags: "loading",
- },
gettingPermissions: {
entry: "clearGetPermissionsError",
invoke: {
@@ -167,9 +141,7 @@ export const workspaceSchedule =
},
},
error: {
- on: {
- GET_WORKSPACE: "gettingWorkspace",
- },
+ type: "final",
},
done: {
type: "final",
@@ -184,12 +156,6 @@ export const workspaceSchedule =
assignSubmissionError: assign({
submitScheduleError: (_, event) => event.data,
}),
- assignWorkspace: assign({
- workspace: (_, event) => event.data,
- }),
- assignGetWorkspaceError: assign({
- getWorkspaceError: (_, event) => event.data,
- }),
assignPermissions: assign({
// Setting event.data as Permissions to be more stricted. So we know
// what permissions we asked for.
@@ -213,12 +179,7 @@ export const workspaceSchedule =
clearGetPermissionsError: assign({
checkPermissionsError: (_) => undefined,
}),
- clearContext: () => {
- assign({ workspace: undefined, permissions: undefined })
- },
- clearGetWorkspaceError: (context) => {
- assign({ ...context, getWorkspaceError: undefined })
- },
+
// action instead of service because we fire and forget so that the
// user can return to the workspace page to see the restart
restartWorkspace: (context) => {
@@ -232,12 +193,6 @@ export const workspaceSchedule =
},
services: {
- getWorkspace: async (_, event) => {
- return await API.getWorkspaceByOwnerAndName(
- event.username,
- event.workspaceName,
- )
- },
getTemplate: async (context) => {
if (context.workspace) {
return await API.getTemplate(context.workspace.template_id)