mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: display user apps in the workspaces table (#17744)
Related to https://github.com/coder/coder/issues/17311 **Demo:** <img width="1511" alt="Screenshot 2025-05-09 at 11 46 59" src="https://github.com/user-attachments/assets/3e9ba618-ed5d-4eeb-996f-d7bcceb9f1a9" />
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
MockTemplate,
|
||||
MockUserOwner,
|
||||
MockWorkspace,
|
||||
MockWorkspaceAgent,
|
||||
MockWorkspaceAppStatus,
|
||||
mockApiError,
|
||||
} from "testHelpers/entities";
|
||||
@@ -299,6 +300,42 @@ export const InvalidPageNumber: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleApps: Story = {
|
||||
args: {
|
||||
workspaces: [
|
||||
{
|
||||
...MockWorkspace,
|
||||
latest_build: {
|
||||
...MockWorkspace.latest_build,
|
||||
resources: [
|
||||
{
|
||||
...MockWorkspace.latest_build.resources[0],
|
||||
agents: [
|
||||
{
|
||||
...MockWorkspaceAgent,
|
||||
apps: [
|
||||
{
|
||||
...MockWorkspaceAgent.apps[0],
|
||||
display_name: "App 1",
|
||||
id: "app-1",
|
||||
},
|
||||
{
|
||||
...MockWorkspaceAgent.apps[0],
|
||||
display_name: "App 2",
|
||||
id: "app-2",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
count: allWorkspaces.length,
|
||||
},
|
||||
};
|
||||
|
||||
export const ShowOrganizations: Story = {
|
||||
args: {
|
||||
workspaces: [{ ...MockWorkspace, organization_name: "limbus-co" }],
|
||||
|
||||
@@ -19,6 +19,7 @@ import { Avatar } from "components/Avatar/Avatar";
|
||||
import { AvatarData } from "components/Avatar/AvatarData";
|
||||
import { AvatarDataSkeleton } from "components/Avatar/AvatarDataSkeleton";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { VSCodeIcon } from "components/Icons/VSCodeIcon";
|
||||
import { VSCodeInsidersIcon } from "components/Icons/VSCodeInsidersIcon";
|
||||
import { InfoTooltip } from "components/InfoTooltip/InfoTooltip";
|
||||
@@ -63,6 +64,7 @@ import {
|
||||
getVSCodeHref,
|
||||
openAppInNewWindow,
|
||||
} from "modules/apps/apps";
|
||||
import { useAppLink } from "modules/apps/useAppLink";
|
||||
import { useDashboard } from "modules/dashboard/useDashboard";
|
||||
import { WorkspaceAppStatus } from "modules/workspaces/WorkspaceAppStatus/WorkspaceAppStatus";
|
||||
import { WorkspaceDormantBadge } from "modules/workspaces/WorkspaceDormantBadge/WorkspaceDormantBadge";
|
||||
@@ -622,6 +624,9 @@ const PrimaryAction: FC<PrimaryActionProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
// The total number of apps that can be displayed in the workspace row
|
||||
const WORKSPACE_APPS_SLOTS = 4;
|
||||
|
||||
type WorkspaceAppsProps = {
|
||||
workspace: Workspace;
|
||||
};
|
||||
@@ -647,11 +652,18 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const builtinApps = new Set(agent.display_apps);
|
||||
builtinApps.delete("port_forwarding_helper");
|
||||
builtinApps.delete("ssh_helper");
|
||||
|
||||
const remainingSlots = WORKSPACE_APPS_SLOTS - builtinApps.size;
|
||||
const userApps = agent.apps.slice(0, remainingSlots);
|
||||
|
||||
const buttons: ReactNode[] = [];
|
||||
|
||||
if (agent.display_apps.includes("vscode")) {
|
||||
if (builtinApps.has("vscode")) {
|
||||
buttons.push(
|
||||
<AppLink
|
||||
<BaseIconLink
|
||||
key="vscode"
|
||||
isLoading={!token}
|
||||
label="Open VSCode"
|
||||
@@ -664,13 +676,13 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
|
||||
})}
|
||||
>
|
||||
<VSCodeIcon />
|
||||
</AppLink>,
|
||||
</BaseIconLink>,
|
||||
);
|
||||
}
|
||||
|
||||
if (agent.display_apps.includes("vscode_insiders")) {
|
||||
if (builtinApps.has("vscode_insiders")) {
|
||||
buttons.push(
|
||||
<AppLink
|
||||
<BaseIconLink
|
||||
key="vscode-insiders"
|
||||
label="Open VSCode Insiders"
|
||||
isLoading={!token}
|
||||
@@ -683,18 +695,29 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
|
||||
})}
|
||||
>
|
||||
<VSCodeInsidersIcon />
|
||||
</AppLink>,
|
||||
</BaseIconLink>,
|
||||
);
|
||||
}
|
||||
|
||||
if (agent.display_apps.includes("web_terminal")) {
|
||||
for (const app of userApps) {
|
||||
buttons.push(
|
||||
<IconAppLink
|
||||
key={app.id}
|
||||
app={app}
|
||||
workspace={workspace}
|
||||
agent={agent}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
|
||||
if (builtinApps.has("web_terminal")) {
|
||||
const href = getTerminalHref({
|
||||
username: workspace.owner_name,
|
||||
workspace: workspace.name,
|
||||
agent: agent.name,
|
||||
});
|
||||
buttons.push(
|
||||
<AppLink
|
||||
<BaseIconLink
|
||||
key="terminal"
|
||||
href={href}
|
||||
onClick={(e) => {
|
||||
@@ -704,21 +727,45 @@ const WorkspaceApps: FC<WorkspaceAppsProps> = ({ workspace }) => {
|
||||
label="Open Terminal"
|
||||
>
|
||||
<SquareTerminalIcon />
|
||||
</AppLink>,
|
||||
</BaseIconLink>,
|
||||
);
|
||||
}
|
||||
|
||||
return buttons;
|
||||
};
|
||||
|
||||
type AppLinkProps = PropsWithChildren<{
|
||||
type IconAppLinkProps = {
|
||||
app: WorkspaceApp;
|
||||
workspace: Workspace;
|
||||
agent: WorkspaceAgent;
|
||||
};
|
||||
|
||||
const IconAppLink: FC<IconAppLinkProps> = ({ app, workspace, agent }) => {
|
||||
const link = useAppLink(app, {
|
||||
workspace,
|
||||
agent,
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseIconLink
|
||||
key={app.id}
|
||||
label={`Open ${link.label}`}
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
>
|
||||
<ExternalImage src={app.icon ?? "/icon/widgets.svg"} />
|
||||
</BaseIconLink>
|
||||
);
|
||||
};
|
||||
|
||||
type BaseIconLinkProps = PropsWithChildren<{
|
||||
label: string;
|
||||
href: string;
|
||||
isLoading?: boolean;
|
||||
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
}>;
|
||||
|
||||
const AppLink: FC<AppLinkProps> = ({
|
||||
const BaseIconLink: FC<BaseIconLinkProps> = ({
|
||||
href,
|
||||
isLoading,
|
||||
label,
|
||||
|
||||
@@ -896,17 +896,10 @@ export const MockWorkspaceApp: TypesGen.WorkspaceApp = {
|
||||
id: "test-app",
|
||||
slug: "test-app",
|
||||
display_name: "Test App",
|
||||
icon: "",
|
||||
subdomain: false,
|
||||
health: "disabled",
|
||||
external: false,
|
||||
url: "",
|
||||
sharing_level: "owner",
|
||||
healthcheck: {
|
||||
url: "",
|
||||
interval: 0,
|
||||
threshold: 0,
|
||||
},
|
||||
hidden: false,
|
||||
open_in: "slim-window",
|
||||
statuses: [],
|
||||
|
||||
Reference in New Issue
Block a user