chore: simplify AgentRow interface (#18087)

This commit is contained in:
Bruno Quaresma
2025-05-29 11:45:50 -03:00
committed by GitHub
parent 8387dd27ab
commit 201b0b10e8
16 changed files with 167 additions and 406 deletions
+2
View File
@@ -1,4 +1,5 @@
import { API } from "api/api";
import { disabledRefetchOptions } from "./util";
export const deploymentConfigQueryKey = ["deployment", "config"];
@@ -26,6 +27,7 @@ export const deploymentStats = () => {
export const deploymentSSHConfig = () => {
return {
...disabledRefetchOptions,
queryKey: ["deployment", "sshConfig"],
queryFn: API.getDeploymentSSHConfig,
};
+6 -6
View File
@@ -42,24 +42,24 @@ export const AgentMetadataView: FC<AgentMetadataViewProps> = ({ metadata }) => {
interface AgentMetadataProps {
agent: WorkspaceAgent;
storybookMetadata?: WorkspaceAgentMetadata[];
initialMetadata?: WorkspaceAgentMetadata[];
}
const maxSocketErrorRetryCount = 3;
export const AgentMetadata: FC<AgentMetadataProps> = ({
agent,
storybookMetadata,
initialMetadata,
}) => {
const [activeMetadata, setActiveMetadata] = useState(storybookMetadata);
const [activeMetadata, setActiveMetadata] = useState(initialMetadata);
useEffect(() => {
// This is an unfortunate pitfall with this component's testing setup,
// but even though we use the value of storybookMetadata as the initial
// but even though we use the value of initialMetadata as the initial
// value of the activeMetadata, we cannot put activeMetadata itself into
// the dependency array. If we did, we would destroy and rebuild each
// connection every single time a new message comes in from the socket,
// because the socket has to be wired up to the state setter
if (storybookMetadata !== undefined) {
if (initialMetadata !== undefined) {
return;
}
@@ -118,7 +118,7 @@ export const AgentMetadata: FC<AgentMetadataProps> = ({
window.clearTimeout(timeoutId);
activeSocket?.close();
};
}, [agent.id, storybookMetadata]);
}, [agent.id, initialMetadata]);
if (activeMetadata === undefined) {
return (
+19 -27
View File
@@ -1,4 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react";
import { spyOn } from "@storybook/test";
import { API } from "api/api";
import { getPreferredProxy } from "contexts/ProxyContext";
import { chromatic } from "testHelpers/chromatic";
import * as M from "testHelpers/entities";
@@ -95,8 +97,7 @@ const meta: Meta<typeof AgentRow> = {
logs_length: logs.length,
},
workspace: M.MockWorkspace,
showApps: true,
storybookAgentMetadata: defaultAgentMetadata,
initialMetadata: defaultAgentMetadata,
},
decorators: [withProxyProvider(), withDashboardProvider, withWebSocket],
parameters: {
@@ -121,24 +122,6 @@ type Story = StoryObj<typeof AgentRow>;
export const Example: Story = {};
export const HideSSHButton: Story = {
args: {
hideSSHButton: true,
},
};
export const HideVSCodeDesktopButton: Story = {
args: {
hideVSCodeDesktopButton: true,
},
};
export const NotShowingApps: Story = {
args: {
showApps: false,
},
};
export const BunchOfApps: Story = {
args: {
agent: {
@@ -155,14 +138,13 @@ export const BunchOfApps: Story = {
],
},
workspace: M.MockWorkspace,
showApps: true,
},
};
export const Connecting: Story = {
args: {
agent: M.MockWorkspaceAgentConnecting,
storybookAgentMetadata: [],
initialMetadata: [],
},
};
@@ -190,7 +172,7 @@ export const Started: Story = {
export const StartedNoMetadata: Story = {
args: {
...Started.args,
storybookAgentMetadata: [],
initialMetadata: [],
},
};
@@ -243,20 +225,30 @@ export const ShowingPortForward: Story = {
};
export const Outdated: Story = {
beforeEach: () => {
spyOn(API, "getBuildInfo").mockResolvedValue({
...M.MockBuildInfo,
version: "v99.999.9999+c1cdf14",
agent_api_version: "1.0",
});
},
args: {
agent: M.MockWorkspaceAgentOutdated,
workspace: M.MockWorkspace,
serverVersion: "v99.999.9999+c1cdf14",
serverAPIVersion: "1.0",
},
};
export const Deprecated: Story = {
beforeEach: () => {
spyOn(API, "getBuildInfo").mockResolvedValue({
...M.MockBuildInfo,
version: "v99.999.9999+c1cdf14",
agent_api_version: "2.0",
});
},
args: {
agent: M.MockWorkspaceAgentDeprecated,
workspace: M.MockWorkspace,
serverVersion: "v99.999.9999+c1cdf14",
serverAPIVersion: "2.0",
},
};
@@ -1,159 +0,0 @@
import { screen } from "@testing-library/react";
import {
MockTemplate,
MockWorkspace,
MockWorkspaceAgent,
MockWorkspaceApp,
} from "testHelpers/entities";
import {
renderWithAuth,
waitForLoaderToBeRemoved,
} from "testHelpers/renderHelpers";
import type { AgentRowProps } from "./AgentRow";
import { AgentRow } from "./AgentRow";
import { DisplayAppNameMap } from "./AppLink/AppLink";
jest.mock("modules/resources/AgentMetadata", () => {
const AgentMetadata = () => <></>;
return { AgentMetadata };
});
describe.each<{
result: "visible" | "hidden";
props: Partial<AgentRowProps>;
}>([
{
result: "visible",
props: {
showApps: true,
agent: {
...MockWorkspaceAgent,
display_apps: ["vscode", "vscode_insiders"],
status: "connected",
},
hideVSCodeDesktopButton: false,
},
},
{
result: "hidden",
props: {
showApps: false,
agent: {
...MockWorkspaceAgent,
display_apps: ["vscode", "vscode_insiders"],
status: "connected",
},
hideVSCodeDesktopButton: false,
},
},
{
result: "hidden",
props: {
showApps: true,
agent: {
...MockWorkspaceAgent,
display_apps: [],
status: "connected",
},
hideVSCodeDesktopButton: false,
},
},
{
result: "hidden",
props: {
showApps: true,
agent: {
...MockWorkspaceAgent,
display_apps: ["vscode", "vscode_insiders"],
status: "disconnected",
},
hideVSCodeDesktopButton: false,
},
},
{
result: "hidden",
props: {
showApps: true,
agent: {
...MockWorkspaceAgent,
display_apps: ["vscode", "vscode_insiders"],
status: "connected",
},
hideVSCodeDesktopButton: true,
},
},
])("VSCode button visibility", ({ props: testProps, result }) => {
const props: AgentRowProps = {
agent: MockWorkspaceAgent,
workspace: MockWorkspace,
template: MockTemplate,
showApps: false,
serverVersion: "",
serverAPIVersion: "",
onUpdateAgent: () => {
throw new Error("Function not implemented.");
},
...testProps,
};
test(`visibility: ${result}, showApps: ${props.showApps}, hideVSCodeDesktopButton: ${props.hideVSCodeDesktopButton}, display apps: ${props.agent.display_apps}`, async () => {
renderWithAuth(<AgentRow {...props} />);
await waitForLoaderToBeRemoved();
if (result === "visible") {
expect(screen.getByText(DisplayAppNameMap.vscode)).toBeVisible();
} else {
expect(screen.queryByText(DisplayAppNameMap.vscode)).toBeNull();
}
});
});
describe.each<{
props: Partial<AgentRowProps>;
}>([
{
props: {
agent: {
...MockWorkspaceAgent,
apps: [
{
...MockWorkspaceApp,
display_name: `${MockWorkspaceApp.display_name} Not Hidden`,
hidden: false,
},
{
...MockWorkspaceApp,
display_name: `${MockWorkspaceApp.display_name} Is Hidden`,
hidden: true,
},
],
},
},
},
])("hidden hides App button", ({ props: testProps }) => {
const props: AgentRowProps = {
agent: MockWorkspaceAgent,
workspace: MockWorkspace,
template: MockTemplate,
showApps: true,
serverVersion: "",
serverAPIVersion: "",
onUpdateAgent: () => {
throw new Error("Function not implemented.");
},
...testProps,
};
test(`apps: ${props.agent.apps}`, async () => {
renderWithAuth(<AgentRow {...props} />);
await waitForLoaderToBeRemoved();
for (const app of props.agent.apps) {
if (app.hidden) {
expect(screen.queryByText(app.display_name as string)).toBeNull();
} else {
expect(screen.getByText(app.display_name as string)).toBeVisible();
}
}
});
});
+28 -54
View File
@@ -15,6 +15,7 @@ import { DropdownArrow } from "components/DropdownArrow/DropdownArrow";
import type { Line } from "components/Logs/LogLine";
import { Stack } from "components/Stack/Stack";
import { useProxy } from "contexts/ProxyContext";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import { AppStatuses } from "pages/WorkspacePage/AppStatuses";
import {
type FC,
@@ -43,46 +44,32 @@ import { TerminalLink } from "./TerminalLink/TerminalLink";
import { VSCodeDesktopButton } from "./VSCodeDesktopButton/VSCodeDesktopButton";
import { useAgentLogs } from "./useAgentLogs";
export interface AgentRowProps {
interface AgentRowProps {
agent: WorkspaceAgent;
workspace: Workspace;
showApps: boolean;
showBuiltinApps?: boolean;
sshPrefix?: string;
hideSSHButton?: boolean;
hideVSCodeDesktopButton?: boolean;
serverVersion: string;
serverAPIVersion: string;
onUpdateAgent: () => void;
template: Template;
storybookAgentMetadata?: WorkspaceAgentMetadata[];
initialMetadata?: WorkspaceAgentMetadata[];
onUpdateAgent: () => void;
}
export const AgentRow: FC<AgentRowProps> = ({
agent,
workspace,
template,
showApps,
showBuiltinApps = true,
hideSSHButton,
hideVSCodeDesktopButton,
serverVersion,
serverAPIVersion,
onUpdateAgent,
storybookAgentMetadata,
sshPrefix,
initialMetadata,
}) => {
// Apps visibility
const { browser_only } = useFeatureVisibility();
const visibleApps = agent.apps.filter((app) => !app.hidden);
const hasAppsToDisplay = !hideVSCodeDesktopButton || visibleApps.length > 0;
const hasAppsToDisplay = !browser_only && visibleApps.length > 0;
const shouldDisplayApps =
showApps &&
((agent.status === "connected" && hasAppsToDisplay) ||
agent.status === "connecting");
(agent.status === "connected" && hasAppsToDisplay) ||
agent.status === "connecting";
const hasVSCodeApp =
agent.display_apps.includes("vscode") ||
agent.display_apps.includes("vscode_insiders");
const showVSCode = hasVSCodeApp && !hideVSCodeDesktopButton;
const showVSCode = hasVSCodeApp && !browser_only;
const hasStartupFeatures = Boolean(agent.logs_length);
const { proxy } = useProxy();
@@ -184,12 +171,7 @@ export const AgentRow: FC<AgentRowProps> = ({
</div>
{agent.status === "connected" && (
<>
<AgentVersion
agent={agent}
serverVersion={serverVersion}
serverAPIVersion={serverAPIVersion}
onUpdate={onUpdateAgent}
/>
<AgentVersion agent={agent} onUpdate={onUpdateAgent} />
<AgentLatency agent={agent} />
</>
)}
@@ -201,28 +183,23 @@ export const AgentRow: FC<AgentRowProps> = ({
)}
</div>
{showBuiltinApps && (
<div css={{ display: "flex" }}>
{!hideSSHButton && agent.display_apps.includes("ssh_helper") && (
<AgentSSHButton
workspaceName={workspace.name}
agentName={agent.name}
sshPrefix={sshPrefix}
<div css={{ display: "flex" }}>
{!browser_only && agent.display_apps.includes("ssh_helper") && (
<AgentSSHButton
workspaceName={workspace.name}
agentName={agent.name}
/>
)}
{proxy.preferredWildcardHostname !== "" &&
agent.display_apps.includes("port_forwarding_helper") && (
<PortForwardButton
host={proxy.preferredWildcardHostname}
workspace={workspace}
agent={agent}
template={template}
/>
)}
{proxy.preferredWildcardHostname !== "" &&
agent.display_apps.includes("port_forwarding_helper") && (
<PortForwardButton
host={proxy.preferredWildcardHostname}
workspaceName={workspace.name}
agent={agent}
username={workspace.owner_username}
workspaceID={workspace.id}
template={template}
/>
)}
</div>
)}
</div>
</header>
<div css={styles.content}>
@@ -257,7 +234,7 @@ export const AgentRow: FC<AgentRowProps> = ({
</>
)}
{showBuiltinApps && agent.display_apps.includes("web_terminal") && (
{agent.display_apps.includes("web_terminal") && (
<TerminalLink
workspaceName={workspace.name}
agentName={agent.name}
@@ -300,10 +277,7 @@ export const AgentRow: FC<AgentRowProps> = ({
</section>
)}
<AgentMetadata
storybookMetadata={storybookAgentMetadata}
agent={agent}
/>
<AgentMetadata initialMetadata={initialMetadata} agent={agent} />
</div>
{hasStartupFeatures && (
+10 -9
View File
@@ -1,26 +1,27 @@
import { buildInfo } from "api/queries/buildInfo";
import type { WorkspaceAgent } from "api/typesGenerated";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import type { FC } from "react";
import { useQuery } from "react-query";
import { agentVersionStatus, getDisplayVersionStatus } from "utils/workspace";
import { AgentOutdatedTooltip } from "./AgentOutdatedTooltip";
interface AgentVersionProps {
agent: WorkspaceAgent;
serverVersion: string;
serverAPIVersion: string;
onUpdate: () => void;
}
export const AgentVersion: FC<AgentVersionProps> = ({
agent,
serverVersion,
serverAPIVersion,
onUpdate,
}) => {
export const AgentVersion: FC<AgentVersionProps> = ({ agent, onUpdate }) => {
const { metadata } = useEmbeddedMetadata();
const { data: build } = useQuery(buildInfo(metadata["build-info"]));
const serverVersion = build?.version ?? "";
const apiServerVersion = build?.agent_api_version ?? "";
const { status } = getDisplayVersionStatus(
agent.version,
serverVersion,
agent.api_version,
serverAPIVersion,
apiServerVersion,
);
if (status === agentVersionStatus.Updated) {
@@ -2,6 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react";
import {
MockListeningPortsResponse,
MockSharedPortsResponse,
MockTemplate,
MockWorkspace,
MockWorkspaceAgent,
} from "testHelpers/entities";
@@ -14,6 +15,8 @@ const meta: Meta<typeof PortForwardButton> = {
decorators: [withDashboardProvider],
args: {
agent: MockWorkspaceAgent,
workspace: MockWorkspace,
template: MockTemplate,
},
};
@@ -17,9 +17,10 @@ import {
} from "api/queries/workspaceportsharing";
import {
type Template,
type UpsertWorkspaceAgentPortShareRequest,
type Workspace,
type WorkspaceAgent,
type WorkspaceAgentListeningPort,
type WorkspaceAgentPortShare,
type WorkspaceAgentPortShareLevel,
type WorkspaceAgentPortShareProtocol,
WorkspaceAppSharingLevels,
@@ -42,7 +43,7 @@ import {
PopoverContent,
PopoverTrigger,
} from "components/deprecated/Popover/Popover";
import { type FormikContextType, useFormik } from "formik";
import { useFormik } from "formik";
import { type ClassName, useClassName } from "hooks/useClassName";
import {
ChevronDownIcon,
@@ -64,31 +65,40 @@ import * as Yup from "yup";
interface PortForwardButtonProps {
host: string;
username: string;
workspaceName: string;
workspaceID: string;
workspace: Workspace;
agent: WorkspaceAgent;
template: Template;
}
export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
const { agent } = props;
export const PortForwardButton: FC<PortForwardButtonProps> = ({
host,
workspace,
template,
agent,
}) => {
const { entitlements } = useDashboard();
const paper = useClassName(classNames.paper, []);
const portsQuery = useQuery({
const { data: listeningPorts } = useQuery({
queryKey: ["portForward", agent.id],
queryFn: () => API.getAgentListeningPorts(agent.id),
enabled: agent.status === "connected",
refetchInterval: 5_000,
select: (res) => res.ports,
});
const { data: sharedPorts, refetch: refetchSharedPorts } = useQuery({
...workspacePortShares(workspace.id),
enabled: agent.status === "connected",
select: (res) => res.shares,
});
return (
<Popover>
<PopoverTrigger>
<Button disabled={!portsQuery.data} size="sm" variant="subtle">
<Spinner loading={!portsQuery.data}>
<span css={styles.portCount}>{portsQuery.data?.ports.length}</span>
<Button disabled={!listeningPorts} size="sm" variant="subtle">
<Spinner loading={!listeningPorts}>
<span css={styles.portCount}>{listeningPorts?.length}</span>
</Spinner>
Open ports
<ChevronDownIcon className="size-4" />
@@ -96,89 +106,89 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
</PopoverTrigger>
<PopoverContent horizontal="right" classes={{ paper }}>
<PortForwardPopoverView
{...props}
listeningPorts={portsQuery.data?.ports}
host={host}
agent={agent}
workspace={workspace}
template={template}
sharedPorts={sharedPorts ?? []}
listeningPorts={listeningPorts ?? []}
portSharingControlsEnabled={
entitlements.features.control_shared_ports.enabled
}
refetchSharedPorts={refetchSharedPorts}
/>
</PopoverContent>
</Popover>
);
};
const getValidationSchema = (): Yup.AnyObjectSchema =>
const openPortSchema = (): Yup.AnyObjectSchema =>
Yup.object({
port: Yup.number().required().min(9).max(65535),
share_level: Yup.string().required().oneOf(WorkspaceAppSharingLevels),
});
interface PortForwardPopoverViewProps extends PortForwardButtonProps {
listeningPorts?: readonly WorkspaceAgentListeningPort[];
interface PortForwardPopoverViewProps {
host: string;
workspace: Workspace;
agent: WorkspaceAgent;
template: Template;
sharedPorts: readonly WorkspaceAgentPortShare[];
listeningPorts: readonly WorkspaceAgentListeningPort[];
portSharingControlsEnabled: boolean;
refetchSharedPorts: () => void;
}
type Optional<T, K extends keyof T> = Pick<Partial<T>, K> & Omit<T, K>;
export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
host,
workspaceName,
workspaceID,
workspace,
agent,
template,
username,
sharedPorts,
listeningPorts,
portSharingControlsEnabled,
refetchSharedPorts,
}) => {
const theme = useTheme();
const [listeningPortProtocol, setListeningPortProtocol] = useState(
getWorkspaceListeningPortsProtocol(workspaceID),
getWorkspaceListeningPortsProtocol(workspace.id),
);
const sharedPortsQuery = useQuery({
...workspacePortShares(workspaceID),
enabled: agent.status === "connected",
const upsertSharedPortMutation = useMutation({
...upsertWorkspacePortShare(workspace.id),
onSuccess: refetchSharedPorts,
});
const sharedPorts = sharedPortsQuery.data?.shares || [];
const upsertSharedPortMutation = useMutation(
upsertWorkspacePortShare(workspaceID),
);
const deleteSharedPortMutation = useMutation({
...deleteWorkspacePortShare(workspace.id),
onSuccess: refetchSharedPorts,
});
const deleteSharedPortMutation = useMutation(
deleteWorkspacePortShare(workspaceID),
);
// share port form
const {
mutateAsync: upsertWorkspacePortShareForm,
isPending: isSubmitting,
error: submitError,
} = useMutation(upsertWorkspacePortShare(workspaceID));
const validationSchema = getValidationSchema();
// TODO: do partial here
const form: FormikContextType<
Optional<UpsertWorkspaceAgentPortShareRequest, "port">
> = useFormik<Optional<UpsertWorkspaceAgentPortShareRequest, "port">>({
} = useMutation({
...upsertWorkspacePortShare(workspace.id),
onSuccess: refetchSharedPorts,
});
const form = useFormik({
initialValues: {
agent_name: agent.name,
port: undefined,
port: "",
protocol: "http",
share_level: "authenticated",
},
validationSchema,
onSubmit: async (values) => {
// we need port to be optional in the initialValues so it appears empty instead of 0.
// because of this we need to reset the form to clear the port field manually.
form.resetForm();
await form.setFieldValue("port", "");
const port = Number(values.port);
validationSchema: openPortSchema(),
onSubmit: async (values, { resetForm }) => {
resetForm();
await upsertWorkspacePortShareForm({
...values,
port,
agent_name: values.agent_name,
port: Number(values.port),
share_level: values.share_level as WorkspaceAgentPortShareLevel,
protocol: values.protocol as WorkspaceAgentPortShareProtocol,
});
await sharedPortsQuery.refetch();
},
});
const getFieldHelpers = getFormHelpers(form, submitError);
@@ -188,7 +198,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
(port) => port.agent_name === agent.name,
);
// we don't want to show listening ports if it's a shared port
const filteredListeningPorts = (listeningPorts ?? []).filter((port) =>
const filteredListeningPorts = listeningPorts.filter((port) =>
filteredSharedPorts.every((sharedPort) => sharedPort.port !== port.port),
);
// only disable the form if shared port controls are entitled and the template doesn't allow sharing ports
@@ -257,7 +267,7 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
| "https";
setListeningPortProtocol(selectedProtocol);
saveWorkspaceListeningPortsProtocol(
workspaceID,
workspace.id,
selectedProtocol,
);
}}
@@ -276,8 +286,8 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
host,
port,
agent.name,
workspaceName,
username,
workspace.name,
workspace.owner_username,
listeningPortProtocol,
);
window.open(url, "_blank");
@@ -317,8 +327,8 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
host,
port.port,
agent.name,
workspaceName,
username,
workspace.name,
workspace.owner_username,
listeningPortProtocol,
);
const label =
@@ -371,7 +381,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
protocol: listeningPortProtocol,
share_level: "authenticated",
});
await sharedPortsQuery.refetch();
}}
>
<ShareIcon />
@@ -407,8 +416,8 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
host,
share.port,
agent.name,
workspaceName,
username,
workspace.name,
workspace.owner_username,
share.protocol,
);
const label = share.port;
@@ -445,7 +454,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
.value as WorkspaceAgentPortShareProtocol,
share_level: share.share_level,
});
await sharedPortsQuery.refetch();
}}
>
<MenuItem value="http">HTTP</MenuItem>
@@ -469,7 +477,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
share_level: event.target
.value as WorkspaceAgentPortShareLevel,
});
await sharedPortsQuery.refetch();
}}
>
<MenuItem value="authenticated">Authenticated</MenuItem>
@@ -488,7 +495,6 @@ export const PortForwardPopoverView: FC<PortForwardPopoverViewProps> = ({
agent_name: agent.name,
port: share.port,
});
await sharedPortsQuery.refetch();
}}
>
<XIcon
@@ -28,7 +28,7 @@ const meta: Meta<typeof PortForwardPopoverView> = {
args: {
agent: MockWorkspaceAgent,
template: MockTemplate,
workspaceID: MockWorkspace.id,
workspace: MockWorkspace,
portSharingControlsEnabled: true,
host: "coder.com",
},
@@ -40,14 +40,7 @@ type Story = StoryObj<typeof PortForwardPopoverView>;
export const WithPorts: Story = {
args: {
listeningPorts: MockListeningPortsResponse.ports,
},
parameters: {
queries: [
{
key: ["sharedPorts", MockWorkspace.id],
data: MockSharedPortsResponse,
},
],
sharedPorts: MockSharedPortsResponse.shares,
},
};
@@ -58,28 +51,14 @@ export const WithManyPorts: Story = {
network: "",
port: 3000 + i,
})),
},
parameters: {
queries: [
{
key: ["sharedPorts", MockWorkspace.id],
data: MockSharedPortsResponse,
},
],
sharedPorts: MockSharedPortsResponse.shares,
},
};
export const Empty: Story = {
args: {
listeningPorts: [],
},
parameters: {
queries: [
{
key: ["sharedPorts", MockWorkspace.id],
data: { shares: [] },
},
],
sharedPorts: [],
},
};
@@ -87,20 +66,14 @@ export const AGPLPortSharing: Story = {
args: {
listeningPorts: MockListeningPortsResponse.ports,
portSharingControlsEnabled: false,
},
parameters: {
queries: [
{
key: ["sharedPorts", MockWorkspace.id],
data: MockSharedPortsResponse,
},
],
sharedPorts: MockSharedPortsResponse.shares,
},
};
export const EnterprisePortSharingControlsOwner: Story = {
args: {
listeningPorts: MockListeningPortsResponse.ports,
sharedPorts: [],
template: {
...MockTemplate,
max_port_share_level: "owner",
@@ -115,17 +88,8 @@ export const EnterprisePortSharingControlsAuthenticated: Story = {
...MockTemplate,
max_port_share_level: "authenticated",
},
},
parameters: {
queries: [
{
key: ["sharedPorts", MockWorkspace.id],
data: {
shares: MockSharedPortsResponse.shares.filter((share) => {
return share.share_level === "authenticated";
}),
},
},
],
sharedPorts: MockSharedPortsResponse.shares.filter((share) => {
return share.share_level === "authenticated";
}),
},
};
@@ -19,12 +19,12 @@ describe("Port Forward Popover View", () => {
<PortForwardPopoverView
agent={MockWorkspaceAgent}
template={MockTemplate}
workspaceID={MockWorkspace.id}
listeningPorts={MockListeningPortsResponse.ports}
portSharingControlsEnabled
host="host"
username="username"
workspaceName="workspaceName"
workspace={MockWorkspace}
sharedPorts={[]}
refetchSharedPorts={jest.fn()}
/>
</QueryClientProvider>,
);
@@ -1,6 +1,11 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import { MockWorkspace, MockWorkspaceAgent } from "testHelpers/entities";
import { spyOn, userEvent, within } from "@storybook/test";
import { API } from "api/api";
import {
MockDeploymentSSH,
MockWorkspace,
MockWorkspaceAgent,
} from "testHelpers/entities";
import { withDesktopViewport } from "testHelpers/storybook";
import { AgentSSHButton } from "./SSHButton";
@@ -16,15 +21,16 @@ export const Closed: Story = {
args: {
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
sshPrefix: "coder.",
},
};
export const Opened: Story = {
beforeEach: () => {
spyOn(API, "getDeploymentSSHConfig").mockResolvedValue(MockDeploymentSSH);
},
args: {
workspaceName: MockWorkspace.name,
agentName: MockWorkspaceAgent.name,
sshPrefix: "coder.",
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
@@ -1,4 +1,5 @@
import type { Interpolation, Theme } from "@emotion/react";
import { deploymentSSHConfig } from "api/queries/deployment";
import { Button } from "components/Button/Button";
import { CodeExample } from "components/CodeExample/CodeExample";
import {
@@ -15,20 +16,21 @@ import {
import { type ClassName, useClassName } from "hooks/useClassName";
import { ChevronDownIcon } from "lucide-react";
import type { FC } from "react";
import { useQuery } from "react-query";
import { docs } from "utils/docs";
interface AgentSSHButtonProps {
workspaceName: string;
agentName: string;
sshPrefix?: string;
}
export const AgentSSHButton: FC<AgentSSHButtonProps> = ({
workspaceName,
agentName,
sshPrefix,
}) => {
const paper = useClassName(classNames.paper, []);
const { data } = useQuery(deploymentSSHConfig());
const sshPrefix = data?.hostname_prefix;
return (
<Popover>
+3 -2
View File
@@ -126,7 +126,7 @@ const TaskPage = () => {
</span>
<Button size="sm" variant="outline" asChild className="mt-4">
<RouterLink
to={`/@${task.workspace.owner_name}/${task.workspace.name}/builds/${task.workspace.latest_build.build_number}`}
to={`/@${task.workspace.owner_username}/${task.workspace.name}/builds/${task.workspace.latest_build.build_number}`}
>
View logs
</RouterLink>
@@ -246,7 +246,8 @@ const TaskPage = () => {
<div className="flex flex-col">
<h1 className="m-0 text-sm font-medium">{task.prompt}</h1>
<span className="text-xs text-content-secondary">
Created by {task.workspace.owner_name}{" "}
Created by{" "}
{task.workspace.owner_name ?? task.workspace.owner_username}{" "}
{timeFrom(new Date(task.workspace.created_at))}
</span>
</div>
@@ -77,7 +77,6 @@ export const Running: Story = {
},
handleStart: action("start"),
handleStop: action("stop"),
buildInfo: Mocks.MockBuildInfo,
template: Mocks.MockTemplate,
},
};
@@ -219,7 +218,6 @@ export const RunningWithAppStatuses: Story = {
},
handleStart: action("start"),
handleStop: action("stop"),
buildInfo: Mocks.MockBuildInfo,
template: Mocks.MockTemplate,
},
};
@@ -31,10 +31,6 @@ interface WorkspaceProps {
permissions: WorkspacePermissions;
isUpdating: boolean;
isRestarting: boolean;
hideSSHButton?: boolean;
hideVSCodeDesktopButton?: boolean;
buildInfo?: TypesGen.BuildInfoResponse;
sshPrefix?: string;
buildLogs?: TypesGen.ProvisionerJobLog[];
latestVersion?: TypesGen.TemplateVersion;
timings?: TypesGen.WorkspaceBuildTimings;
@@ -56,10 +52,6 @@ export const Workspace: FC<WorkspaceProps> = ({
workspace,
isUpdating,
isRestarting,
hideSSHButton,
hideVSCodeDesktopButton,
buildInfo,
sshPrefix,
template,
buildLogs,
latestVersion,
@@ -252,13 +244,6 @@ export const Workspace: FC<WorkspaceProps> = ({
agent={agent}
workspace={workspace}
template={template}
sshPrefix={sshPrefix}
showApps={permissions.updateWorkspace}
showBuiltinApps={permissions.updateWorkspace}
hideSSHButton={hideSSHButton}
hideVSCodeDesktopButton={hideVSCodeDesktopButton}
serverVersion={buildInfo?.version || ""}
serverAPIVersion={buildInfo?.agent_api_version || ""}
onUpdateAgent={handleUpdate} // On updating the workspace the agent version is also updated
/>
))}
@@ -1,7 +1,5 @@
import { API } from "api/api";
import { getErrorMessage } from "api/errors";
import { buildInfo } from "api/queries/buildInfo";
import { deploymentSSHConfig } from "api/queries/deployment";
import { templateVersion } from "api/queries/templates";
import { workspaceBuildTimings } from "api/queries/workspaceBuilds";
import {
@@ -18,9 +16,7 @@ import {
type ConfirmDialogProps,
} from "components/Dialogs/ConfirmDialog/ConfirmDialog";
import { displayError } from "components/GlobalSnackbar/utils";
import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata";
import { useWorkspaceBuildLogs } from "hooks/useWorkspaceBuildLogs";
import { useFeatureVisibility } from "modules/dashboard/useFeatureVisibility";
import {
WorkspaceUpdateDialogs,
useWorkspaceUpdate,
@@ -43,10 +39,7 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
template,
permissions,
}) => {
const { metadata } = useEmbeddedMetadata();
const buildInfoQuery = useQuery(buildInfo(metadata["build-info"]));
const queryClient = useQueryClient();
const featureVisibility = useFeatureVisibility();
// Build logs
const shouldStreamBuildLogs = workspace.latest_build.status !== "running";
@@ -65,9 +58,6 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
mutationFn: API.restartWorkspace,
});
// SSH Prefix
const sshPrefixQuery = useQuery(deploymentSSHConfig());
// Favicon
const favicon = getFaviconByStatus(workspace.latest_build);
const [faviconTheme, setFaviconTheme] = useState<"light" | "dark">("dark");
@@ -205,10 +195,6 @@ export const WorkspaceReadyPage: FC<WorkspaceReadyPageProps> = ({
isRestarting={isRestarting}
workspace={workspace}
latestVersion={latestVersion}
hideSSHButton={featureVisibility.browser_only}
hideVSCodeDesktopButton={featureVisibility.browser_only}
buildInfo={buildInfoQuery.data}
sshPrefix={sshPrefixQuery.data?.hostname_prefix}
template={template}
buildLogs={buildLogs}
timings={timingsQuery.data}