From f2a410566c60acfcae17a5adfb8d6fc758860fea Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Wed, 22 Oct 2025 15:35:16 +0200 Subject: [PATCH] feat: add support buttons (#20339) Fixes: https://github.com/coder/coder/issues/16804 --- coderd/apidoc/docs.go | 10 ++- coderd/apidoc/swagger.json | 6 +- codersdk/deployment.go | 11 ++-- docs/reference/api/enterprise.md | 1 + docs/reference/api/general.md | 1 + docs/reference/api/schemas.md | 30 ++++++--- enterprise/coderd/appearance_test.go | 11 ++++ site/src/api/typesGenerated.ts | 1 + site/src/modules/dashboard/Navbar/Navbar.tsx | 9 ++- .../dashboard/Navbar/NavbarView.stories.tsx | 62 ++++++++++++++++++ .../dashboard/Navbar/NavbarView.test.tsx | 4 ++ .../modules/dashboard/Navbar/NavbarView.tsx | 48 +++++++++++++- .../modules/dashboard/Navbar/SupportIcon.tsx | 39 ++++++++++++ .../Navbar/UserDropdown/UserDropdown.tsx | 2 +- .../UserDropdown/UserDropdownContent.test.tsx | 12 +++- .../UserDropdown/UserDropdownContent.tsx | 63 ++++--------------- 16 files changed, 237 insertions(+), 73 deletions(-) create mode 100644 site/src/modules/dashboard/Navbar/SupportIcon.tsx diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 585cc22710..c086141a9b 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -14897,7 +14897,15 @@ const docTemplate = `{ "enum": [ "bug", "chat", - "docs" + "docs", + "star" + ] + }, + "location": { + "type": "string", + "enum": [ + "navbar", + "dropdown" ] }, "name": { diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ac25626a03..3edb176257 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -13489,7 +13489,11 @@ "properties": { "icon": { "type": "string", - "enum": ["bug", "chat", "docs"] + "enum": ["bug", "chat", "docs", "star"] + }, + "location": { + "type": "string", + "enum": ["navbar", "dropdown"] }, "name": { "type": "string" diff --git a/codersdk/deployment.go b/codersdk/deployment.go index 008ae6e2b9..be9131a467 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -984,9 +984,10 @@ func DefaultSupportLinks(docsURL string) []LinkConfig { Icon: "bug", }, { - Name: "Join the Coder Discord", - Target: "https://coder.com/chat?utm_source=coder&utm_medium=coder&utm_campaign=server-footer", - Icon: "chat", + Name: "Join the Coder Discord", + Target: "https://discord.gg/coder", + Icon: "chat", + Location: "navbar", }, { Name: "Star the Repo", @@ -3339,7 +3340,9 @@ type SupportConfig struct { type LinkConfig struct { Name string `json:"name" yaml:"name"` Target string `json:"target" yaml:"target"` - Icon string `json:"icon" yaml:"icon" enums:"bug,chat,docs"` + Icon string `json:"icon" yaml:"icon" enums:"bug,chat,docs,star"` + + Location string `json:"location,omitempty" yaml:"location,omitempty" enums:"navbar,dropdown"` } // Validate checks cross-field constraints for deployment values. diff --git a/docs/reference/api/enterprise.md b/docs/reference/api/enterprise.md index b6043544d4..1ec87e0000 100644 --- a/docs/reference/api/enterprise.md +++ b/docs/reference/api/enterprise.md @@ -120,6 +120,7 @@ curl -X GET http://coder-server:8080/api/v2/appearance \ "support_links": [ { "icon": "bug", + "location": "navbar", "name": "string", "target": "string" } diff --git a/docs/reference/api/general.md b/docs/reference/api/general.md index 0f3330da79..ad15e8fac2 100644 --- a/docs/reference/api/general.md +++ b/docs/reference/api/general.md @@ -473,6 +473,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "value": [ { "icon": "bug", + "location": "navbar", "name": "string", "target": "string" } diff --git a/docs/reference/api/schemas.md b/docs/reference/api/schemas.md index a36199a6f6..12fca35a84 100644 --- a/docs/reference/api/schemas.md +++ b/docs/reference/api/schemas.md @@ -1110,6 +1110,7 @@ "support_links": [ { "icon": "bug", + "location": "navbar", "name": "string", "target": "string" } @@ -3138,6 +3139,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "value": [ { "icon": "bug", + "location": "navbar", "name": "string", "target": "string" } @@ -3644,6 +3646,7 @@ CreateWorkspaceRequest provides options for creating a new workspace. Only one o "value": [ { "icon": "bug", + "location": "navbar", "name": "string", "target": "string" } @@ -4752,6 +4755,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ```json { "icon": "bug", + "location": "navbar", "name": "string", "target": "string" } @@ -4759,19 +4763,23 @@ Only certain features set these fields: - FeatureManagedAgentLimit| ### Properties -| Name | Type | Required | Restrictions | Description | -|----------|--------|----------|--------------|-------------| -| `icon` | string | false | | | -| `name` | string | false | | | -| `target` | string | false | | | +| Name | Type | Required | Restrictions | Description | +|------------|--------|----------|--------------|-------------| +| `icon` | string | false | | | +| `location` | string | false | | | +| `name` | string | false | | | +| `target` | string | false | | | #### Enumerated Values -| Property | Value | -|----------|--------| -| `icon` | `bug` | -| `icon` | `chat` | -| `icon` | `docs` | +| Property | Value | +|------------|------------| +| `icon` | `bug` | +| `icon` | `chat` | +| `icon` | `docs` | +| `icon` | `star` | +| `location` | `navbar` | +| `location` | `dropdown` | ## codersdk.ListInboxNotificationsResponse @@ -7625,6 +7633,7 @@ Only certain features set these fields: - FeatureManagedAgentLimit| "value": [ { "icon": "bug", + "location": "navbar", "name": "string", "target": "string" } @@ -13685,6 +13694,7 @@ None "value": [ { "icon": "bug", + "location": "navbar", "name": "string", "target": "string" } diff --git a/enterprise/coderd/appearance_test.go b/enterprise/coderd/appearance_test.go index 81ba7eddc7..8255dd4c8a 100644 --- a/enterprise/coderd/appearance_test.go +++ b/enterprise/coderd/appearance_test.go @@ -201,6 +201,17 @@ func TestCustomSupportLinks(t *testing.T) { Target: "http://second-link-2", Icon: "bug", }, + { + Name: "First button", + Target: "http://first-button-1", + Icon: "bug", + Location: "navbar", + }, + { + Name: "Third link", + Target: "http://third-link-3", + Icon: "star", + }, } cfg := coderdtest.DeploymentValues(t) cfg.Support.Links = serpent.Struct[[]codersdk.LinkConfig]{ diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 65b1d2d3e5..91b685c0f5 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -2515,6 +2515,7 @@ export interface LinkConfig { readonly name: string; readonly target: string; readonly icon: string; + readonly location?: string; } // From codersdk/inboxnotification.go diff --git a/site/src/modules/dashboard/Navbar/Navbar.tsx b/site/src/modules/dashboard/Navbar/Navbar.tsx index 8db0252c00..012053341e 100644 --- a/site/src/modules/dashboard/Navbar/Navbar.tsx +++ b/site/src/modules/dashboard/Navbar/Navbar.tsx @@ -1,4 +1,5 @@ import { buildInfo } from "api/queries/buildInfo"; +import type { LinkConfig } from "api/typesGenerated"; import { useProxy } from "contexts/ProxyContext"; import { useAuthenticated } from "hooks"; import { useEmbeddedMetadata } from "hooks/useEmbeddedMetadata"; @@ -25,12 +26,18 @@ export const Navbar: FC = () => { const canViewConnectionLog = featureVisibility.connection_log && permissions.viewAnyConnectionLog; + const uniqueLinks = new Map(); + for (const link of appearance.support_links ?? []) { + if (!uniqueLinks.has(link.name)) { + uniqueLinks.set(link.name, link); + } + } return ( = { canViewDeployment: true, canViewHealth: true, canViewOrganizations: true, + supportLinks: [], }, decorators: [withDashboardProvider], }; @@ -129,3 +130,64 @@ export const IdleTasks: Story = { ], }, }; + +export const SupportLinks: Story = { + args: { + user: MockUserMember, + canViewAuditLog: false, + canViewDeployment: false, + canViewHealth: false, + canViewOrganizations: false, + supportLinks: [ + { + name: "This is a bug", + icon: "bug", + target: "#", + }, + { + name: "This is a star", + icon: "star", + target: "#", + location: "navbar", + }, + { + name: "This is a chat", + icon: "chat", + target: "#", + location: "navbar", + }, + { + name: "No icon here", + icon: "", + target: "#", + location: "navbar", + }, + { + name: "No icon here too", + icon: "", + target: "#", + }, + ], + }, +}; + +export const DefaultSupportLinks: Story = { + args: { + user: MockUserMember, + canViewAuditLog: false, + canViewDeployment: false, + canViewHealth: false, + canViewOrganizations: false, + supportLinks: [ + { icon: "docs", name: "Documentation", target: "" }, + { icon: "bug", name: "Report a bug", target: "" }, + { + icon: "chat", + name: "Join the Coder Discord", + target: "", + location: "navbar", + }, + { icon: "star", name: "Star the Repo", target: "" }, + ], + }, +}; diff --git a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx index 9d011089ba..f313b6aa2b 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.test.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.test.tsx @@ -34,6 +34,7 @@ describe("NavbarView", () => { canViewHealth canViewAuditLog canViewConnectionLog + supportLinks={[]} />, ); const workspacesLink = @@ -52,6 +53,7 @@ describe("NavbarView", () => { canViewHealth canViewAuditLog canViewConnectionLog + supportLinks={[]} />, ); const templatesLink = @@ -70,6 +72,7 @@ describe("NavbarView", () => { canViewHealth canViewAuditLog canViewConnectionLog + supportLinks={[]} />, ); const deploymentMenu = await screen.findByText("Admin settings"); @@ -89,6 +92,7 @@ describe("NavbarView", () => { canViewHealth canViewAuditLog canViewConnectionLog + supportLinks={[]} />, ); const deploymentMenu = await screen.findByText("Admin settings"); diff --git a/site/src/modules/dashboard/Navbar/NavbarView.tsx b/site/src/modules/dashboard/Navbar/NavbarView.tsx index 0cafaa8fdd..a4107f0b84 100644 --- a/site/src/modules/dashboard/Navbar/NavbarView.tsx +++ b/site/src/modules/dashboard/Navbar/NavbarView.tsx @@ -21,13 +21,14 @@ import { cn } from "utils/cn"; import { DeploymentDropdown } from "./DeploymentDropdown"; import { MobileMenu } from "./MobileMenu"; import { ProxyMenu } from "./ProxyMenu"; +import { SupportIcon } from "./SupportIcon"; import { UserDropdown } from "./UserDropdown/UserDropdown"; interface NavbarViewProps { logo_url?: string; user: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; - supportLinks?: readonly TypesGen.LinkConfig[]; + supportLinks: readonly TypesGen.LinkConfig[]; onSignOut: () => void; canViewDeployment: boolean; canViewOrganizations: boolean; @@ -71,6 +72,16 @@ export const NavbarView: FC = ({
+ {supportLinks.filter(isNavbarLink).map((link) => ( +
+ +
+ ))} + {proxyContextValue && (
@@ -121,7 +132,7 @@ export const NavbarView: FC = ({ !isNavbarLink(link))} onSignOut={onSignOut} />
@@ -240,3 +251,36 @@ const TasksNavItem: FC = ({ user }) => { function idleTasksLabel(count: number) { return `You have ${count} ${count === 1 ? "task" : "tasks"} waiting for input`; } + +function isNavbarLink(link: TypesGen.LinkConfig): boolean { + return link.location === "navbar"; +} + +interface SupportButtonProps { + name: string; + target: string; + icon: string; + location?: string; +} + +const SupportButton: FC = ({ name, target, icon }) => { + return ( + + ); +}; diff --git a/site/src/modules/dashboard/Navbar/SupportIcon.tsx b/site/src/modules/dashboard/Navbar/SupportIcon.tsx new file mode 100644 index 0000000000..6c32f03aea --- /dev/null +++ b/site/src/modules/dashboard/Navbar/SupportIcon.tsx @@ -0,0 +1,39 @@ +import type { SvgIconProps } from "@mui/material/SvgIcon"; +import { ExternalImage } from "components/ExternalImage/ExternalImage"; +import { BookOpenTextIcon, BugIcon, MessageSquareIcon } from "lucide-react"; +import type { FC } from "react"; + +interface SupportIconProps { + icon: string; + className?: string; +} + +export const SupportIcon: FC = ({ icon, className }) => { + switch (icon) { + case "bug": + return ; + case "chat": + return ; + case "docs": + return ; + case "star": + return ; + default: + return ; + } +}; + +const GithubStar: FC = (props) => ( + +); diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx index 6240a68c95..4c796ee436 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdown.tsx @@ -11,7 +11,7 @@ import { UserDropdownContent } from "./UserDropdownContent"; interface UserDropdownProps { user: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; - supportLinks?: readonly TypesGen.LinkConfig[]; + supportLinks: readonly TypesGen.LinkConfig[]; onSignOut: () => void; } diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.test.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.test.tsx index 70e2f35c94..1d25d894ea 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.test.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.test.tsx @@ -8,7 +8,11 @@ describe("UserDropdownContent", () => { it("has the correct link for the account item", async () => { render( - + , ); await waitForLoaderToBeRemoved(); @@ -25,7 +29,11 @@ describe("UserDropdownContent", () => { const onSignOut = jest.fn(); render( - + , ); await waitForLoaderToBeRemoved(); diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index fd0636da3d..b56c1c67de 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -6,24 +6,20 @@ import { } from "@emotion/react"; import Divider from "@mui/material/Divider"; import MenuItem from "@mui/material/MenuItem"; -import type { SvgIconProps } from "@mui/material/SvgIcon"; import Tooltip from "@mui/material/Tooltip"; import { PopoverClose } from "@radix-ui/react-popover"; import type * as TypesGen from "api/typesGenerated"; import { CopyButton } from "components/CopyButton/CopyButton"; -import { ExternalImage } from "components/ExternalImage/ExternalImage"; import { Stack } from "components/Stack/Stack"; import { - BookOpenTextIcon, - BugIcon, CircleUserIcon, LogOutIcon, - MessageSquareIcon, MonitorDownIcon, SquareArrowOutUpRightIcon, } from "lucide-react"; -import type { FC, JSX } from "react"; +import type { FC } from "react"; import { Link } from "react-router"; +import { SupportIcon } from "../SupportIcon"; export const Language = { accountLabel: "Account", @@ -34,7 +30,7 @@ export const Language = { interface UserDropdownContentProps { user: TypesGen.User; buildInfo?: TypesGen.BuildInfoResponse; - supportLinks?: readonly TypesGen.LinkConfig[]; + supportLinks: readonly TypesGen.LinkConfig[]; onSignOut: () => void; } @@ -44,26 +40,6 @@ export const UserDropdownContent: FC = ({ supportLinks, onSignOut, }) => { - const renderMenuIcon = (icon: string): JSX.Element => { - switch (icon) { - case "bug": - return ; - case "chat": - return ; - case "docs": - return ; - case "star": - return ; - default: - return ( - - ); - } - }; - return (
@@ -76,7 +52,7 @@ export const UserDropdownContent: FC = ({ - + Install CLI @@ -85,14 +61,14 @@ export const UserDropdownContent: FC = ({ - + {Language.accountLabel} - + {Language.signOutLabel} @@ -109,7 +85,12 @@ export const UserDropdownContent: FC = ({ > - {renderMenuIcon(link.icon)} + {link.icon && ( + + )} {link.name} @@ -152,21 +133,6 @@ export const UserDropdownContent: FC = ({ ); }; -const GithubStar: FC = (props) => ( - -); - const styles = { info: (theme) => [ theme.typography.body2 as CSSObject, @@ -196,11 +162,6 @@ const styles = { transition: background-color 0.3s ease; } `, - menuItemIcon: (theme) => ({ - color: theme.palette.text.secondary, - width: 20, - height: 20, - }), menuItemText: { fontSize: 14, },