mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: display app templates correctly in build preview (#10994)
* fix: appropriately display display_app apps in template build preview * added display apps to build preview * added test, consolidated names * handling empty state
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
import { MockWorkspaceAgent } from "testHelpers/entities";
|
||||
import { WorkspaceAgent, DisplayApps, DisplayApp } from "api/typesGenerated";
|
||||
import { renderComponent } from "testHelpers/renderHelpers";
|
||||
import { AgentRowPreview } from "./AgentRowPreview";
|
||||
import { screen } from "@testing-library/react";
|
||||
import { DisplayAppNameMap } from "./AppLink/AppLink";
|
||||
|
||||
const AllDisplayAppsAndModule = MockWorkspaceAgent;
|
||||
const VSCodeNoInsiders = {
|
||||
...MockWorkspaceAgent,
|
||||
display_apps: [
|
||||
"ssh_helper",
|
||||
"port_forwarding_helper",
|
||||
"vscode",
|
||||
"web_terminal",
|
||||
] as DisplayApp[],
|
||||
};
|
||||
const VSCodeWithInsiders = {
|
||||
...MockWorkspaceAgent,
|
||||
display_apps: [
|
||||
"ssh_helper",
|
||||
"port_forwarding_helper",
|
||||
"vscode",
|
||||
"vscode_insiders",
|
||||
"web_terminal",
|
||||
] as DisplayApp[],
|
||||
};
|
||||
const NoVSCode = {
|
||||
...MockWorkspaceAgent,
|
||||
display_apps: [
|
||||
"ssh_helper",
|
||||
"port_forwarding_helper",
|
||||
"web_terminal",
|
||||
] as DisplayApp[],
|
||||
};
|
||||
|
||||
const NoModulesJustApps = {
|
||||
...MockWorkspaceAgent,
|
||||
apps: [],
|
||||
};
|
||||
|
||||
const NoAppsJustModules = {
|
||||
...MockWorkspaceAgent,
|
||||
display_apps: [] as DisplayApp[],
|
||||
};
|
||||
|
||||
const EmptyAppPreview = {
|
||||
...MockWorkspaceAgent,
|
||||
apps: [],
|
||||
display_apps: [] as DisplayApp[],
|
||||
};
|
||||
|
||||
describe("AgentRowPreviewApps", () => {
|
||||
it.each<{
|
||||
workspaceAgent: WorkspaceAgent;
|
||||
testName: string;
|
||||
}>([
|
||||
{
|
||||
workspaceAgent: AllDisplayAppsAndModule,
|
||||
testName: "AllDisplayAppsAndModule",
|
||||
},
|
||||
{
|
||||
workspaceAgent: VSCodeNoInsiders,
|
||||
testName: "VSCodeNoInsiders",
|
||||
},
|
||||
{
|
||||
workspaceAgent: VSCodeWithInsiders,
|
||||
testName: "VSCodeWithInsiders",
|
||||
},
|
||||
{
|
||||
workspaceAgent: NoVSCode,
|
||||
testName: "NoVSCode",
|
||||
},
|
||||
{
|
||||
workspaceAgent: NoModulesJustApps,
|
||||
testName: "NoModulesJustApps",
|
||||
},
|
||||
{
|
||||
workspaceAgent: NoAppsJustModules,
|
||||
testName: "NoAppsJustModules",
|
||||
},
|
||||
{
|
||||
workspaceAgent: EmptyAppPreview,
|
||||
testName: "EmptyAppPreview",
|
||||
},
|
||||
])(
|
||||
`<AgentRowPreview agent={$testName} /> displays appropriately`,
|
||||
({ workspaceAgent }) => {
|
||||
renderComponent(<AgentRowPreview agent={workspaceAgent} />);
|
||||
workspaceAgent.apps.forEach((module) => {
|
||||
expect(screen.getByText(module.display_name)).toBeInTheDocument();
|
||||
});
|
||||
workspaceAgent.display_apps
|
||||
.filter((app) => app !== "vscode" && app !== "vscode_insiders") // these get special treatment
|
||||
.forEach((app) => {
|
||||
expect(screen.getByText(DisplayAppNameMap[app])).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// test VS Code display
|
||||
if (workspaceAgent.display_apps.includes("vscode")) {
|
||||
expect(
|
||||
screen.getByText(DisplayAppNameMap["vscode"]),
|
||||
).toBeInTheDocument();
|
||||
} else if (workspaceAgent.display_apps.includes("vscode_insiders")) {
|
||||
expect(
|
||||
screen.getByText(DisplayAppNameMap["vscode_insiders"]),
|
||||
).toBeInTheDocument();
|
||||
} else {
|
||||
expect(screen.queryByText("vscode")).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("vscode_insiders")).not.toBeInTheDocument();
|
||||
}
|
||||
|
||||
// difference between all possible display apps and those displayed
|
||||
const excludedApps = DisplayApps.filter(
|
||||
(a) => !workspaceAgent.display_apps.includes(a),
|
||||
);
|
||||
|
||||
excludedApps.forEach((app) => {
|
||||
expect(
|
||||
screen.queryByText(DisplayAppNameMap[app]),
|
||||
).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
// test empty state
|
||||
if (
|
||||
workspaceAgent.display_apps.length === 0 &&
|
||||
workspaceAgent.apps.length === 0
|
||||
) {
|
||||
expect(screen.getByText("None")).toBeInTheDocument();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -2,7 +2,10 @@ import { type Interpolation, type Theme } from "@emotion/react";
|
||||
import { type FC } from "react";
|
||||
import type { WorkspaceAgent } from "api/typesGenerated";
|
||||
import { Stack } from "../Stack/Stack";
|
||||
import { AppPreviewLink } from "./AppLink/AppPreviewLink";
|
||||
import { AppPreview } from "./AppLink/AppPreview";
|
||||
import { BaseIcon } from "./AppLink/BaseIcon";
|
||||
import { VSCodeIcon } from "components/Icons/VSCodeIcon";
|
||||
import { DisplayAppNameMap } from "./AppLink/AppLink";
|
||||
|
||||
interface AgentRowPreviewStyles {
|
||||
// Helpful when there are more than one row so the values are aligned
|
||||
@@ -86,10 +89,43 @@ export const AgentRowPreview: FC<AgentRowPreviewProps> = ({
|
||||
spacing={0.5}
|
||||
wrap="wrap"
|
||||
>
|
||||
{/* We display all modules returned in agent.apps */}
|
||||
{agent.apps.map((app) => (
|
||||
<AppPreviewLink key={app.slug} app={app} />
|
||||
<AppPreview key={app.slug}>
|
||||
<>
|
||||
<BaseIcon app={app} />
|
||||
{app.display_name}
|
||||
</>
|
||||
</AppPreview>
|
||||
))}
|
||||
{agent.apps.length === 0 && (
|
||||
{/* Additionally, we display any apps that are visible, e.g.
|
||||
apps that are included in agent.display_apps */}
|
||||
{agent.display_apps.includes("web_terminal") && (
|
||||
<AppPreview>{DisplayAppNameMap["web_terminal"]}</AppPreview>
|
||||
)}
|
||||
{agent.display_apps.includes("ssh_helper") && (
|
||||
<AppPreview>{DisplayAppNameMap["ssh_helper"]}</AppPreview>
|
||||
)}
|
||||
{agent.display_apps.includes("port_forwarding_helper") && (
|
||||
<AppPreview>
|
||||
{DisplayAppNameMap["port_forwarding_helper"]}
|
||||
</AppPreview>
|
||||
)}
|
||||
{/* VSCode display apps (vscode, vscode_insiders) get special presentation */}
|
||||
{agent.display_apps.includes("vscode") ? (
|
||||
<AppPreview>
|
||||
<VSCodeIcon sx={{ width: 12, height: 12 }} />
|
||||
{DisplayAppNameMap["vscode"]}
|
||||
</AppPreview>
|
||||
) : (
|
||||
agent.display_apps.includes("vscode_insiders") && (
|
||||
<AppPreview>
|
||||
<VSCodeIcon sx={{ width: 12, height: 12 }} />
|
||||
{DisplayAppNameMap["vscode_insiders"]}
|
||||
</AppPreview>
|
||||
)
|
||||
)}
|
||||
{agent.apps.length === 0 && agent.display_apps.length === 0 && (
|
||||
<span css={styles.agentDataValue}>None</span>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -13,6 +13,14 @@ import { generateRandomString } from "utils/random";
|
||||
import { BaseIcon } from "./BaseIcon";
|
||||
import { ShareIcon } from "./ShareIcon";
|
||||
|
||||
export const DisplayAppNameMap: Record<TypesGen.DisplayApp, string> = {
|
||||
port_forwarding_helper: "Ports",
|
||||
ssh_helper: "SSH",
|
||||
vscode: "VS Code Desktop",
|
||||
vscode_insiders: "VS Code Insiders",
|
||||
web_terminal: "Terminal",
|
||||
};
|
||||
|
||||
const Language = {
|
||||
appTitle: (appName: string, identifier: string): string =>
|
||||
`${appName} - ${identifier}`,
|
||||
|
||||
+3
-12
@@ -1,14 +1,7 @@
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { type FC } from "react";
|
||||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { BaseIcon } from "./BaseIcon";
|
||||
import { ShareIcon } from "./ShareIcon";
|
||||
import { type FC, type PropsWithChildren } from "react";
|
||||
|
||||
interface AppPreviewProps {
|
||||
app: TypesGen.WorkspaceApp;
|
||||
}
|
||||
|
||||
export const AppPreviewLink: FC<AppPreviewProps> = ({ app }) => {
|
||||
export const AppPreview: FC<PropsWithChildren> = ({ children }) => {
|
||||
return (
|
||||
<Stack
|
||||
css={(theme) => ({
|
||||
@@ -29,9 +22,7 @@ export const AppPreviewLink: FC<AppPreviewProps> = ({ app }) => {
|
||||
direction="row"
|
||||
spacing={1}
|
||||
>
|
||||
<BaseIcon app={app} />
|
||||
{app.display_name}
|
||||
<ShareIcon app={app} />
|
||||
{children}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "components/Popover/Popover";
|
||||
import { DisplayAppNameMap } from "./AppLink/AppLink";
|
||||
|
||||
export interface PortForwardButtonProps {
|
||||
host: string;
|
||||
@@ -50,7 +51,7 @@ export const PortForwardButton: FC<PortForwardButtonProps> = (props) => {
|
||||
<Popover>
|
||||
<PopoverTrigger>
|
||||
<AgentButton disabled={!portsQuery.data}>
|
||||
Ports
|
||||
{DisplayAppNameMap["port_forwarding_helper"]}
|
||||
{portsQuery.data ? (
|
||||
<div css={styles.portCount}>{portsQuery.data.ports.length}</div>
|
||||
) : (
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from "components/Popover/Popover";
|
||||
import { Stack } from "components/Stack/Stack";
|
||||
import { AgentButton } from "../AgentButton";
|
||||
import { DisplayAppNameMap } from "../AppLink/AppLink";
|
||||
|
||||
export interface SSHButtonProps {
|
||||
workspaceName: string;
|
||||
@@ -34,7 +35,7 @@ export const SSHButton: FC<PropsWithChildren<SSHButtonProps>> = ({
|
||||
return (
|
||||
<Popover isDefaultOpen={isDefaultOpen}>
|
||||
<PopoverTrigger>
|
||||
<AgentButton>SSH</AgentButton>
|
||||
<AgentButton>{DisplayAppNameMap["ssh_helper"]}</AgentButton>
|
||||
</PopoverTrigger>
|
||||
|
||||
<PopoverContent horizontal="right" classes={{ paper }}>
|
||||
|
||||
@@ -3,9 +3,9 @@ import { AgentButton } from "components/Resources/AgentButton";
|
||||
import { FC } from "react";
|
||||
import * as TypesGen from "api/typesGenerated";
|
||||
import { generateRandomString } from "utils/random";
|
||||
import { DisplayAppNameMap } from "../AppLink/AppLink";
|
||||
|
||||
export const Language = {
|
||||
linkText: "Terminal",
|
||||
terminalTitle: (identifier: string): string => `Terminal - ${identifier}`,
|
||||
};
|
||||
|
||||
@@ -46,7 +46,7 @@ export const TerminalLink: FC<React.PropsWithChildren<TerminalLinkProps>> = ({
|
||||
}}
|
||||
data-testid="terminal"
|
||||
>
|
||||
<AgentButton>{Language.linkText}</AgentButton>
|
||||
<AgentButton>{DisplayAppNameMap["web_terminal"]}</AgentButton>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { useLocalStorage } from "hooks";
|
||||
import Menu from "@mui/material/Menu";
|
||||
import MenuItem from "@mui/material/MenuItem";
|
||||
import { DisplayApp } from "api/typesGenerated";
|
||||
import { DisplayAppNameMap } from "../AppLink/AppLink";
|
||||
|
||||
export interface VSCodeDesktopButtonProps {
|
||||
userName: string;
|
||||
@@ -97,7 +98,7 @@ export const VSCodeDesktopButton: FC<
|
||||
}}
|
||||
>
|
||||
<VSCodeIcon css={{ width: 12, height: 12 }} />
|
||||
VS Code Desktop
|
||||
{DisplayAppNameMap["vscode"]}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
css={{ fontSize: 14 }}
|
||||
@@ -106,7 +107,7 @@ export const VSCodeDesktopButton: FC<
|
||||
}}
|
||||
>
|
||||
<VSCodeInsidersIcon css={{ width: 12, height: 12 }} />
|
||||
VS Code Insiders
|
||||
{DisplayAppNameMap["vscode_insiders"]}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
@@ -156,7 +157,7 @@ const VSCodeButton = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
VS Code Desktop
|
||||
{DisplayAppNameMap["vscode"]}
|
||||
</AgentButton>
|
||||
);
|
||||
};
|
||||
@@ -200,7 +201,7 @@ const VSCodeInsidersButton = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
VS Code Insiders
|
||||
{DisplayAppNameMap["vscode_insiders"]}
|
||||
</AgentButton>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user