Default first workspace for picker access sources
Picker providers (GitHub, Sentry, GitLab, Bitbucket, Heroku, Asana, Netlify, ClickUp, DocuSign) require the user to pick an org/workspace in a follow-up step after connecting. When that step is skipped, the source stays connected but unconfigured, and its first campaign silently resolves no users because the driver needs an org — the same "connects fine, campaign fetches nobody" symptom seen on Sentry. When a connector is linked to a source, auto-select the first workspace the connection can list, so the source is usable immediately. An org the user already chose is never overridden, and the picker stays visible (the frontend shows the selector whenever an org is selected) so they can switch when several are listed. Best-effort: a provider that is unreachable or lists nothing leaves the source in its existing needs-configuration state rather than failing the create/update mutation. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
@@ -628,6 +628,8 @@ func (r *mutationResolver) CreateAccessReviewSource(ctx context.Context, input t
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
r.autoSelectDefaultOrganization(ctx, scope, source)
|
||||
|
||||
return &types.CreateAccessReviewSourcePayload{
|
||||
AccessReviewSourceEdge: types.NewAccessReviewSourceEdge(source, coredata.AccessReviewSourceOrderFieldCreatedAt),
|
||||
}, nil
|
||||
@@ -660,6 +662,13 @@ func (r *mutationResolver) UpdateAccessReviewSource(ctx context.Context, input t
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
// A connector was just (re)linked: default its org so the source is usable
|
||||
// right away. Skipped on name/CSV-only updates to avoid a needless
|
||||
// provider round-trip.
|
||||
if input.ConnectorID.IsSet() {
|
||||
r.autoSelectDefaultOrganization(ctx, scope, source)
|
||||
}
|
||||
|
||||
return &types.UpdateAccessReviewSourcePayload{
|
||||
AccessReviewSource: types.NewAccessReviewSource(source),
|
||||
}, nil
|
||||
@@ -717,6 +726,80 @@ func (r *mutationResolver) ConfigureAccessReviewSource(ctx context.Context, inpu
|
||||
}, nil
|
||||
}
|
||||
|
||||
// autoSelectDefaultOrganization picks the first workspace/org the connector can
|
||||
// see for a freshly linked picker-provider source that has none selected yet.
|
||||
// Without it a connected source stays "needs configuration" until the user
|
||||
// completes the picker step; if they skip it, the first campaign silently
|
||||
// resolves no users (the driver requires an org). Defaulting to the first
|
||||
// available makes the source immediately usable; the picker UI stays available
|
||||
// to switch when several are listed.
|
||||
//
|
||||
// Best-effort: any failure (provider unreachable, nothing listed) leaves the
|
||||
// source in its existing "needs configuration" state, where the picker is the
|
||||
// fallback. It never fails the create/update mutation that triggered it.
|
||||
func (r *mutationResolver) autoSelectDefaultOrganization(
|
||||
ctx context.Context,
|
||||
scope coredata.Scoper,
|
||||
source *coredata.AccessReviewSource,
|
||||
) {
|
||||
if source == nil || source.ConnectorID == nil {
|
||||
return
|
||||
}
|
||||
|
||||
httpClient, dbConnector, err := r.accessReview.ConnectorHTTPClient(ctx, scope, *source.ConnectorID)
|
||||
if err != nil {
|
||||
// A missing connector is not an error worth logging: the picker
|
||||
// simply never surfaces a default.
|
||||
if !errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
r.logger.WarnCtx(ctx, "cannot load connector for default organization", log.Error(err))
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
cfg, ok := providerOrgConfigs[dbConnector.Provider]
|
||||
if !ok || !cfg.NeedsPicker || cfg.ListOrgs == nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Never override an org the user (or an earlier default) already picked.
|
||||
if cfg.SelectedSlug(dbConnector) != "" {
|
||||
return
|
||||
}
|
||||
|
||||
orgs, err := cfg.ListOrgs(ctx, httpClient)
|
||||
if err != nil {
|
||||
r.logger.WarnCtx(
|
||||
ctx,
|
||||
"cannot list provider organizations for default selection",
|
||||
log.String("provider", dbConnector.Provider.String()),
|
||||
log.Error(err),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if len(orgs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := r.accessReview.ConfigureAccessReviewSource(
|
||||
ctx,
|
||||
scope,
|
||||
accessreview.ConfigureAccessReviewSourceRequest{
|
||||
AccessReviewSourceID: source.ID,
|
||||
OrganizationSlug: orgs[0].Slug,
|
||||
},
|
||||
); err != nil {
|
||||
r.logger.WarnCtx(
|
||||
ctx,
|
||||
"cannot apply default provider organization",
|
||||
log.String("provider", dbConnector.Provider.String()),
|
||||
log.Error(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAccessReviewCampaign is the resolver for the createAccessReviewCampaign field.
|
||||
func (r *mutationResolver) CreateAccessReviewCampaign(ctx context.Context, input types.CreateAccessReviewCampaignInput) (*types.CreateAccessReviewCampaignPayload, error) {
|
||||
scope, err := r.authorize(ctx, input.OrganizationID, accessreview.ActionCampaignCreate)
|
||||
|
||||
Reference in New Issue
Block a user