mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: Enable custom support links (#6313)
* backend: support links * frontend: Support links * fmt * test: CODER_SUPPORT_LINKS_0_NAME * Go tests * Use UpdateAppearanceConfig * ui: UpdateAppearanceConfig * fix: fmt * Fix: site * Fix: site tests * fix: fmt * fix * test: check default support links
This commit is contained in:
@@ -34,6 +34,7 @@ export interface AppHostResponse {
|
||||
export interface AppearanceConfig {
|
||||
readonly logo_url: string
|
||||
readonly service_banner: ServiceBannerConfig
|
||||
readonly support_links?: LinkConfig[]
|
||||
}
|
||||
|
||||
// From codersdk/roles.go
|
||||
@@ -334,6 +335,7 @@ export interface DeploymentConfig {
|
||||
readonly disable_password_auth: DeploymentConfigField<boolean>
|
||||
readonly address: DeploymentConfigField<string>
|
||||
readonly experimental: DeploymentConfigField<boolean>
|
||||
readonly support: SupportConfig
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
@@ -434,6 +436,13 @@ export interface License {
|
||||
readonly claims: Record<string, any>
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface LinkConfig {
|
||||
readonly name: string
|
||||
readonly target: string
|
||||
readonly icon: string
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface LoggingConfig {
|
||||
readonly human: DeploymentConfigField<string>
|
||||
@@ -657,6 +666,11 @@ export interface ServiceBannerConfig {
|
||||
readonly background_color?: string
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface SupportConfig {
|
||||
readonly links: DeploymentConfigField<LinkConfig[]>
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface SwaggerConfig {
|
||||
readonly enable: DeploymentConfigField<boolean>
|
||||
@@ -818,6 +832,12 @@ export interface UpdateActiveTemplateVersion {
|
||||
readonly id: string
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface UpdateAppearanceConfig {
|
||||
readonly logo_url: string
|
||||
readonly service_banner: ServiceBannerConfig
|
||||
}
|
||||
|
||||
// From codersdk/updatecheck.go
|
||||
export interface UpdateCheckResponse {
|
||||
readonly current: boolean
|
||||
@@ -1331,4 +1351,10 @@ export const WorkspaceTransitions: WorkspaceTransition[] = [
|
||||
]
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export type Flaggable = string | number | boolean | string[] | GitAuthConfig[]
|
||||
export type Flaggable =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| string[]
|
||||
| GitAuthConfig[]
|
||||
| LinkConfig[]
|
||||
|
||||
@@ -22,6 +22,7 @@ export const Navbar: FC = () => {
|
||||
user={me}
|
||||
logo_url={appearance.config.logo_url}
|
||||
buildInfo={buildInfo}
|
||||
supportLinks={appearance.config.support_links}
|
||||
onSignOut={onSignOut}
|
||||
canViewAuditLog={canViewAuditLog}
|
||||
canViewDeployment={canViewDeployment}
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface NavbarViewProps {
|
||||
logo_url?: string
|
||||
user?: TypesGen.User
|
||||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
supportLinks?: TypesGen.LinkConfig[]
|
||||
onSignOut: () => void
|
||||
canViewAuditLog: boolean
|
||||
canViewDeployment: boolean
|
||||
@@ -86,6 +87,7 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
user,
|
||||
logo_url,
|
||||
buildInfo,
|
||||
supportLinks,
|
||||
onSignOut,
|
||||
canViewAuditLog,
|
||||
canViewDeployment,
|
||||
@@ -147,6 +149,7 @@ export const NavbarView: React.FC<React.PropsWithChildren<NavbarViewProps>> = ({
|
||||
<UserDropdown
|
||||
user={user}
|
||||
buildInfo={buildInfo}
|
||||
supportLinks={supportLinks}
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fireEvent, screen } from "@testing-library/react"
|
||||
import { MockUser } from "../../testHelpers/entities"
|
||||
import { MockSupportLinks, MockUser } from "../../testHelpers/entities"
|
||||
import { render } from "../../testHelpers/renderHelpers"
|
||||
import { Language } from "../UserDropdownContent/UserDropdownContent"
|
||||
import { UserDropdown, UserDropdownProps } from "./UsersDropdown"
|
||||
@@ -8,6 +8,7 @@ const renderAndClick = async (props: Partial<UserDropdownProps> = {}) => {
|
||||
render(
|
||||
<UserDropdown
|
||||
user={props.user ?? MockUser}
|
||||
supportLinks={MockSupportLinks}
|
||||
onSignOut={props.onSignOut ?? jest.fn()}
|
||||
/>,
|
||||
)
|
||||
@@ -20,7 +21,9 @@ describe("UserDropdown", () => {
|
||||
it("opens the menu", async () => {
|
||||
await renderAndClick()
|
||||
expect(screen.getByText(Language.accountLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.docsLabel)).toBeDefined()
|
||||
expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined()
|
||||
expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined()
|
||||
expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined()
|
||||
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -13,12 +13,14 @@ import { UserDropdownContent } from "../UserDropdownContent/UserDropdownContent"
|
||||
export interface UserDropdownProps {
|
||||
user: TypesGen.User
|
||||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
supportLinks?: TypesGen.LinkConfig[]
|
||||
onSignOut: () => void
|
||||
}
|
||||
|
||||
export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
|
||||
buildInfo,
|
||||
user,
|
||||
supportLinks,
|
||||
onSignOut,
|
||||
}: UserDropdownProps) => {
|
||||
const styles = useStyles()
|
||||
@@ -69,6 +71,7 @@ export const UserDropdown: FC<PropsWithChildren<UserDropdownProps>> = ({
|
||||
<UserDropdownContent
|
||||
user={user}
|
||||
buildInfo={buildInfo}
|
||||
supportLinks={supportLinks}
|
||||
onPopoverClose={onPopoverClose}
|
||||
onSignOut={onSignOut}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { screen } from "@testing-library/react"
|
||||
import { MockBuildInfo, MockUser } from "../../testHelpers/entities"
|
||||
import {
|
||||
MockBuildInfo,
|
||||
MockSupportLinks,
|
||||
MockUser,
|
||||
} from "../../testHelpers/entities"
|
||||
import { render } from "../../testHelpers/renderHelpers"
|
||||
import { Language, UserDropdownContent } from "./UserDropdownContent"
|
||||
|
||||
@@ -21,16 +25,23 @@ describe("UserDropdownContent", () => {
|
||||
<UserDropdownContent
|
||||
user={MockUser}
|
||||
buildInfo={MockBuildInfo}
|
||||
supportLinks={MockSupportLinks}
|
||||
onSignOut={jest.fn()}
|
||||
onPopoverClose={jest.fn()}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText(Language.accountLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.docsLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.signOutLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.bugLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.discordLabel)).toBeDefined()
|
||||
expect(screen.getByText(Language.copyrightText)).toBeDefined()
|
||||
expect(screen.getByText(MockSupportLinks[0].name)).toBeDefined()
|
||||
expect(screen.getByText(MockSupportLinks[1].name)).toBeDefined()
|
||||
expect(screen.getByText(MockSupportLinks[2].name)).toBeDefined()
|
||||
expect(
|
||||
screen.getByText(MockSupportLinks[2].name).closest("a"),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
"https://github.com/coder/coder/issues/new?labels=needs+grooming&body=Version%3A%20%5B%60v99.999.9999%2Bc9cdf14%60%5D(file%3A%2F%2F%2Fmock-url)",
|
||||
)
|
||||
expect(screen.getByText(MockBuildInfo.version)).toBeDefined()
|
||||
})
|
||||
|
||||
|
||||
@@ -15,16 +15,14 @@ import { combineClasses } from "util/combineClasses"
|
||||
|
||||
export const Language = {
|
||||
accountLabel: "Account",
|
||||
docsLabel: "Documentation",
|
||||
signOutLabel: "Sign Out",
|
||||
bugLabel: "Report a Bug",
|
||||
discordLabel: "Join the Coder Discord",
|
||||
copyrightText: `\u00a9 ${new Date().getFullYear()} Coder Technologies, Inc.`,
|
||||
}
|
||||
|
||||
export interface UserDropdownContentProps {
|
||||
user: TypesGen.User
|
||||
buildInfo?: TypesGen.BuildInfoResponse
|
||||
supportLinks?: TypesGen.LinkConfig[]
|
||||
onPopoverClose: () => void
|
||||
onSignOut: () => void
|
||||
}
|
||||
@@ -32,14 +30,11 @@ export interface UserDropdownContentProps {
|
||||
export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||
buildInfo,
|
||||
user,
|
||||
supportLinks,
|
||||
onPopoverClose,
|
||||
onSignOut,
|
||||
}) => {
|
||||
const styles = useStyles()
|
||||
const githubUrl = `https://github.com/coder/coder/issues/new?labels=needs+grooming&body=${encodeURIComponent(`Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})
|
||||
|
||||
<!--- Ask a question or leave feedback! -->`)}`
|
||||
const discordUrl = `https://coder.com/chat?utm_source=coder&utm_medium=coder&utm_campaign=server-footer`
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -64,43 +59,33 @@ export const UserDropdownContent: FC<UserDropdownContentProps> = ({
|
||||
|
||||
<Divider className={styles.divider} />
|
||||
|
||||
<a
|
||||
href="https://coder.com/docs/coder-oss"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
|
||||
<DocsIcon className={styles.menuItemIcon} />
|
||||
<span className={styles.menuItemText}>{Language.docsLabel}</span>
|
||||
</MenuItem>
|
||||
</a>
|
||||
<>
|
||||
{supportLinks &&
|
||||
supportLinks.map((link) => (
|
||||
<a
|
||||
href={includeBuildInfo(link.target, buildInfo)}
|
||||
key={link.name}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
|
||||
{link.icon === "bug" && (
|
||||
<BugIcon className={styles.menuItemIcon} />
|
||||
)}
|
||||
{link.icon === "chat" && (
|
||||
<ChatIcon className={styles.menuItemIcon} />
|
||||
)}
|
||||
{link.icon === "docs" && (
|
||||
<DocsIcon className={styles.menuItemIcon} />
|
||||
)}
|
||||
<span className={styles.menuItemText}>{link.name}</span>
|
||||
</MenuItem>
|
||||
</a>
|
||||
))}
|
||||
</>
|
||||
|
||||
<a
|
||||
href={githubUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
|
||||
<BugIcon className={styles.menuItemIcon} />
|
||||
<span className={styles.menuItemText}>{Language.bugLabel}</span>
|
||||
</MenuItem>
|
||||
</a>
|
||||
|
||||
<a
|
||||
href={discordUrl}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className={styles.link}
|
||||
>
|
||||
<MenuItem className={styles.menuItem} onClick={onPopoverClose}>
|
||||
<ChatIcon className={styles.menuItemIcon} />
|
||||
<span className={styles.menuItemText}>{Language.discordLabel}</span>
|
||||
</MenuItem>
|
||||
</a>
|
||||
|
||||
<Divider className={styles.divider} />
|
||||
{supportLinks && <Divider className={styles.divider} />}
|
||||
|
||||
<Stack className={styles.info} spacing={0}>
|
||||
<a
|
||||
@@ -181,3 +166,15 @@ const useStyles = makeStyles((theme) => ({
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
}))
|
||||
|
||||
const includeBuildInfo = (
|
||||
href: string,
|
||||
buildInfo?: TypesGen.BuildInfoResponse,
|
||||
): string => {
|
||||
return href.replace(
|
||||
"{CODER_BUILD_INFO}",
|
||||
`${encodeURIComponent(
|
||||
`Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})`,
|
||||
)}`,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AppearanceConfig } from "api/typesGenerated"
|
||||
import { UpdateAppearanceConfig } from "api/typesGenerated"
|
||||
import { useDashboard } from "components/Dashboard/DashboardProvider"
|
||||
import { FC } from "react"
|
||||
import { Helmet } from "react-helmet-async"
|
||||
@@ -15,7 +15,7 @@ const AppearanceSettingsPage: FC = () => {
|
||||
entitlements.features["appearance"].entitlement !== "not_entitled"
|
||||
|
||||
const updateAppearance = (
|
||||
newConfig: Partial<AppearanceConfig>,
|
||||
newConfig: Partial<UpdateAppearanceConfig>,
|
||||
preview: boolean,
|
||||
) => {
|
||||
const newAppearance = {
|
||||
|
||||
+18
-16
@@ -17,16 +17,16 @@ import { useTranslation } from "react-i18next"
|
||||
import makeStyles from "@material-ui/core/styles/makeStyles"
|
||||
import Switch from "@material-ui/core/Switch"
|
||||
import TextField from "@material-ui/core/TextField"
|
||||
import { AppearanceConfig } from "api/typesGenerated"
|
||||
import { UpdateAppearanceConfig } from "api/typesGenerated"
|
||||
import { Stack } from "components/Stack/Stack"
|
||||
import { useFormik } from "formik"
|
||||
import { useTheme } from "@material-ui/core/styles"
|
||||
|
||||
export type AppearanceSettingsPageViewProps = {
|
||||
appearance: AppearanceConfig
|
||||
appearance: UpdateAppearanceConfig
|
||||
isEntitled: boolean
|
||||
updateAppearance: (
|
||||
newConfig: Partial<AppearanceConfig>,
|
||||
newConfig: Partial<UpdateAppearanceConfig>,
|
||||
preview: boolean,
|
||||
) => void
|
||||
}
|
||||
@@ -48,20 +48,22 @@ export const AppearanceSettingsPageView = ({
|
||||
})
|
||||
const logoFieldHelpers = getFormHelpers(logoForm)
|
||||
|
||||
const serviceBannerForm = useFormik<AppearanceConfig["service_banner"]>({
|
||||
initialValues: {
|
||||
message: appearance.service_banner.message,
|
||||
enabled: appearance.service_banner.enabled,
|
||||
background_color: appearance.service_banner.background_color,
|
||||
const serviceBannerForm = useFormik<UpdateAppearanceConfig["service_banner"]>(
|
||||
{
|
||||
initialValues: {
|
||||
message: appearance.service_banner.message,
|
||||
enabled: appearance.service_banner.enabled,
|
||||
background_color: appearance.service_banner.background_color,
|
||||
},
|
||||
onSubmit: (values) =>
|
||||
updateAppearance(
|
||||
{
|
||||
service_banner: values,
|
||||
},
|
||||
false,
|
||||
),
|
||||
},
|
||||
onSubmit: (values) =>
|
||||
updateAppearance(
|
||||
{
|
||||
service_banner: values,
|
||||
},
|
||||
false,
|
||||
),
|
||||
})
|
||||
)
|
||||
const serviceBannerFieldHelpers = getFormHelpers(serviceBannerForm)
|
||||
const [backgroundColor, setBackgroundColor] = useState(
|
||||
serviceBannerForm.values.background_color,
|
||||
|
||||
@@ -60,6 +60,25 @@ export const MockBuildInfo: TypesGen.BuildInfoResponse = {
|
||||
version: "v99.999.9999+c9cdf14",
|
||||
}
|
||||
|
||||
export const MockSupportLinks: TypesGen.LinkConfig[] = [
|
||||
{
|
||||
name: "First link",
|
||||
target: "http://first-link",
|
||||
icon: "chat",
|
||||
},
|
||||
{
|
||||
name: "Second link",
|
||||
target: "http://second-link",
|
||||
icon: "docs",
|
||||
},
|
||||
{
|
||||
name: "Third link",
|
||||
target:
|
||||
"https://github.com/coder/coder/issues/new?labels=needs+grooming&body={CODER_BUILD_INFO}",
|
||||
icon: "",
|
||||
},
|
||||
]
|
||||
|
||||
export const MockUpdateCheck: TypesGen.UpdateCheckResponse = {
|
||||
current: true,
|
||||
url: "file:///mock-url",
|
||||
|
||||
Reference in New Issue
Block a user