mirror of
https://github.com/coder/coder.git
synced 2026-06-06 22:48:19 +00:00
Co-authored-by: Jake Howell <jacob@coder.com> Co-authored-by: Atif Ali <atif@coder.com> Co-authored-by: Jeremy Ruppel <jeremy.ruppel@gmail.com>
This commit is contained in:
committed by
GitHub
parent
e1d7ab0f68
commit
328c649c0f
@@ -189,6 +189,42 @@ export const Connecting: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ConnectingWithStartupLogs: Story = {
|
||||
args: {
|
||||
agent: {
|
||||
...M.MockWorkspaceAgentConnecting,
|
||||
logs_length: 1,
|
||||
},
|
||||
initialMetadata: [],
|
||||
},
|
||||
parameters: {
|
||||
webSocket: [
|
||||
{
|
||||
event: "message",
|
||||
data: JSON.stringify([
|
||||
{
|
||||
id: 1,
|
||||
level: "info",
|
||||
output: "starting up",
|
||||
source_id: M.MockWorkspaceAgentLogSource.id,
|
||||
created_at: fixedLogTimestamp,
|
||||
},
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Agent is connecting (hasAgentIssues=true) but no script has failed.
|
||||
// Old code snapped to the Startup Script tab; the fix keeps us on All Logs.
|
||||
const allLogsTab = await canvas.findByRole("tab", { name: "All Logs" });
|
||||
await waitFor(() =>
|
||||
expect(allLogsTab).toHaveAttribute("data-state", "active"),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Timeout: Story = {
|
||||
args: {
|
||||
agent: M.MockWorkspaceAgentTimeout,
|
||||
@@ -257,6 +293,135 @@ export const StartError: Story = {
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// MockWorkspaceAgentStartError ships with a Startup Script whose script
|
||||
// has exit_code: 1, so the auto-select should land us there.
|
||||
const startupScriptTab = await canvas.findByRole("tab", {
|
||||
name: "Startup Script",
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(startupScriptTab).toHaveAttribute("data-state", "active"),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const StartErrorWithoutFailedSourceLogs: Story = {
|
||||
args: {
|
||||
agent: M.MockWorkspaceAgentStartError,
|
||||
},
|
||||
parameters: {
|
||||
// Send log entries only for the OK script, mirroring the case where a
|
||||
// failed script never emitted any output. The selected tab must not be
|
||||
// initialized to a source that has no rendered tab.
|
||||
webSocket: [
|
||||
{
|
||||
event: "message",
|
||||
data: JSON.stringify(
|
||||
M.MockWorkspaceAgentStartError.log_sources
|
||||
.filter((source) => {
|
||||
const script = M.MockWorkspaceAgentStartError.scripts.find(
|
||||
(s) => s.log_source_id === source.id,
|
||||
);
|
||||
return !script?.exit_code && script?.status === "ok";
|
||||
})
|
||||
.flatMap((source, i) => [
|
||||
{
|
||||
id: i,
|
||||
level: "info",
|
||||
output: `output from '${source.display_name}'`,
|
||||
source_id: source.id,
|
||||
created_at: fixedLogTimestamp,
|
||||
},
|
||||
]),
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Wait for a non-failed source tab to render, confirming logs streamed in.
|
||||
await canvas.findByRole("tab", { name: "coder" });
|
||||
|
||||
// All Logs must stay active because no failed source has rendered logs.
|
||||
const allLogsTab = canvas.getByRole("tab", { name: "All Logs" });
|
||||
await waitFor(() =>
|
||||
expect(allLogsTab).toHaveAttribute("data-state", "active"),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
const NON_STARTUP_SCRIPT_SOURCE_ID = "install-script-source-id";
|
||||
|
||||
export const NonStartupScriptError: Story = {
|
||||
args: {
|
||||
agent: {
|
||||
...M.MockWorkspaceAgent,
|
||||
logs_length: 2,
|
||||
scripts: [
|
||||
// Startup Script succeeded.
|
||||
{
|
||||
...M.MockWorkspaceAgent.scripts[0],
|
||||
exit_code: 0,
|
||||
status: "ok",
|
||||
},
|
||||
// A non-startup script failed; that's the tab we should auto-select.
|
||||
{
|
||||
...M.MockWorkspaceAgent.scripts[0],
|
||||
id: "install-script-id",
|
||||
log_source_id: NON_STARTUP_SCRIPT_SOURCE_ID,
|
||||
exit_code: 1,
|
||||
status: "exit_failure",
|
||||
display_name: "Install Script",
|
||||
},
|
||||
],
|
||||
log_sources: [
|
||||
...M.MockWorkspaceAgent.log_sources,
|
||||
{
|
||||
...M.MockWorkspaceAgent.log_sources[0],
|
||||
id: NON_STARTUP_SCRIPT_SOURCE_ID,
|
||||
display_name: "Install Script",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
webSocket: [
|
||||
{
|
||||
event: "message",
|
||||
data: JSON.stringify([
|
||||
{
|
||||
id: 1,
|
||||
level: "info",
|
||||
output: "startup ok",
|
||||
source_id: M.MockWorkspaceAgentLogSource.id,
|
||||
created_at: fixedLogTimestamp,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
level: "error",
|
||||
output: "install failed",
|
||||
source_id: NON_STARTUP_SCRIPT_SOURCE_ID,
|
||||
created_at: fixedLogTimestamp,
|
||||
},
|
||||
]),
|
||||
},
|
||||
],
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
// Startup Script is OK; only Install Script failed. The auto-select must
|
||||
// follow the failure, not the position or display name.
|
||||
const installScriptTab = await canvas.findByRole("tab", {
|
||||
name: "Install Script",
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(installScriptTab).toHaveAttribute("data-state", "active"),
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ShuttingDown: Story = {
|
||||
|
||||
@@ -25,6 +25,7 @@ import type {
|
||||
Workspace,
|
||||
WorkspaceAgent,
|
||||
WorkspaceAgentMetadata,
|
||||
WorkspaceAgentScript,
|
||||
} from "#/api/typesGenerated";
|
||||
import { CheckIcon } from "#/components/AnimatedIcons/Check";
|
||||
import { ChevronDownIcon } from "#/components/AnimatedIcons/ChevronDown";
|
||||
@@ -128,6 +129,13 @@ const getAgentBorderClass = (
|
||||
|
||||
const STARTUP_SCRIPT_DISPLAY_NAME = "Startup Script";
|
||||
|
||||
// A script is considered failed if it exited with a non-zero code, or if its
|
||||
// status reports a known failure mode (anything other than "ok"). Kept aligned
|
||||
// with the per-tab error indicator so the auto-selected tab matches the visual
|
||||
// warning badge.
|
||||
const isScriptFailed = (script: WorkspaceAgentScript | undefined): boolean =>
|
||||
Boolean(script?.exit_code || (script?.status && script.status !== "ok"));
|
||||
|
||||
export const AgentRow: FC<AgentRowProps> = ({
|
||||
agent,
|
||||
subAgents,
|
||||
@@ -235,14 +243,34 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
agent,
|
||||
Boolean(hasDevcontainerErrors || shouldShowWildcardWarning),
|
||||
);
|
||||
const failedStartupScriptSource = hasAgentIssues
|
||||
? agent.log_sources.find(
|
||||
(s) => s.display_name === STARTUP_SCRIPT_DISPLAY_NAME,
|
||||
)
|
||||
: undefined;
|
||||
const [selectedLogTab, setSelectedLogTab] = useState(
|
||||
failedStartupScriptSource?.id ?? "all",
|
||||
);
|
||||
const [selectedLogTab, setSelectedLogTab] = useState("all");
|
||||
const hasAutoSelectedLogTabRef = useRef(false);
|
||||
// Auto-select the first log tab whose script failed and has rendered output.
|
||||
useEffect(() => {
|
||||
if (hasAutoSelectedLogTabRef.current) {
|
||||
return;
|
||||
}
|
||||
const failedSourceWithLogs = agent.log_sources.find((logSource) => {
|
||||
const script = agent.scripts.find(
|
||||
(s) => s.log_source_id === logSource.id,
|
||||
);
|
||||
if (!isScriptFailed(script)) {
|
||||
return false;
|
||||
}
|
||||
return agentLogs.some(
|
||||
(log) =>
|
||||
log.source_id === logSource.id && (log.output?.length ?? 0) > 0,
|
||||
);
|
||||
});
|
||||
if (failedSourceWithLogs) {
|
||||
hasAutoSelectedLogTabRef.current = true;
|
||||
setSelectedLogTab(failedSourceWithLogs.id);
|
||||
}
|
||||
}, [agent.log_sources, agent.scripts, agentLogs]);
|
||||
const handleSelectedLogTabChange = (value: string) => {
|
||||
hasAutoSelectedLogTabRef.current = true;
|
||||
setSelectedLogTab(value);
|
||||
};
|
||||
const sortedSourceLogTabs = agent.log_sources
|
||||
.filter((logSource) => {
|
||||
// Remove the logSources that have no entries.
|
||||
@@ -269,9 +297,7 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
) : null,
|
||||
title: logSource.display_name,
|
||||
value: logSource.id,
|
||||
error: Boolean(
|
||||
script?.exit_code || (script?.status && script.status !== "ok"),
|
||||
),
|
||||
error: isScriptFailed(script),
|
||||
};
|
||||
})
|
||||
.sort((a, b) => {
|
||||
@@ -563,7 +589,7 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
<Tabs
|
||||
className="-mx-px -mt-px"
|
||||
value={selectedLogTab}
|
||||
onValueChange={setSelectedLogTab}
|
||||
onValueChange={handleSelectedLogTabChange}
|
||||
>
|
||||
<div className="flex items-stretch">
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
@@ -621,7 +647,7 @@ export const AgentRow: FC<AgentRowProps> = ({
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuRadioGroup
|
||||
value={selectedLogTab}
|
||||
onValueChange={setSelectedLogTab}
|
||||
onValueChange={handleSelectedLogTabChange}
|
||||
>
|
||||
{overflowLogTabs.map((tab) => (
|
||||
<DropdownMenuRadioItem
|
||||
|
||||
Reference in New Issue
Block a user