Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,16 @@
connecting" problem is captured even when the server is simply unreachable and
the socket never reaches a terminal failure. The extension keeps retrying until
the server is reachable again.
- Updating a workspace whose template uses dynamic parameters works like the
dashboard: the update goes ahead when the current values are still valid, and
a form asks only for missing or invalid ones. Servers before v2.24 keep the
classic prompts.
- The update prompts show the new template version's message.

### Fixed

- A failed workspace update shows the server's error detail instead of the HTTP
status.
- OAuth sign-in now works on deployments that enforce token scopes. Users signed
in with OAuth are asked to sign in again once.
- Revoke OAuth tokens on logout even when their scopes are outdated.
Expand Down
1 change: 1 addition & 0 deletions packages/mocks/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./parameters";
export * from "./tasks";
export * from "./workspaces";
48 changes: 48 additions & 0 deletions packages/mocks/src/parameters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* Test factories for dynamic template parameter types.
*/

import type {
FriendlyDiagnostic,
PreviewParameter,
PreviewParameterOption,
} from "coder/site/src/api/typesGenerated";

export function previewParameter(
overrides: Partial<PreviewParameter> = {},
): PreviewParameter {
return {
name: "region",
display_name: "",
description: "",
type: "string",
form_type: "input",
styling: {},
mutable: true,
default_value: { valid: true, value: "" },
value: { valid: true, value: "" },
icon: "",
options: [],
validations: [],
required: false,
order: 0,
ephemeral: false,
diagnostics: [],
...overrides,
};
}

export function parameterOption(
value: string,
name = value,
description = "",
): PreviewParameterOption {
return { name, description, value: { valid: true, value }, icon: "" };
}

export function diagnostic(
summary: string,
severity: FriendlyDiagnostic["severity"] = "error",
): FriendlyDiagnostic {
return { severity, summary, detail: "", extra: { code: "" } };
}
2 changes: 2 additions & 0 deletions packages/shared/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ export type {
// Workspaces types and API
export * from "./workspaces/types";
export { WorkspacesApi } from "./workspaces/api";

export * from "./workspaceUpdate/api";
44 changes: 44 additions & 0 deletions packages/shared/src/workspaceUpdate/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { defineCommand, defineRequest } from "../ipc/protocol";

import type { DynamicParametersResponse } from "coder/site/src/api/typesGenerated";

export type ParameterValues = Readonly<Record<string, string>>;

export interface WorkspaceUpdateInit {
readonly workspaceName: string;
/** Omitted parameters use the server's value. */
readonly values: ParameterValues;
/** The server's evaluation of `values`. */
readonly evaluation: DynamicParametersResponse;
readonly restart: boolean;
}

export const WorkspaceUpdateApi = {
init: defineRequest<void, WorkspaceUpdateInit>("workspaceUpdate/init"),
evaluate: defineRequest<ParameterValues, DynamicParametersResponse>(
"workspaceUpdate/evaluate",
),
submit: defineCommand<ParameterValues>("workspaceUpdate/submit"),
cancel: defineCommand<void>("workspaceUpdate/cancel"),
} as const;

export function hasErrorDiagnostics({
parameters,
diagnostics,
}: DynamicParametersResponse): boolean {
return [...diagnostics, ...parameters.flatMap((p) => p.diagnostics)].some(
(d) => d.severity === "error",
);
}

/** Multi-select values are stored as a JSON-encoded string array. */
export function parseMultiSelectValue(raw: string): string[] | null {
try {
const parsed: unknown = JSON.parse(raw);
return Array.isArray(parsed) && parsed.every((v) => typeof v === "string")
? parsed
: null;
} catch {
return null;
}
}
7 changes: 5 additions & 2 deletions packages/ui/src/components/Field/Field.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,18 @@
}

.ui-field {
--ui-field-gap: 4px;

display: flex;
flex-direction: column;
gap: 4px;
gap: var(--ui-field-gap);
}

.ui-field__description {
color: var(--ui-description-foreground);
}

