From b1a67aba0059e0243d41873ec8388a36dfae79b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Fri, 24 Jul 2026 11:49:59 +0200 Subject: [PATCH] Link connector docs in the Add Source dialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The access review Add Source dialog listed each connector with no path to its setup documentation. Connectors that have a published docs page on probo.com now surface a "Documentation" link on the card, opening the page in a new tab; connectors without a page show nothing extra. The link is data-driven from the connector registry: a new DocumentationURL on the provider Registration, populated for the 12 documented providers via a single accessReviewDocsURL helper, is surfaced as a nullable documentationUrl on ConnectorProviderInfo and rendered by the console only when present. This keeps the registry the single source of truth and adds no client-side provider map. The links resolve once the probo.com access-review docs pages are deployed; until then they 404, so deploy the docs alongside this change. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- .../dialogs/AddAccessReviewSourceDialog.tsx | 13 ++++++++ e2e/console/connector_test.go | 24 +++++++++++--- pkg/connector/provider/anthropic.go | 7 ++-- pkg/connector/provider/apollo.go | 7 ++-- pkg/connector/provider/cloudflare.go | 9 ++--- pkg/connector/provider/crisp.go | 5 +-- pkg/connector/provider/cursor.go | 7 ++-- pkg/connector/provider/deepgram.go | 7 ++-- pkg/connector/provider/docs.go | 33 +++++++++++++++++++ pkg/connector/provider/openai.go | 9 ++--- pkg/connector/provider/openrouter.go | 7 ++-- pkg/connector/provider/railway.go | 7 ++-- pkg/connector/provider/resend.go | 9 ++--- pkg/connector/provider/scaleway.go | 7 ++-- pkg/connector/provider/types.go | 4 +++ pkg/connector/provider/yousign.go | 7 ++-- pkg/server/api/console/v1/base_resolvers.go | 6 ++++ .../api/console/v1/graphql/connector.graphql | 5 +++ 18 files changed, 130 insertions(+), 43 deletions(-) create mode 100644 pkg/connector/provider/docs.go diff --git a/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog.tsx b/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog.tsx index 5c638261e..f3fe65d8e 100644 --- a/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog.tsx +++ b/apps/console/src/pages/organizations/access-reviews/dialogs/AddAccessReviewSourceDialog.tsx @@ -29,6 +29,7 @@ import { DialogContent, DialogFooter, DropdownItem, + IconArrowLink, Input, ThirdPartyLogo, useDialogRef, @@ -51,6 +52,7 @@ export const addAccessReviewSourceDialogConnectorProviderInfoFragment = graphql` fragment AddAccessReviewSourceDialogConnectorProviderInfoFragment on ConnectorProviderInfo @relay(plural: true) { provider displayName + documentationUrl oauthConfigured apiKeySupported apiKeyManaged @@ -160,6 +162,17 @@ export function AddAccessReviewSourceDialog({

{info.displayName}

+ {info.documentationUrl && ( + + {__("Documentation")} + + + )}
{isConnected ? ( diff --git a/e2e/console/connector_test.go b/e2e/console/connector_test.go index e9f91c479..a9f9ae1f1 100644 --- a/e2e/console/connector_test.go +++ b/e2e/console/connector_test.go @@ -37,6 +37,7 @@ func TestAccessReviewDrivers(t *testing.T) { accessReviewDrivers { provider displayName + documentationUrl oauthConfigured apiKeySupported clientCredentialsSupported @@ -51,11 +52,12 @@ func TestAccessReviewDrivers(t *testing.T) { var result struct { AccessReviewDrivers []struct { - Provider string `json:"provider"` - DisplayName string `json:"displayName"` - OauthConfigured bool `json:"oauthConfigured"` - APIKeySupported bool `json:"apiKeySupported"` - ClientCredentialsSupported bool `json:"clientCredentialsSupported"` + Provider string `json:"provider"` + DisplayName string `json:"displayName"` + DocumentationURL *string `json:"documentationUrl"` + OauthConfigured bool `json:"oauthConfigured"` + APIKeySupported bool `json:"apiKeySupported"` + ClientCredentialsSupported bool `json:"clientCredentialsSupported"` ExtraSettings []struct { Key string `json:"key"` Label string `json:"label"` @@ -69,17 +71,29 @@ func TestAccessReviewDrivers(t *testing.T) { assert.NotEmpty(t, result.AccessReviewDrivers) providerNames := make(map[string]bool) + docURLByProvider := make(map[string]*string) for _, info := range result.AccessReviewDrivers { assert.NotEmpty(t, info.Provider) assert.NotEmpty(t, info.DisplayName) assert.NotNil(t, info.ExtraSettings) providerNames[info.Provider] = true + docURLByProvider[info.Provider] = info.DocumentationURL } assert.True(t, providerNames["BREX"], "expected BREX provider to be present") assert.True(t, providerNames["HUBSPOT"], "expected HUBSPOT provider to be present") + // A documented provider exposes its probo.com docs URL; an undocumented one + // exposes null. See pkg/connector/provider/docs.go. + require.Contains(t, docURLByProvider, "ANTHROPIC") + + if url := docURLByProvider["ANTHROPIC"]; assert.NotNil(t, url) { + assert.Equal(t, "https://www.probo.com/docs/product/access-review/anthropic", *url) + } + + assert.Nil(t, docURLByProvider["BREX"], "BREX has no doc page, documentationUrl must be null") + t.Run("viewer can list access review drivers", func(t *testing.T) { t.Parallel() viewer := testutil.NewClientInOrg(t, testutil.RoleViewer, owner) diff --git a/pkg/connector/provider/anthropic.go b/pkg/connector/provider/anthropic.go index 5eefe541e..3549aaac2 100644 --- a/pkg/connector/provider/anthropic.go +++ b/pkg/connector/provider/anthropic.go @@ -31,9 +31,10 @@ import ( func anthropicRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderAnthropic, - DisplayName: "Anthropic", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderAnthropic, + DisplayName: "Anthropic", + DocumentationURL: accessReviewDocsURL("anthropic"), + SupportsAPIKey: true, // Anthropic's Admin API authenticates with the admin key in the // x-api-key header; it rejects Authorization: Bearer and returns // 400 when both headers are present. APIKeyHeader makes the diff --git a/pkg/connector/provider/apollo.go b/pkg/connector/provider/apollo.go index bd812f04f..1a108aa0d 100644 --- a/pkg/connector/provider/apollo.go +++ b/pkg/connector/provider/apollo.go @@ -31,9 +31,10 @@ import ( func apolloRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderApollo, - DisplayName: "Apollo.io", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderApollo, + DisplayName: "Apollo.io", + DocumentationURL: accessReviewDocsURL("apollo"), + SupportsAPIKey: true, // Apollo's REST API authenticates with a master API key in the // x-api-key header; it rejects Authorization: Bearer (and, since // Sept 2024, query/body key params). APIKeyHeader makes the diff --git a/pkg/connector/provider/cloudflare.go b/pkg/connector/provider/cloudflare.go index 2b995b0bc..e6946b24e 100644 --- a/pkg/connector/provider/cloudflare.go +++ b/pkg/connector/provider/cloudflare.go @@ -31,10 +31,11 @@ import ( func cloudflareRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderCloudflare, - DisplayName: "Cloudflare", - ProbeURL: "https://api.cloudflare.com/client/v4/user/tokens/verify", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderCloudflare, + DisplayName: "Cloudflare", + DocumentationURL: accessReviewDocsURL("cloudflare"), + ProbeURL: "https://api.cloudflare.com/client/v4/user/tokens/verify", + SupportsAPIKey: true, NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) { return drivers.NewCloudflareDriver(c), nil }, diff --git a/pkg/connector/provider/crisp.go b/pkg/connector/provider/crisp.go index fb40f78e6..87d96443e 100644 --- a/pkg/connector/provider/crisp.go +++ b/pkg/connector/provider/crisp.go @@ -32,8 +32,9 @@ import ( func crispRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderCrisp, - DisplayName: "Crisp", + Provider: coredata.ConnectorProviderCrisp, + DisplayName: "Crisp", + DocumentationURL: accessReviewDocsURL("crisp"), // Model B: the plugin token is Probo's own Crisp Marketplace plugin // credential, held server-side in bootstrap config, not pasted by // the customer. ManagedAPIKey injects it at connect time; the diff --git a/pkg/connector/provider/cursor.go b/pkg/connector/provider/cursor.go index 62d297cd8..7d10899b7 100644 --- a/pkg/connector/provider/cursor.go +++ b/pkg/connector/provider/cursor.go @@ -33,9 +33,10 @@ const cursorMembersEndpoint = "https://api.cursor.com/teams/members" func cursorRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderCursor, - DisplayName: "Cursor", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderCursor, + DisplayName: "Cursor", + DocumentationURL: accessReviewDocsURL("cursor"), + SupportsAPIKey: true, // Cursor's Admin API has no third-party OAuth2 flow; it // authenticates with a team admin key (key_...) presented as the // HTTP Basic auth username with an empty password ("-u :") diff --git a/pkg/connector/provider/deepgram.go b/pkg/connector/provider/deepgram.go index 3c0ae6348..e630ff54a 100644 --- a/pkg/connector/provider/deepgram.go +++ b/pkg/connector/provider/deepgram.go @@ -31,9 +31,10 @@ import ( func deepgramRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderDeepgram, - DisplayName: "Deepgram", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderDeepgram, + DisplayName: "Deepgram", + DocumentationURL: accessReviewDocsURL("deepgram"), + SupportsAPIKey: true, // Deepgram authenticates with an API key under the `Token` scheme // (`Authorization: Token `), not Bearer. APIKeyAuthScheme makes // the APIKeyConnection use that scheme. There is no third-party diff --git a/pkg/connector/provider/docs.go b/pkg/connector/provider/docs.go new file mode 100644 index 000000000..bf6684707 --- /dev/null +++ b/pkg/connector/provider/docs.go @@ -0,0 +1,33 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +package provider + +// accessReviewDocsBaseURL is the public probo.com docs root for access-review +// connectors; each documented provider's page lives at this base + its slug. +const accessReviewDocsBaseURL = "https://www.probo.com/docs/product/access-review/" + +// accessReviewDocsURL builds the public documentation URL for an access-review +// connector from its page slug (e.g. "anthropic"). The slug is passed explicitly +// by each registration rather than derived from the provider enum, so an enum +// containing an underscore (e.g. BETTER_STACK) cannot produce a wrong URL. +func accessReviewDocsURL(slug string) string { + return accessReviewDocsBaseURL + slug +} diff --git a/pkg/connector/provider/openai.go b/pkg/connector/provider/openai.go index 6e18972e7..95e9deb1b 100644 --- a/pkg/connector/provider/openai.go +++ b/pkg/connector/provider/openai.go @@ -31,10 +31,11 @@ import ( func openaiRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderOpenAI, - DisplayName: "OpenAI", - ProbeURL: "https://api.openai.com/v1/models", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderOpenAI, + DisplayName: "OpenAI", + DocumentationURL: accessReviewDocsURL("openai"), + ProbeURL: "https://api.openai.com/v1/models", + SupportsAPIKey: true, NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) { return drivers.NewOpenAIDriver(c), nil }, diff --git a/pkg/connector/provider/openrouter.go b/pkg/connector/provider/openrouter.go index 1a795a216..79109bdb1 100644 --- a/pkg/connector/provider/openrouter.go +++ b/pkg/connector/provider/openrouter.go @@ -31,9 +31,10 @@ import ( func openrouterRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderOpenRouter, - DisplayName: "OpenRouter", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderOpenRouter, + DisplayName: "OpenRouter", + DocumentationURL: accessReviewDocsURL("openrouter"), + SupportsAPIKey: true, // OpenRouter authenticates with an organization management // (provisioning) API key presented as Authorization: Bearer, the // default APIKeyConnection scheme. The key is bound to one diff --git a/pkg/connector/provider/railway.go b/pkg/connector/provider/railway.go index 4f4054073..68d719685 100644 --- a/pkg/connector/provider/railway.go +++ b/pkg/connector/provider/railway.go @@ -31,9 +31,10 @@ import ( func railwayRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderRailway, - DisplayName: "Railway", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderRailway, + DisplayName: "Railway", + DocumentationURL: accessReviewDocsURL("railway"), + SupportsAPIKey: true, // Railway authenticates with an account API token as Authorization: // Bearer. A single GraphQL call resolves the account's workspaces and // their members, so there is nothing to pick (Pattern 3). Railway diff --git a/pkg/connector/provider/resend.go b/pkg/connector/provider/resend.go index 82fd069bd..7a72c8b39 100644 --- a/pkg/connector/provider/resend.go +++ b/pkg/connector/provider/resend.go @@ -31,10 +31,11 @@ import ( func resendRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderResend, - DisplayName: "Resend", - ProbeURL: "https://api.resend.com/domains", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderResend, + DisplayName: "Resend", + DocumentationURL: accessReviewDocsURL("resend"), + ProbeURL: "https://api.resend.com/domains", + SupportsAPIKey: true, NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) { return drivers.NewResendDriver(c), nil }, diff --git a/pkg/connector/provider/scaleway.go b/pkg/connector/provider/scaleway.go index 5d7ffb328..48b2991f3 100644 --- a/pkg/connector/provider/scaleway.go +++ b/pkg/connector/provider/scaleway.go @@ -32,9 +32,10 @@ import ( func scalewayRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderScaleway, - DisplayName: "Scaleway", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderScaleway, + DisplayName: "Scaleway", + DocumentationURL: accessReviewDocsURL("scaleway"), + SupportsAPIKey: true, // Scaleway authenticates with the secret key in the X-Auth-Token header // rather than Authorization: Bearer. APIKeyHeader makes the // APIKeyConnection send that header and omit Authorization. The key is diff --git a/pkg/connector/provider/types.go b/pkg/connector/provider/types.go index 9050c88d1..96371d2e4 100644 --- a/pkg/connector/provider/types.go +++ b/pkg/connector/provider/types.go @@ -39,6 +39,10 @@ type Registration struct { // Identity. Provider coredata.ConnectorProvider DisplayName string + // DocumentationURL is the public probo.com docs page for connecting this + // provider as an access source. Empty for providers with no doc page yet; + // surfaced (nullable) on ConnectorProviderInfo so the console renders a link. + DocumentationURL string // OAuth2 metadata. AuthURL string diff --git a/pkg/connector/provider/yousign.go b/pkg/connector/provider/yousign.go index 0636c6bb3..009710bcf 100644 --- a/pkg/connector/provider/yousign.go +++ b/pkg/connector/provider/yousign.go @@ -31,9 +31,10 @@ import ( func yousignRegistration() *Registration { return &Registration{ - Provider: coredata.ConnectorProviderYousign, - DisplayName: "Yousign", - SupportsAPIKey: true, + Provider: coredata.ConnectorProviderYousign, + DisplayName: "Yousign", + DocumentationURL: accessReviewDocsURL("yousign"), + SupportsAPIKey: true, // Yousign authenticates with an API key as Authorization: Bearer. The // key is bound to one organization, so GET /v3/users returns everyone // with nothing to pick (Pattern 3). The connector targets Yousign diff --git a/pkg/server/api/console/v1/base_resolvers.go b/pkg/server/api/console/v1/base_resolvers.go index 72b7d4363..7a9894548 100644 --- a/pkg/server/api/console/v1/base_resolvers.go +++ b/pkg/server/api/console/v1/base_resolvers.go @@ -607,9 +607,15 @@ func (r *queryResolver) AccessReviewDrivers(ctx context.Context) ([]*types.Conne ) } + var documentationURL *string + if reg.DocumentationURL != "" { + documentationURL = new(reg.DocumentationURL) + } + infos = append(infos, &types.ConnectorProviderInfo{ Provider: provider, DisplayName: reg.DisplayName, + DocumentationURL: documentationURL, OauthConfigured: oauthConfigured, APIKeySupported: apiKeySupported, APIKeyManaged: apiKeyManaged, diff --git a/pkg/server/api/console/v1/graphql/connector.graphql b/pkg/server/api/console/v1/graphql/connector.graphql index cdf4c16b3..1193fb7ab 100644 --- a/pkg/server/api/console/v1/graphql/connector.graphql +++ b/pkg/server/api/console/v1/graphql/connector.graphql @@ -104,6 +104,11 @@ enum ConnectorProvider type ConnectorProviderInfo { provider: ConnectorProvider! displayName: String! + """ + documentationUrl is the public probo.com docs page for connecting this + provider as an access source, or null when no doc page exists yet. + """ + documentationUrl: String oauthConfigured: Boolean! apiKeySupported: Boolean! """