diff --git a/codersdk/client.go b/codersdk/client.go index b0fb4d9764b3c..2097225ff489c 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -354,7 +354,7 @@ func (c *Client) Dial(ctx context.Context, path string, opts *websocket.DialOpti if opts.HTTPHeader == nil { opts.HTTPHeader = http.Header{} } - if opts.HTTPHeader.Get("tokenHeader") == "" { + if opts.HTTPHeader.Get(tokenHeader) == "" { opts.HTTPHeader.Set(tokenHeader, c.SessionToken()) } diff --git a/go.mod b/go.mod index 9d445ff693885..12deb9bab3745 100644 --- a/go.mod +++ b/go.mod @@ -104,7 +104,7 @@ require ( github.com/coder/quartz v0.2.1 github.com/coder/retry v1.5.1 github.com/coder/serpent v0.10.0 - github.com/coder/terraform-provider-coder/v2 v2.7.0 + github.com/coder/terraform-provider-coder/v2 v2.8.0 github.com/coder/websocket v1.8.13 github.com/coder/wgtunnel v0.1.13-0.20240522110300-ade90dfb2da0 github.com/coreos/go-oidc/v3 v3.14.1 @@ -483,7 +483,7 @@ require ( require ( github.com/anthropics/anthropic-sdk-go v0.2.0-beta.3 github.com/coder/agentapi-sdk-go v0.0.0-20250505131810-560d1d88d225 - github.com/coder/preview v0.0.2-0.20250611164554-2e5caa65a54a + github.com/coder/preview v1.0.1 github.com/fsnotify/fsnotify v1.9.0 github.com/kylecarbs/aisdk-go v0.0.8 github.com/mark3labs/mcp-go v0.32.0 diff --git a/go.sum b/go.sum index 0ef90205bceef..7a996d81c6348 100644 --- a/go.sum +++ b/go.sum @@ -914,8 +914,8 @@ github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048 h1:3jzYUlGH7ZELIH4XggX github.com/coder/pq v1.10.5-0.20240813183442-0c420cb5a048/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0 h1:3A0ES21Ke+FxEM8CXx9n47SZOKOpgSE1bbJzlE4qPVs= github.com/coder/pretty v0.0.0-20230908205945-e89ba86370e0/go.mod h1:5UuS2Ts+nTToAMeOjNlnHFkPahrtDkmpydBen/3wgZc= -github.com/coder/preview v0.0.2-0.20250611164554-2e5caa65a54a h1:rArAOPl5zHB7lhT2sy+jfcmyLeDlm6tXDoGkGdWNq7g= -github.com/coder/preview v0.0.2-0.20250611164554-2e5caa65a54a/go.mod h1:nXz3bBwbU8/9NYI4OISUsoLDFlEREtTozYhJq6FAE8E= +github.com/coder/preview v1.0.1 h1:f6q+RjNelwnkyXfGbmVlb4dcUOQ0z4mPsb2kuQpFHuU= +github.com/coder/preview v1.0.1/go.mod h1:efDWGlO/PZPrvdt5QiDhMtTUTkPxejXo9c0wmYYLLjM= github.com/coder/quartz v0.2.1 h1:QgQ2Vc1+mvzewg2uD/nj8MJ9p9gE+QhGJm+Z+NGnrSE= github.com/coder/quartz v0.2.1/go.mod h1:vsiCc+AHViMKH2CQpGIpFgdHIEQsxwm8yCscqKmzbRA= github.com/coder/retry v1.5.1 h1:iWu8YnD8YqHs3XwqrqsjoBTAVqT9ml6z9ViJ2wlMiqc= diff --git a/site/src/components/Icons/CoderIcon.tsx b/site/src/components/Icons/CoderIcon.tsx index 7dd2a7625734d..7053ffe8d255d 100644 --- a/site/src/components/Icons/CoderIcon.tsx +++ b/site/src/components/Icons/CoderIcon.tsx @@ -1,4 +1,4 @@ -import SvgIcon, { type SvgIconProps } from "@mui/material/SvgIcon"; +import type { SvgIconProps } from "@mui/material/SvgIcon"; import type { FC } from "react"; import { cn } from "utils/cn"; @@ -7,23 +7,16 @@ import { cn } from "utils/cn"; * contain additional aspects, like the word 'Coder'. */ export const CoderIcon: FC = ({ className, ...props }) => ( - Codestin Search App - - - - - - - - - + + ); diff --git a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx index 3680b7f5e88c5..fa72142d52837 100644 --- a/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx +++ b/site/src/modules/workspaces/DynamicParameter/DynamicParameter.tsx @@ -379,11 +379,17 @@ const ParameterField: FC = ({ id, }) => { switch (parameter.form_type) { - case "dropdown": + case "dropdown": { + const EMPTY_VALUE_PLACEHOLDER = "__EMPTY_STRING__"; + const selectValue = value === "" ? EMPTY_VALUE_PLACEHOLDER : value; + const handleSelectChange = (newValue: string) => { + onChange(newValue === EMPTY_VALUE_PLACEHOLDER ? "" : newValue); + }; + return ( ); + } case "multi-select": { const parsedValues = parseStringArrayValue(value ?? ""); diff --git a/site/src/pages/TaskPage/TaskPage.stories.tsx b/site/src/pages/TaskPage/TaskPage.stories.tsx index a24968d483e38..03f8cfe739d89 100644 --- a/site/src/pages/TaskPage/TaskPage.stories.tsx +++ b/site/src/pages/TaskPage/TaskPage.stories.tsx @@ -1,5 +1,6 @@ import type { Meta, StoryObj } from "@storybook/react"; import { expect, spyOn, within } from "@storybook/test"; +import { API } from "api/api"; import type { Workspace, WorkspaceApp, @@ -9,6 +10,7 @@ import { MockFailedWorkspace, MockStartingWorkspace, MockStoppedWorkspace, + MockTemplate, MockWorkspace, MockWorkspaceAgent, MockWorkspaceApp, @@ -59,6 +61,16 @@ export const WaitingOnBuild: Story = { }, }; +export const WaitingOnBuildWithTemplate: Story = { + beforeEach: () => { + spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); + spyOn(data, "fetchTask").mockResolvedValue({ + prompt: "Create competitors page", + workspace: MockStartingWorkspace, + }); + }, +}; + export const WaitingOnStatus: Story = { beforeEach: () => { spyOn(data, "fetchTask").mockResolvedValue({ diff --git a/site/src/pages/TaskPage/TaskPage.tsx b/site/src/pages/TaskPage/TaskPage.tsx index a46e0f09c7cc9..c340a96cfef11 100644 --- a/site/src/pages/TaskPage/TaskPage.tsx +++ b/site/src/pages/TaskPage/TaskPage.tsx @@ -1,10 +1,12 @@ import { API } from "api/api"; import { getErrorDetail, getErrorMessage } from "api/errors"; +import { template as templateQueryOptions } from "api/queries/templates"; import type { Workspace, WorkspaceStatus } from "api/typesGenerated"; import { Button } from "components/Button/Button"; import { Loader } from "components/Loader/Loader"; import { Margins } from "components/Margins/Margins"; import { Spinner } from "components/Spinner/Spinner"; +import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs"; import { ArrowLeftIcon, RotateCcwIcon } from "lucide-react"; import { AI_PROMPT_PARAMETER_NAME, type Task } from "modules/tasks/tasks"; import type { ReactNode } from "react"; @@ -14,6 +16,10 @@ import { useParams } from "react-router-dom"; import { Link as RouterLink } from "react-router-dom"; import { ellipsizeText } from "utils/ellipsizeText"; import { pageTitle } from "utils/page"; +import { + ActiveTransition, + WorkspaceBuildProgress, +} from "../WorkspacePage/WorkspaceBuildProgress"; import { TaskApps } from "./TaskApps"; import { TaskSidebar } from "./TaskSidebar"; @@ -32,6 +38,19 @@ const TaskPage = () => { refetchInterval: 5_000, }); + const { data: template } = useQuery({ + ...templateQueryOptions(task?.workspace.template_id ?? ""), + enabled: Boolean(task), + }); + + const waitingStatuses: WorkspaceStatus[] = ["starting", "pending"]; + const shouldStreamBuildLogs = + task && waitingStatuses.includes(task.workspace.latest_build.status); + const buildLogs = useWorkspaceBuildLogs( + task?.workspace.latest_build.id ?? "", + shouldStreamBuildLogs, + ); + if (error) { return ( <> @@ -77,7 +96,6 @@ const TaskPage = () => { } let content: ReactNode = null; - const waitingStatuses: WorkspaceStatus[] = ["starting", "pending"]; const terminatedStatuses: WorkspaceStatus[] = [ "canceled", "canceling", @@ -88,16 +106,25 @@ const TaskPage = () => { ]; if (waitingStatuses.includes(task.workspace.latest_build.status)) { + // If no template yet, use an indeterminate progress bar. + const transition = (template && + ActiveTransition(template, task.workspace)) || { P50: 0, P95: null }; + const lastStage = + buildLogs?.[buildLogs.length - 1]?.stage || "Waiting for build status"; content = ( -
-
- +
+

Starting your workspace

- - This should take a few minutes - +
{lastStage}
+
+
+
); diff --git a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx index 715ceb136c262..306da719be0ca 100644 --- a/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx +++ b/site/src/pages/WorkspacePage/WorkspaceBuildProgress.tsx @@ -62,11 +62,18 @@ const estimateFinish = ( interface WorkspaceBuildProgressProps { workspace: Workspace; transitionStats: TransitionStats; + // variant changes how the progress bar is displayed: with the workspace + // variant the workspace transition and time remaining are displayed under the + // bar aligned to the left and right respectively. With the task variant the + // workspace transition is not displayed and the time remaining is displayed + // centered above the bar, and the bar's border radius is removed. + variant?: "workspace" | "task"; } export const WorkspaceBuildProgress: FC = ({ workspace, transitionStats, + variant, }) => { const job = workspace.latest_build.job; const [progressValue, setProgressValue] = useState(0); @@ -114,6 +121,13 @@ export const WorkspaceBuildProgress: FC = ({ } return (
+ {variant === "task" && ( +
+
+ {progressText} +
+
+ )} = ({ ? "determinate" : "indeterminate" } - // If a transition is set, there is a moment on new load where the - // bar accelerates to progressValue and then rapidly decelerates, which - // is not indicative of true progress. - classes={{ bar: classNames.bar }} + classes={{ + // If a transition is set, there is a moment on new load where the bar + // accelerates to progressValue and then rapidly decelerates, which is + // not indicative of true progress. + bar: classNames.bar, + // With the "task" variant, the progress bar is fullscreen, so remove + // the border radius. + root: variant === "task" ? classNames.root : undefined, + }} /> -
-
- {capitalize(workspace.latest_build.status)} workspace... -
-
- {progressText} + {variant !== "task" && ( +
+
+ {capitalize(workspace.latest_build.status)} workspace... +
+
+ {progressText} +
-
+ )}
); }; @@ -146,6 +167,9 @@ export const WorkspaceBuildProgress: FC = ({ const classNames = { bar: css` transition: none; + `, + root: css` + border-radius: 0; `, }; @@ -154,11 +178,6 @@ const styles = { paddingLeft: 2, paddingRight: 2, }, - barHelpers: { - display: "flex", - justifyContent: "space-between", - marginTop: 4, - }, label: (theme) => ({ fontSize: 12, display: "block", diff --git a/site/static/favicon.ico b/site/static/favicon.ico index 2e20e00e1a1dc..dfa915be64f24 100644 Binary files a/site/static/favicon.ico and b/site/static/favicon.ico differ diff --git a/site/static/favicons/favicon-dark.png b/site/static/favicons/favicon-dark.png index e71c650d80ce0..7fd4583ad0a70 100644 Binary files a/site/static/favicons/favicon-dark.png and b/site/static/favicons/favicon-dark.png differ diff --git a/site/static/favicons/favicon-dark.svg b/site/static/favicons/favicon-dark.svg index 2c1867d71575d..5be16fb2a8af4 100644 --- a/site/static/favicons/favicon-dark.svg +++ b/site/static/favicons/favicon-dark.svg @@ -1,8 +1,3 @@ - - - - - - + diff --git a/site/static/favicons/favicon-error-dark.png b/site/static/favicons/favicon-error-dark.png index bfa8e566e018d..f2fa4eb4d1d02 100644 Binary files a/site/static/favicons/favicon-error-dark.png and b/site/static/favicons/favicon-error-dark.png differ diff --git a/site/static/favicons/favicon-error-dark.svg b/site/static/favicons/favicon-error-dark.svg index 01eb0927661cb..f5784d5fbf486 100644 --- a/site/static/favicons/favicon-error-dark.svg +++ b/site/static/favicons/favicon-error-dark.svg @@ -1,4 +1,4 @@ - - + + diff --git a/site/static/favicons/favicon-error-light.png b/site/static/favicons/favicon-error-light.png index 4905ae236cc99..177454532e1ee 100644 Binary files a/site/static/favicons/favicon-error-light.png and b/site/static/favicons/favicon-error-light.png differ diff --git a/site/static/favicons/favicon-error-light.svg b/site/static/favicons/favicon-error-light.svg index 7737cfd357f97..d3627d99e8706 100644 --- a/site/static/favicons/favicon-error-light.svg +++ b/site/static/favicons/favicon-error-light.svg @@ -1,4 +1,4 @@ - - + + diff --git a/site/static/favicons/favicon-light.png b/site/static/favicons/favicon-light.png index 048d4e974ccec..05342c103d7b8 100644 Binary files a/site/static/favicons/favicon-light.png and b/site/static/favicons/favicon-light.png differ diff --git a/site/static/favicons/favicon-light.svg b/site/static/favicons/favicon-light.svg index 03949d61e9dcf..f8f77a51a8b90 100644 --- a/site/static/favicons/favicon-light.svg +++ b/site/static/favicons/favicon-light.svg @@ -1,8 +1,3 @@ - - - - - - + diff --git a/site/static/favicons/favicon-running-dark.png b/site/static/favicons/favicon-running-dark.png index 97698d87d5ed0..8f97541b588ad 100644 Binary files a/site/static/favicons/favicon-running-dark.png and b/site/static/favicons/favicon-running-dark.png differ diff --git a/site/static/favicons/favicon-running-dark.svg b/site/static/favicons/favicon-running-dark.svg index bb8e2ecaecefe..70f87b6cbcb69 100644 --- a/site/static/favicons/favicon-running-dark.svg +++ b/site/static/favicons/favicon-running-dark.svg @@ -1,4 +1,4 @@ - - + + diff --git a/site/static/favicons/favicon-running-light.png b/site/static/favicons/favicon-running-light.png index 5cd57da4d814a..32850641803e9 100644 Binary files a/site/static/favicons/favicon-running-light.png and b/site/static/favicons/favicon-running-light.png differ diff --git a/site/static/favicons/favicon-running-light.svg b/site/static/favicons/favicon-running-light.svg index 92e70c99444e9..f4149682cfe4e 100644 --- a/site/static/favicons/favicon-running-light.svg +++ b/site/static/favicons/favicon-running-light.svg @@ -1,4 +1,4 @@ - - + + diff --git a/site/static/favicons/favicon-success-dark.png b/site/static/favicons/favicon-success-dark.png index dcb8a37844400..c267c991823c3 100644 Binary files a/site/static/favicons/favicon-success-dark.png and b/site/static/favicons/favicon-success-dark.png differ diff --git a/site/static/favicons/favicon-success-dark.svg b/site/static/favicons/favicon-success-dark.svg index 7b146d31c9fe1..d86e54adab35f 100644 --- a/site/static/favicons/favicon-success-dark.svg +++ b/site/static/favicons/favicon-success-dark.svg @@ -1,4 +1,4 @@ - - + + diff --git a/site/static/favicons/favicon-success-light.png b/site/static/favicons/favicon-success-light.png index 45ee20c2ef059..b4d4a46af94b1 100644 Binary files a/site/static/favicons/favicon-success-light.png and b/site/static/favicons/favicon-success-light.png differ diff --git a/site/static/favicons/favicon-success-light.svg b/site/static/favicons/favicon-success-light.svg index 45254597e3d9e..142d47a90914f 100644 --- a/site/static/favicons/favicon-success-light.svg +++ b/site/static/favicons/favicon-success-light.svg @@ -1,4 +1,4 @@ - - + + diff --git a/site/static/favicons/favicon-warning-dark.png b/site/static/favicons/favicon-warning-dark.png index e15e126acc3b1..8aed49de34072 100644 Binary files a/site/static/favicons/favicon-warning-dark.png and b/site/static/favicons/favicon-warning-dark.png differ diff --git a/site/static/favicons/favicon-warning-dark.svg b/site/static/favicons/favicon-warning-dark.svg index 18dcf60cf13e3..535c3045f9769 100644 --- a/site/static/favicons/favicon-warning-dark.svg +++ b/site/static/favicons/favicon-warning-dark.svg @@ -1,4 +1,4 @@ - - + + diff --git a/site/static/favicons/favicon-warning-light.png b/site/static/favicons/favicon-warning-light.png index 240a4629b9bad..ce6cf44ee22c4 100644 Binary files a/site/static/favicons/favicon-warning-light.png and b/site/static/favicons/favicon-warning-light.png differ diff --git a/site/static/favicons/favicon-warning-light.svg b/site/static/favicons/favicon-warning-light.svg index fa951792ee62b..1451473617db2 100644 --- a/site/static/favicons/favicon-warning-light.svg +++ b/site/static/favicons/favicon-warning-light.svg @@ -1,4 +1,4 @@ - - + + diff --git a/site/static/icon/coder.svg b/site/static/icon/coder.svg index f77e5cbb92ced..18db594d4fae5 100644 --- a/site/static/icon/coder.svg +++ b/site/static/icon/coder.svg @@ -1,8 +1,3 @@ - - - - - - - + + diff --git a/site/static/open-in-coder.svg b/site/static/open-in-coder.svg index 43b51615b68c5..933258e1b73a1 100644 --- a/site/static/open-in-coder.svg +++ b/site/static/open-in-coder.svg @@ -1,16 +1,11 @@ - - - - - - - - + + + - - - + + + diff --git a/vpn/client.go b/vpn/client.go index e3f3e767fc477..d52718e7fa7ab 100644 --- a/vpn/client.go +++ b/vpn/client.go @@ -92,7 +92,7 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string sdk.SetSessionToken(token) sdk.HTTPClient.Transport = &codersdk.HeaderTransport{ Transport: http.DefaultTransport, - Header: headers, + Header: headers.Clone(), } // New context, separate from initCtx. We don't want to cancel the @@ -129,17 +129,18 @@ func (*client) NewConn(initCtx context.Context, serverURL *url.URL, token string headers.Set(codersdk.SessionTokenHeader, token) dialer := workspacesdk.NewWebsocketDialer(options.Logger, rpcURL, &websocket.DialOptions{ HTTPClient: sdk.HTTPClient, - HTTPHeader: headers, + HTTPHeader: headers.Clone(), CompressionMode: websocket.CompressionDisabled, }, workspacesdk.WithWorkspaceUpdates(&proto.WorkspaceUpdatesRequest{ WorkspaceOwnerId: tailnet.UUIDToByteSlice(me.ID), })) + clonedHeaders := headers.Clone() ip := tailnet.CoderServicePrefix.RandomAddr() conn, err := tailnet.NewConn(&tailnet.Options{ Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)}, DERPMap: connInfo.DERPMap, - DERPHeader: &headers, + DERPHeader: &clonedHeaders, DERPForceWebSockets: connInfo.DERPForceWebSockets, Logger: options.Logger, BlockEndpoints: connInfo.DisableDirectConnections, diff --git a/vpn/client_test.go b/vpn/client_test.go index 4b05bf108e8e4..de13b2349d5d4 100644 --- a/vpn/client_test.go +++ b/vpn/client_test.go @@ -90,6 +90,8 @@ func TestClient_WorkspaceUpdates(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v2/users/me": + values := r.Header.Values(codersdk.SessionTokenHeader) + assert.Len(t, values, 1, "expected exactly one session token header value") httpapi.Write(ctx, w, http.StatusOK, codersdk.User{ ReducedUser: codersdk.ReducedUser{ MinimalUser: codersdk.MinimalUser{ @@ -101,6 +103,8 @@ func TestClient_WorkspaceUpdates(t *testing.T) { user <- struct{}{} case "/api/v2/workspaceagents/connection": + values := r.Header.Values(codersdk.SessionTokenHeader) + assert.Len(t, values, 1, "expected exactly one session token header value") httpapi.Write(ctx, w, http.StatusOK, tc.agentConnectionInfo) connInfo <- struct{}{} @@ -109,6 +113,9 @@ func TestClient_WorkspaceUpdates(t *testing.T) { cVer := r.URL.Query().Get("version") assert.Equal(t, "2.3", cVer) + values := r.Header.Values(codersdk.SessionTokenHeader) + assert.Len(t, values, 1, "expected exactly one session token header value") + sws, err := websocket.Accept(w, r, nil) if !assert.NoError(t, err) { return