/* Attaches to the control above, like VS Code's input validation. */
.ui-field__error {
color: var(--ui-error-foreground);
margin-top: calc(-1 * var(--ui-field-gap));
}
7 changes: 6 additions & 1 deletion packages/ui/src/components/Field/Field.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useId, useState } from "react";
import { PIXEL_ALL_THEMES } from "#storybook";

import { Input } from "../Input/Input";
import { ValidationMessage } from "../ValidationMessage/ValidationMessage";

import { Field } from "./Field";

Expand Down Expand Up @@ -30,7 +31,11 @@ const FieldStates = (): React.JSX.Element => {
<Field
label="CPU cores"
htmlFor={coresId}
error="Value must be between 1 and 16."
error={
<ValidationMessage severity="error">
Value must be between 1 and 16.
</ValidationMessage>
}
errorId={`${coresId}-error`}
>
<Input
Expand Down
5 changes: 3 additions & 2 deletions packages/ui/src/components/Field/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface FieldProps extends ComponentProps<"div"> {
description?: ReactNode;
/** ID for the description; associate it with the control via aria-describedby. */
descriptionId?: string;
/** Usually one or more `ValidationMessage`s. */
error?: ReactNode;
/** ID for the error; the consumer owns aria-describedby and aria-invalid. */
errorId?: string;
Expand All @@ -22,7 +23,7 @@ export interface FieldProps extends ComponentProps<"div"> {
}

/* Lays out a labelled control like a settings-editor entry: semibold label,
control, then muted description or error text. */
muted description, control, then validation messages flush below it. */
export function Field({
label,
htmlFor,
Expand All @@ -37,12 +38,12 @@ export function Field({
return (
<div {...props} className={cx("ui-field", className)}>
{label !== undefined && <Label htmlFor={htmlFor}>{label}</Label>}
{children}
{description !== undefined && (
<div id={descriptionId} className="ui-field__description">
{description}
</div>
)}
{children}
{error !== undefined && (
<div id={errorId} className="ui-field__error">
{error}
Expand Down
4 changes: 4 additions & 0 deletions packages/ui/src/components/Select/Select.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
outline: 0;
}

.ui-select__trigger[aria-invalid="true"] {
border-color: var(--ui-input-validation-error-border);
}

.ui-select__trigger:focus {
border-color: var(--ui-focus-border);
}
Expand Down
30 changes: 30 additions & 0 deletions packages/ui/src/components/ValidationMessage/ValidationMessage.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/* Matches VS Code's .monaco-inputbox-message. */
.ui-validation-message {
box-sizing: border-box;
margin-top: -1px;
padding: 0.4em;
color: var(--ui-validation-foreground);
background: var(--ui-validation-background);
border: 1px solid var(--ui-validation-border);
font-size: 12px;
line-height: 17px;
overflow-wrap: break-word;
}

.ui-validation-message--info {
--ui-validation-foreground: var(--ui-input-validation-info-foreground);
--ui-validation-background: var(--ui-input-validation-info-background);
--ui-validation-border: var(--ui-input-validation-info-border);
}

.ui-validation-message--warning {
--ui-validation-foreground: var(--ui-input-validation-warning-foreground);
--ui-validation-background: var(--ui-input-validation-warning-background);
--ui-validation-border: var(--ui-input-validation-warning-border);
}

.ui-validation-message--error {
--ui-validation-foreground: var(--ui-input-validation-error-foreground);
--ui-validation-background: var(--ui-input-validation-error-background);
--ui-validation-border: var(--ui-input-validation-error-border);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { PIXEL_ALL_THEMES } from "#storybook";

import { Input } from "../Input/Input";

import {
ValidationMessage,
type ValidationSeverity,
} from "./ValidationMessage";

import type { Meta, StoryObj } from "@storybook/react-vite";

const MESSAGES: ReadonlyArray<{
severity: ValidationSeverity;
value: string;
message: string;
}> = [
{ severity: "info", value: "dev", message: "Names are case-sensitive." },
{
severity: "warning",
value: "4",
message: "Fewer than 8 cores may slow down builds.",
},
{
severity: "error",
value: "32",
message: "Value must be between 1 and 16.",
},
];

const Messages = (): React.JSX.Element => (
<div style={{ display: "grid", gap: "16px", width: "260px" }}>
{MESSAGES.map(({ severity, value, message }) => (
<div key={severity}>
<Input
aria-label={severity}
aria-invalid={severity === "error" || undefined}
value={value}
onChange={() => undefined}
/>
<ValidationMessage severity={severity}>{message}</ValidationMessage>
</div>
))}
</div>
);

const meta: Meta<typeof Messages> = {
title: "UI/ValidationMessage",
component: Messages,
parameters: { pixel: PIXEL_ALL_THEMES },
};
export default meta;
type Story = StoryObj<typeof Messages>;

export const Severities: Story = {};
29 changes: 29 additions & 0 deletions packages/ui/src/components/ValidationMessage/ValidationMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { type ComponentProps } from "react";

import { cx } from "#cx";

import "./ValidationMessage.css";

export type ValidationSeverity = "info" | "warning" | "error";

export interface ValidationMessageProps extends ComponentProps<"div"> {
severity: ValidationSeverity;
}

/** VS Code's input box validation message. */
export function ValidationMessage({
severity,
className,
...props
}: ValidationMessageProps): React.JSX.Element {
return (
<div
{...props}
className={cx(
"ui-validation-message",
`ui-validation-message--${severity}`,
className,
)}
/>
);
}
4 changes: 4 additions & 0 deletions packages/ui/src/components/text-control.css
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
height: 26px;
}

.ui-text-control:is([aria-invalid="true"], :has(> [aria-invalid="true"])) {
border-color: var(--ui-input-validation-error-border);
}

.ui-text-control:focus-within {
border-color: var(--ui-focus-border);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ export {
type StatusPillTone,
} from "./components/StatusPill/StatusPill";
export { Textarea, type TextareaProps } from "./components/Textarea/Textarea";
export {
ValidationMessage,
type ValidationMessageProps,
type ValidationSeverity,
} from "./components/ValidationMessage/ValidationMessage";
export type { CodiconName } from "./codicons";
export {
ContextMenu,
Expand Down
31 changes: 31 additions & 0 deletions packages/ui/src/tokens.css
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
);
--ui-description-foreground: var(--vscode-descriptionForeground);
--ui-border: var(--vscode-sideBar-border, transparent);
/* Divides sections in an editor tab, like the settings editor's headers */
--ui-separator: var(
--vscode-settings-headerBorder,
var(--vscode-panel-border, var(--ui-border))
);
--ui-link-foreground: var(--vscode-textLink-foreground);
--ui-focus-border: var(
--vscode-focusBorder,
Expand Down Expand Up @@ -79,6 +84,32 @@
var(--vscode-contrastBorder, transparent)
);
--ui-input-placeholder-foreground: var(--vscode-input-placeholderForeground);
--ui-input-validation-info-foreground: var(
--vscode-inputValidation-infoForeground,
var(--ui-foreground)
);
--ui-input-validation-info-background: var(
--vscode-inputValidation-infoBackground
);
--ui-input-validation-info-border: var(--vscode-inputValidation-infoBorder);
--ui-input-validation-warning-foreground: var(
--vscode-inputValidation-warningForeground,
var(--ui-foreground)
);
--ui-input-validation-warning-background: var(
--vscode-inputValidation-warningBackground
);
--ui-input-validation-warning-border: var(
--vscode-inputValidation-warningBorder
);
--ui-input-validation-error-foreground: var(
--vscode-inputValidation-errorForeground,
var(--ui-foreground)
);
--ui-input-validation-error-background: var(
--vscode-inputValidation-errorBackground
);
--ui-input-validation-error-border: var(--vscode-inputValidation-errorBorder);
--ui-checkbox-background: var(
--vscode-checkbox-background,
var(--ui-input-background)
Expand Down
Loading
Loading