diff --git a/coderd/appearance/appearance.go b/coderd/appearance/appearance.go index 452ba071e1..a22380a2d5 100644 --- a/coderd/appearance/appearance.go +++ b/coderd/appearance/appearance.go @@ -2,7 +2,10 @@ package appearance import ( "context" + "fmt" + "strings" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/codersdk" ) @@ -10,36 +13,50 @@ type Fetcher interface { Fetch(ctx context.Context) (codersdk.AppearanceConfig, error) } -var DefaultSupportLinks = []codersdk.LinkConfig{ - { - Name: "Documentation", - Target: "https://coder.com/docs/coder-oss", - Icon: "docs", - }, - { - Name: "Report a bug", - Target: "https://github.com/coder/coder/issues/new?labels=needs+grooming&body={CODER_BUILD_INFO}", - 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: "Star the Repo", - Target: "https://github.com/coder/coder", - Icon: "star", - }, +func DefaultSupportLinks(docsURL string) []codersdk.LinkConfig { + version := buildinfo.Version() + if docsURL == "" { + docsURL = "https://coder.com/docs/@" + strings.Split(version, "-")[0] + } + buildInfo := fmt.Sprintf("Version: [`%s`](%s)", version, buildinfo.ExternalURL()) + + return []codersdk.LinkConfig{ + { + Name: "Documentation", + Target: docsURL, + Icon: "docs", + }, + { + Name: "Report a bug", + Target: "https://github.com/coder/coder/issues/new?labels=needs+grooming&body=" + buildInfo, + 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: "Star the Repo", + Target: "https://github.com/coder/coder", + Icon: "star", + }, + } } -type AGPLFetcher struct{} +type AGPLFetcher struct { + docsURL string +} -func (AGPLFetcher) Fetch(context.Context) (codersdk.AppearanceConfig, error) { +func (f AGPLFetcher) Fetch(context.Context) (codersdk.AppearanceConfig, error) { return codersdk.AppearanceConfig{ AnnouncementBanners: []codersdk.BannerConfig{}, - SupportLinks: DefaultSupportLinks, + SupportLinks: DefaultSupportLinks(f.docsURL), }, nil } -var DefaultFetcher Fetcher = AGPLFetcher{} +func NewDefaultFetcher(docsURL string) Fetcher { + return &AGPLFetcher{ + docsURL: docsURL, + } +} diff --git a/coderd/coderd.go b/coderd/coderd.go index 896918f1c6..a4b36e8fec 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -475,7 +475,8 @@ func New(options *Options) *API { dbRolluper: options.DatabaseRolluper, } - api.AppearanceFetcher.Store(&appearance.DefaultFetcher) + f := appearance.NewDefaultFetcher(api.DeploymentValues.DocsURL.String()) + api.AppearanceFetcher.Store(&f) api.PortSharer.Store(&portsharing.DefaultPortSharer) buildInfo := codersdk.BuildInfoResponse{ ExternalURL: buildinfo.ExternalURL(), diff --git a/enterprise/coderd/appearance.go b/enterprise/coderd/appearance.go index b53c812c3e..80150914bd 100644 --- a/enterprise/coderd/appearance.go +++ b/enterprise/coderd/appearance.go @@ -44,12 +44,16 @@ func (api *API) appearance(rw http.ResponseWriter, r *http.Request) { type appearanceFetcher struct { database database.Store supportLinks []codersdk.LinkConfig + docsURL string + coderVersion string } -func newAppearanceFetcher(store database.Store, links []codersdk.LinkConfig) agpl.Fetcher { +func newAppearanceFetcher(store database.Store, links []codersdk.LinkConfig, docsURL, coderVersion string) agpl.Fetcher { return &appearanceFetcher{ database: store, supportLinks: links, + docsURL: docsURL, + coderVersion: coderVersion, } } @@ -90,7 +94,7 @@ func (f *appearanceFetcher) Fetch(ctx context.Context) (codersdk.AppearanceConfi ApplicationName: applicationName, LogoURL: logoURL, AnnouncementBanners: []codersdk.BannerConfig{}, - SupportLinks: agpl.DefaultSupportLinks, + SupportLinks: agpl.DefaultSupportLinks(f.docsURL), } if announcementBannersJSON != "" { diff --git a/enterprise/coderd/appearance_test.go b/enterprise/coderd/appearance_test.go index 0e2358e1ee..895a2ecab1 100644 --- a/enterprise/coderd/appearance_test.go +++ b/enterprise/coderd/appearance_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "net/http" + "net/url" "testing" "github.com/stretchr/testify/require" @@ -229,6 +230,26 @@ func TestCustomSupportLinks(t *testing.T) { require.Equal(t, supportLinks, appr.SupportLinks) } +func TestDefaultSupportLinksWithCustomDocsUrl(t *testing.T) { + t.Parallel() + + // Don't need to set the license, as default links are passed without it. + testURLRawString := "http://google.com" + testURL, err := url.Parse(testURLRawString) + require.NoError(t, err) + cfg := coderdtest.DeploymentValues(t) + cfg.DocsURL = *serpent.URLOf(testURL) + adminClient, adminUser := coderdenttest.New(t, &coderdenttest.Options{DontAddLicense: true, Options: &coderdtest.Options{DeploymentValues: cfg}}) + anotherClient, _ := coderdtest.CreateAnotherUser(t, adminClient, adminUser.OrganizationID) + + ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitMedium) + defer cancel() + + appr, err := anotherClient.Appearance(ctx) + require.NoError(t, err) + require.Equal(t, appearance.DefaultSupportLinks(testURLRawString), appr.SupportLinks) +} + func TestDefaultSupportLinks(t *testing.T) { t.Parallel() @@ -241,5 +262,5 @@ func TestDefaultSupportLinks(t *testing.T) { appr, err := anotherClient.Appearance(ctx) require.NoError(t, err) - require.Equal(t, appearance.DefaultSupportLinks, appr.SupportLinks) + require.Equal(t, appearance.DefaultSupportLinks(""), appr.SupportLinks) } diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go index 501e6086ea..6196ac32e2 100644 --- a/enterprise/coderd/coderd.go +++ b/enterprise/coderd/coderd.go @@ -12,6 +12,7 @@ import ( "sync" "time" + "github.com/coder/coder/v2/buildinfo" "github.com/coder/coder/v2/coderd/appearance" "github.com/coder/coder/v2/coderd/database" agplportsharing "github.com/coder/coder/v2/coderd/portsharing" @@ -791,10 +792,13 @@ func (api *API) updateEntitlements(ctx context.Context) error { f := newAppearanceFetcher( api.Database, api.DeploymentValues.Support.Links.Value, + api.DeploymentValues.DocsURL.String(), + buildinfo.Version(), ) api.AGPL.AppearanceFetcher.Store(&f) } else { - api.AGPL.AppearanceFetcher.Store(&appearance.DefaultFetcher) + f := appearance.NewDefaultFetcher(api.DeploymentValues.DocsURL.String()) + api.AGPL.AppearanceFetcher.Store(&f) } } diff --git a/site/site.go b/site/site.go index 9eee2e66d1..42d7968b33 100644 --- a/site/site.go +++ b/site/site.go @@ -84,7 +84,8 @@ type Options struct { func New(opts *Options) *Handler { if opts.AppearanceFetcher == nil { daf := atomic.Pointer[appearance.Fetcher]{} - daf.Store(&appearance.DefaultFetcher) + f := appearance.NewDefaultFetcher(opts.DocsURL) + daf.Store(&f) opts.AppearanceFetcher = &daf } handler := &Handler{ diff --git a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx index 047a4d7dab..ab46306c84 100644 --- a/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx +++ b/site/src/modules/dashboard/Navbar/UserDropdown/UserDropdownContent.tsx @@ -93,7 +93,7 @@ export const UserDropdownContent: FC = ({ {supportLinks.map((link) => ( = (props) => ( ); -const includeBuildInfo = ( - href: string, - buildInfo?: TypesGen.BuildInfoResponse, -): string => { - return href.replace( - "{CODER_BUILD_INFO}", - `${encodeURIComponent( - `Version: [\`${buildInfo?.version}\`](${buildInfo?.external_url})`, - )}`, - ); -}; - const styles = { info: (theme) => [ theme.typography.body2 as CSSObject,