Skip to content
Open
Show file tree
Hide file tree
Changes from 141 commits
Commits
Show all changes
193 commits
Select commit Hold shift + click to select a range
dff46f4
Add access review data layer and migration
gearnode Mar 15, 2026
27771c9
Add API key connector protocol
gearnode Mar 15, 2026
a89dc55
Add access review domain services
gearnode Mar 15, 2026
1c0dcdc
Add access source driver interface
gearnode Mar 15, 2026
1ede3e0
Add Google Workspace access source driver
gearnode Mar 15, 2026
e5cda07
Add Linear access source driver
gearnode Mar 15, 2026
417a23e
Add Slack access source driver
gearnode Mar 15, 2026
21f0972
Add Figma access source driver
gearnode Mar 15, 2026
46ba2f0
Add 1Password access source driver
gearnode Mar 15, 2026
674a454
Add HubSpot access source driver
gearnode Mar 15, 2026
1672cf4
Add DocuSign access source driver
gearnode Mar 15, 2026
7db02a7
Add Notion access source driver
gearnode Mar 15, 2026
b0268e5
Add Brex access source driver
gearnode Mar 15, 2026
3fe4a5f
Add Tally access source driver
gearnode Mar 15, 2026
9135362
Add Cloudflare access source driver
gearnode Mar 15, 2026
a8294f1
Add CSV access source driver
gearnode Mar 15, 2026
1dd9d88
Add Probo memberships access source driver
gearnode Mar 15, 2026
195314b
Add Sentry access source driver
gearnode Mar 15, 2026
e6e0775
Add OpenAI access source driver
gearnode Mar 15, 2026
594b422
Add access review campaign service and worker
gearnode Mar 15, 2026
eb5a4d0
Add access review console GraphQL API
gearnode Mar 15, 2026
2a08cc5
Add access review console UI
gearnode Mar 15, 2026
117d7e9
Add access review MCP tools
gearnode Mar 15, 2026
17c684a
Add e2e tests for access review API
gearnode Mar 15, 2026
5bd9587
Add access review campaign detail page and clean up schema
gearnode Mar 24, 2026
ca16647
Refactor access review API and access source drivers
gearnode Mar 26, 2026
088a35f
Replace goto LOOP with idiomatic for/select
aureliensibiril Mar 26, 2026
644a873
Wrap bare errors in campaign service
aureliensibiril Mar 26, 2026
40c1b34
Reduce campaign name max length to 255
aureliensibiril Mar 26, 2026
1da51aa
Clean up name resolvers
aureliensibiril Mar 26, 2026
5c4c17d
Add pagination guard to Cloudflare driver
aureliensibiril Mar 26, 2026
270a362
Use ProfileStateActive const in memberships driver
aureliensibiril Mar 26, 2026
d0bb4f7
Fix import ordering in review engine
aureliensibiril Mar 26, 2026
63139e1
Fix OrderField Column and validation
aureliensibiril Mar 26, 2026
64237fb
Add Scoper to scope system and source fetch
aureliensibiril Mar 26, 2026
a720331
Add decision history to entity registry
aureliensibiril Mar 26, 2026
058fdea
Use RETURNING in coredata Update methods
aureliensibiril Mar 26, 2026
6513231
Clean up access review migrations
aureliensibiril Mar 26, 2026
b9c05d4
Add enum Scan/Value unit tests
aureliensibiril Mar 26, 2026
0502f75
Rename CLI directory to kebab-case
aureliensibiril Mar 26, 2026
49c94ab
Rename entry flag package to setflag
aureliensibiril Mar 26, 2026
42a77df
Fix org-resolution error message wording
aureliensibiril Mar 26, 2026
c3c8073
Use Flags.Changed for csv-file in source update
aureliensibiril Mar 26, 2026
461d486
Add confirmation prompt to start and close
aureliensibiril Mar 26, 2026
5a528c0
Add exclusivity check for source create flags
aureliensibiril Mar 26, 2026
25365c2
Add order-direction validation to list commands
aureliensibiril Mar 26, 2026
cfe8d4d
Revert "Use RETURNING in coredata Update methods"
aureliensibiril Mar 26, 2026
cd7c934
Improve Linear driver field coverage
aureliensibiril Mar 26, 2026
e106575
Add JobTitle and bot support to Slack driver
aureliensibiril Mar 26, 2026
f2cac15
Resolve HubSpot role names via roles endpoint
aureliensibiril Mar 26, 2026
b43b149
Include bots as ServiceAccount in Notion driver
aureliensibiril Mar 26, 2026
4a37902
Fix Google Workspace role mapping and projection
aureliensibiril Mar 26, 2026
a54d49e
Concatenate all Cloudflare roles in driver
aureliensibiril Mar 26, 2026
e31fcf2
Add JobTitle to DocuSign driver
aureliensibiril Mar 26, 2026
93a2fe7
Add Role, IsAdmin, CreatedAt to Probo driver
aureliensibiril Mar 26, 2026
f5db928
Add go-vcr test infrastructure for drivers
aureliensibiril Mar 26, 2026
0e63996
Add go-vcr tests for existing drivers
aureliensibiril Mar 26, 2026
a27ef38
Add Sentry access review driver
aureliensibiril Mar 26, 2026
c8ff264
Add Supabase access review driver
aureliensibiril Mar 26, 2026
822084f
Add GitHub access review driver
aureliensibiril Mar 26, 2026
5326044
Add Intercom access review driver
aureliensibiril Mar 26, 2026
5ae9803
Add Resend access review driver
aureliensibiril Mar 26, 2026
d66cb71
Wire new drivers into the review engine
aureliensibiril Mar 26, 2026
15b9f56
Accept variable-length rows in CSV driver
aureliensibiril Mar 26, 2026
0302ca8
Handle pagination parse errors in DocuSign driver
aureliensibiril Mar 26, 2026
b0b802b
Return error when pagination limit is reached
aureliensibiril Mar 26, 2026
d8a1288
Fix name worker retry on resolution failure
aureliensibiril Mar 26, 2026
d53150c
Add vendor icon components
aureliensibiril Mar 26, 2026
f9f6d89
Extract shared access review helpers
aureliensibiril Mar 26, 2026
719b5be
Improve access review layout and source row
aureliensibiril Mar 26, 2026
c3dabf9
Refactor campaign pages to use shared helpers
aureliensibiril Mar 26, 2026
21d189c
Fix access source dialog error handling
aureliensibiril Mar 26, 2026
84174f7
Fix CSV access source page error handling
aureliensibiril Mar 26, 2026
c621d0e
Fix access source creation page
aureliensibiril Mar 26, 2026
7afb890
Fix scope sources query returning all org sources
aureliensibiril Mar 26, 2026
2186f0b
Add OAuth2 token exchange styles and client credentials grant
aureliensibiril Mar 26, 2026
ada13aa
Add bootstrap config for six new OAuth providers
aureliensibiril Mar 26, 2026
219851f
Add ConnectorProviders helper and 1Password Users API settings
aureliensibiril Mar 26, 2026
9581bd1
Add dynamic connector provider info and client credentials mutation
aureliensibiril Mar 26, 2026
1e794ad
Add OAuth2 token refresh to review engine
aureliensibiril Mar 26, 2026
bf4b60a
Add Sentry org auto-discovery and 1Password Users API driver
aureliensibiril Mar 26, 2026
c5c6a43
Replace hardcoded providers with dynamic connector query
aureliensibiril Mar 26, 2026
8db50fd
Add unit and E2E tests for connector operations
aureliensibiril Mar 26, 2026
68c7329
Add VendorLogo component with tint support
aureliensibiril Mar 26, 2026
bb60c3c
Add search bar, vendor logos, and region select
aureliensibiril Mar 26, 2026
62b2960
Fix VendorLogo tint to use Tailwind classes
aureliensibiril Mar 26, 2026
24182c2
Add Linear and new OAuth providers to dev config
aureliensibiril Mar 26, 2026
be381b9
Add Brex and Linear OAuth connector config
aureliensibiril Mar 26, 2026
61ca0ef
Add access source dialog with OAuth callback handling
aureliensibiril Mar 26, 2026
f22f338
Remove standalone CreateAccessSourcePage
aureliensibiril Mar 26, 2026
200b5e8
Wrap bare error returns in access review services
aureliensibiril Mar 27, 2026
1f1b4b3
Extract statistics-to-types conversion helper
aureliensibiril Mar 27, 2026
fe7519a
Fix OAuth2 token refresh in source name worker
aureliensibiril Mar 27, 2026
1c17e8b
Fix bulk decide to resolve profile per entry
aureliensibiril Mar 27, 2026
460c0d3
Add create campaign button and dialog
aureliensibiril Mar 27, 2026
435dc5d
Add start campaign and add source to draft
aureliensibiril Mar 27, 2026
df409c9
Fix Sentry name resolver for empty org slug
aureliensibiril Mar 27, 2026
79b2208
Persist refreshed OAuth tokens to database
aureliensibiril Mar 27, 2026
c630bd7
Widen Linear bot detection to all *.linear.app
aureliensibiril Mar 27, 2026
fa82ede
Fix Intercom role and admin mapping
aureliensibiril Mar 27, 2026
a1392ee
Fix Google Workspace MFA status for non-enrolled users
aureliensibiril Mar 27, 2026
8121804
Add IconRobot to icon library
aureliensibiril Mar 27, 2026
c61a8f8
Add connector org listing and source configuration
aureliensibiril Mar 27, 2026
8276c3e
Add OAuth connector reconnect flow
aureliensibiril Mar 27, 2026
5ddfd41
Rework sources page with status and inline org select
aureliensibiril Mar 27, 2026
76e0a31
Add ISC headers to new files
aureliensibiril Mar 27, 2026
128164a
Add probe-url to connector config
aureliensibiril Mar 27, 2026
17ea331
Expand AccessEntryFlag enum with new values
aureliensibiril Mar 30, 2026
2024c2f
Add SelectGroup and SelectLabel to UI library
aureliensibiril Mar 30, 2026
84e7805
Add flag and decision label helpers
aureliensibiril Mar 30, 2026
0e621cd
Add entry flag select and decision actions components
aureliensibiril Mar 30, 2026
81e6b1d
Wire flag and decision controls in campaign detail
aureliensibiril Mar 30, 2026
a406407
Move probe URLs from config to hardcoded registry
aureliensibiril Mar 30, 2026
4676c0d
Change access entry flag from single value to array
aureliensibiril Mar 30, 2026
9e54c59
Update service layer for multi-flag entries
aureliensibiril Mar 30, 2026
1093d0d
Update GraphQL schema for multi-flag entries
aureliensibiril Mar 30, 2026
31fb465
Update MCP specification for multi-flag entries
aureliensibiril Mar 30, 2026
58651c4
Update CLI for multi-flag entries
aureliensibiril Mar 30, 2026
7ad43d1
Add multi-select flag UI with checkbox popover
aureliensibiril Mar 30, 2026
2261e5b
Batch flag mutations and clean up migration
aureliensibiril Mar 30, 2026
4f6cf00
Remove type assertions in flag and decision components
aureliensibiril Mar 30, 2026
5739e96
Fix nil flag arrays and allow re-deciding entries
aureliensibiril Mar 30, 2026
9c1b93e
Fix flag popover text color and add decision edit
aureliensibiril Mar 30, 2026
751ae98
Add complete campaign button
aureliensibiril Mar 30, 2026
6a8d92a
Use pendingEntryCount for complete button state
aureliensibiril Mar 30, 2026
e390422
Add bulk selection for entry flags and decisions
aureliensibiril Mar 30, 2026
0fa4925
Fix bulk decision for already-decided entries
aureliensibiril Mar 30, 2026
66c2dd6
Use multi-select popover for bulk flag actions
aureliensibiril Mar 30, 2026
ce1907e
Allow re-deciding single entries
aureliensibiril Mar 30, 2026
bd87241
Merge remote-tracking branch 'origin/main' into aureliensibiril/eng-1…
aureliensibiril Mar 30, 2026
87da22e
Merge remote-tracking branch 'origin/main' into aureliensibiril/eng-1…
aureliensibiril Mar 30, 2026
d4a3280
Split merged migration into separate files
aureliensibiril Mar 30, 2026
65e512b
Remove PII from CLI decision output
aureliensibiril Mar 30, 2026
8efb3d0
Add Slack as selectable access source provider
aureliensibiril Mar 30, 2026
157fdbe
Reset campaign dialog state on close
aureliensibiril Mar 30, 2026
3a68391
Fix OAuth callback retry loop on error
aureliensibiril Mar 30, 2026
a434988
Fix bulk flag error handling and entry limit
aureliensibiril Mar 30, 2026
edd7463
Add organization_id to access entries and decision history
aureliensibiril Mar 30, 2026
bb012e7
Fix sprintf usage in campaign detail page
aureliensibiril Mar 30, 2026
9c433db
Add missing flag values to CLI list filter
aureliensibiril Mar 30, 2026
245fa72
Update dev config to use agents LLM config
aureliensibiril Mar 30, 2026
4e44589
Rename SnapshotSource to FetchSource
aureliensibiril Mar 30, 2026
3c961fa
Drop default on campaign description column
aureliensibiril Mar 30, 2026
a242018
Remove SQL defaults from access review tables
aureliensibiril Mar 30, 2026
a6e0109
Initialize FlagReasons in entry creation
aureliensibiril Mar 30, 2026
e564a4b
Fix confirm dialog button for campaign completion
aureliensibiril Mar 30, 2026
31a2ae4
Log warnings when skipping GitHub members on error
aureliensibiril Mar 30, 2026
1390bb6
Fix gofmt formatting in decision history
aureliensibiril Mar 30, 2026
0942897
Remove PII from GitHub driver log warnings
aureliensibiril Mar 30, 2026
986ebd2
Fix eslint errors in access review components
aureliensibiril Mar 30, 2026
092ec4d
Merge remote-tracking branch 'origin/main' into aureliensibiril/eng-1…
aureliensibiril Mar 30, 2026
6ef11c5
Wrap bare return err across access review code
aureliensibiril Mar 30, 2026
c6a2728
Fix slackResponseMetdata typo
aureliensibiril Mar 30, 2026
95365fe
Remove duplicate RecordDecision and unnecessary RETURNING
aureliensibiril Mar 30, 2026
21bf604
Fix gofmt formatting in slack driver
aureliensibiril Mar 30, 2026
234d2a5
Merge remote-tracking branch 'origin/main' into aureliensibiril/eng-1…
aureliensibiril Mar 30, 2026
2e25d34
Guard against null source in scope sources list
aureliensibiril Mar 30, 2026
0654911
Remove unnecessary type assertion in scope source IDs
aureliensibiril Mar 30, 2026
b60de41
Extract RFC 5988 link header parser
aureliensibiril Mar 31, 2026
80161e6
Simplify LoadNextUnsyncedName return value
aureliensibiril Mar 31, 2026
a30cb5b
Move OAuth2 state decode near OAuth2State type
aureliensibiril Mar 31, 2026
45f6de5
Remove CampaignReader interface indirection
aureliensibiril Mar 31, 2026
8488dee
Fix error wrapping and style in campaign service
aureliensibiril Mar 31, 2026
86c8faa
Clean up source name worker style and scoping
aureliensibiril Mar 31, 2026
7bb09aa
Use time.NewTicker and fix style in fetch worker
aureliensibiril Mar 31, 2026
055e403
Move types to top in brex driver
aureliensibiril Mar 31, 2026
5d5eda1
Move types to top in cloudflare driver
aureliensibiril Mar 31, 2026
dfceedb
Move types to top in docusign driver
aureliensibiril Mar 31, 2026
580c915
Move types to top in hubspot driver
aureliensibiril Mar 31, 2026
ad73d43
Move types to top in intercom driver
aureliensibiril Mar 31, 2026
9789890
Move types to top in linear driver
aureliensibiril Mar 31, 2026
d66d324
Move types to top in probo_memberships driver
aureliensibiril Mar 31, 2026
cbada8c
Drop comment and move types to top in notion driver
aureliensibiril Mar 31, 2026
2d32231
Drop comment and move types to top in openai driver
aureliensibiril Mar 31, 2026
e7baec2
Drop comment and move types to top in slack driver
aureliensibiril Mar 31, 2026
0d9defe
Drop comment, use net/url, move types to top in supabase driver
aureliensibiril Mar 31, 2026
8e518b9
Drop comment, use net/url, move types to top in tally driver
aureliensibiril Mar 31, 2026
840888a
Use net/url and move types to top in onepassword driver
aureliensibiril Mar 31, 2026
3be2217
Use net/url and move types to top in onepassword users API driver
aureliensibiril Mar 31, 2026
dc8f8a2
Remove comment, drop User-Agent, move types to top in resend driver
aureliensibiril Mar 31, 2026
3e8f95e
Use rfc5988 package and move types to top in sentry driver
aureliensibiril Mar 31, 2026
6eddec6
Use rfc5988 package and move types to top in github driver
aureliensibiril Mar 31, 2026
85787a4
Replace inline retry with retryRoundTripper in google workspace driver
aureliensibiril Mar 31, 2026
0445932
Return raw value in status and decision label helpers
aureliensibiril Mar 31, 2026
8462bcb
Use @required directive and config-only URL in AccessSourceRow
aureliensibiril Mar 31, 2026
fa90c13
Fix gofmt in google workspace driver
aureliensibiril Mar 31, 2026
8f511e2
Restore window.location.origin fallback for VITE_API_URL
aureliensibiril Mar 31, 2026
1da519e
Merge remote-tracking branch 'origin/main' into aureliensibiril/eng-1…
aureliensibiril Mar 31, 2026
31324f4
Sort access source providers alphabetically
aureliensibiril Mar 31, 2026
69b7543
Derive allDecided from local entry state
aureliensibiril Mar 31, 2026
0681e99
Remove nested Card rounding in campaign entries table
aureliensibiril Mar 31, 2026
5357986
Guard against missing entries on draft scope sources
aureliensibiril Mar 31, 2026
2d97016
Fix eslint indentation in campaign detail page
aureliensibiril Mar 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ compose/keycloak/probo-realm.json
# Generated files (codegen)
__generated__/
pkg/server/api/*/v1/types/types.go
cfg/dev_local.yaml
11 changes: 11 additions & 0 deletions apps/console/src/pages/iam/organizations/_components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
IconFire3,
IconGroup1,
IconInboxEmpty,
IconKey,
IconListStack,
IconLock,
IconMagnifyingGlass,
Expand Down Expand Up @@ -52,6 +53,9 @@ const fragment = graphql`
canListStatesOfApplicability: permission(
action: "core:state-of-applicability:list"
)
canListAccessReviewCampaigns: permission(
action: "core:access-review-campaign:list"
)
}
`;

Expand Down Expand Up @@ -186,6 +190,13 @@ export function Sidebar(props: { fKey: SidebarFragment$key }) {
to={`${prefix}/snapshots`}
/>
)}
{organization.canListAccessReviewCampaigns && (
<SidebarItem
label={__("Access Reviews")}
icon={IconKey}
to={`${prefix}/access-reviews`}
/>
)}
{organization.canGetTrustCenter && (
<SidebarItem
label={__("Compliance Page")}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.

import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
IconFolder2,
IconKey,
PageHeader,
TabLink,
Tabs,
} from "@probo/ui";
import { type PreloadedQuery, usePreloadedQuery } from "react-relay";
import { Outlet } from "react-router";
import { graphql } from "relay-runtime";

import type { AccessReviewLayoutQuery } from "#/__generated__/core/AccessReviewLayoutQuery.graphql";
import { useOrganizationId } from "#/hooks/useOrganizationId";

export const accessReviewLayoutQuery = graphql`
query AccessReviewLayoutQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
__typename
... on Organization {
id
canCreateSource: permission(action: "core:access-source:create")
canCreateCampaign: permission(action: "core:access-review-campaign:create")
connectorProviderInfos {
provider
displayName
oauthConfigured
apiKeySupported
clientCredentialsSupported
extraSettings {
key
label
required
}
}
...AccessReviewCampaignsTabFragment
...AccessReviewSourcesTabFragment
}
}
}
`;

export default function AccessReviewLayout({
queryRef,
}: {
queryRef: PreloadedQuery<AccessReviewLayoutQuery>;
}) {
const { __ } = useTranslate();
const organizationId = useOrganizationId();

usePageTitle(__("Access Reviews"));

const { organization } = usePreloadedQuery(accessReviewLayoutQuery, queryRef);
if (organization.__typename !== "Organization") {
throw new Error("Organization not found");
}

return (
<div className="space-y-6">
<PageHeader
title={__("Access Reviews")}
description={__(
"Review and manage user access across your organization's systems and applications.",
)}
/>

<Tabs>
<TabLink to={`/organizations/${organizationId}/access-reviews`} end>
<IconKey className="size-4" />
{__("Campaigns")}
</TabLink>
<TabLink to={`/organizations/${organizationId}/access-reviews/sources`}>
<IconFolder2 className="size-4" />
{__("Sources")}
</TabLink>
</Tabs>

<Outlet context={{
organizationRef: organization,
canCreateSource: organization.canCreateSource,
canCreateCampaign: organization.canCreateCampaign,
connectorProviderInfos: organization.connectorProviderInfos,
}}
/>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.

import { Suspense, useEffect } from "react";
import { useQueryLoader } from "react-relay";

import type { AccessReviewLayoutQuery } from "#/__generated__/core/AccessReviewLayoutQuery.graphql";
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
import { useOrganizationId } from "#/hooks/useOrganizationId";

import AccessReviewLayout, { accessReviewLayoutQuery } from "./AccessReviewLayout";

export default function AccessReviewLayoutLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader<AccessReviewLayoutQuery>(accessReviewLayoutQuery);

useEffect(() => {
if (!queryRef) {
loadQuery({ organizationId });
}
}, [loadQuery, organizationId]);

if (!queryRef) return <PageSkeleton />;

return (
<Suspense fallback={<PageSkeleton />}>
<AccessReviewLayout queryRef={queryRef} />
</Suspense>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) 2026 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.

import { formatError, type GraphQLError } from "@probo/helpers";
import { usePageTitle } from "@probo/hooks";
import { useTranslate } from "@probo/i18n";
import {
Button,
Card,
Field,
PageHeader,
useToast,
} from "@probo/ui";
import { type PreloadedQuery, useMutation, usePreloadedQuery } from "react-relay";
import { Link, useNavigate } from "react-router";
import { ConnectionHandler, graphql } from "relay-runtime";
import { z } from "zod";

import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
import type { CreateCsvAccessSourcePageQuery } from "#/__generated__/core/CreateCsvAccessSourcePageQuery.graphql";
import { useFormWithSchema } from "#/hooks/useFormWithSchema";
import { useOrganizationId } from "#/hooks/useOrganizationId";

import { createAccessSourceMutation } from "./dialogs/CreateAccessSourceDialog";

export const createCsvAccessSourcePageQuery = graphql`
query CreateCsvAccessSourcePageQuery($organizationId: ID!) {
organization: node(id: $organizationId) {
__typename
... on Organization {
id
canCreateSource: permission(action: "core:access-source:create")
}
}
}
`;

const csvSchema = z.object({
name: z.string().min(1),
csvData: z.string().min(1),
});

export default function CreateCsvAccessSourcePage({
queryRef,
}: {
queryRef: PreloadedQuery<CreateCsvAccessSourcePageQuery>;
}) {
const { __ } = useTranslate();
const { toast } = useToast();
const navigate = useNavigate();
const organizationId = useOrganizationId();
const { register, handleSubmit }
= useFormWithSchema(csvSchema, {
defaultValues: {
name: "",
csvData: "",
},
});

usePageTitle(__("Add CSV Access Source"));

const { organization } = usePreloadedQuery(createCsvAccessSourcePageQuery, queryRef);
if (organization.__typename !== "Organization") {
throw new Error("Organization not found");
}

const connectionId = ConnectionHandler.getConnectionID(
organization.id,
"AccessReviewSourcesTab_accessSources",
);

const [createAccessSource, isCreating]
= useMutation<CreateAccessSourceDialogMutation>(
createAccessSourceMutation,
);

if (!organization.canCreateSource) {
return (
<Card padded>
<p className="text-txt-secondary text-sm">
{__("You do not have permission to create access sources.")}
</p>
</Card>
);
}

const onSubmit = (data: z.infer<typeof csvSchema>) => {
createAccessSource({
variables: {
input: {
organizationId,
connectorId: null,
name: data.name,
csvData: data.csvData,
},
connections: connectionId ? [connectionId] : [],
},
onCompleted(_, errors) {
if (errors?.length) {
toast({
title: __("Error"),
description: formatError(
__("Failed to create access source"),
errors as GraphQLError[],
),
variant: "error",
});
return;
}
toast({
title: __("Success"),
description: __("Access source created successfully."),
variant: "success",
});
void navigate(`/organizations/${organizationId}/access-reviews/sources`);
},
onError(error) {
toast({
title: __("Error"),
description: formatError(
__("Failed to create access source"),
error as GraphQLError,
),
variant: "error",
});
},
});
};

return (
<div className="space-y-6">
<PageHeader
title={__("Add CSV access source")}
description={__(
"Paste CSV content with a header row. This source will be saved and available in Access Reviews.",
)}
/>

<Card padded>
<form onSubmit={e => void handleSubmit(onSubmit)(e)} className="space-y-4">
<Field
label={__("Name")}
{...register("name")}
type="text"
required
/>

<Field
label={__("CSV Data")}
{...register("csvData")}
type="textarea"
placeholder="email,full_name,role,job_title,is_admin,active,external_id"
required
/>
<p className="text-txt-secondary text-sm">
{__("Supported columns: email, full_name, role, job_title, is_admin, active, external_id.")}
</p>

<div className="flex items-center justify-end gap-2">
<Button variant="secondary" asChild>
<Link to={`/organizations/${organizationId}/access-reviews/sources`}>
{__("Back")}
</Link>
</Button>
<Button disabled={isCreating} type="submit">
{__("Create")}
</Button>
</div>
</form>
</Card>
</div>
);
}
Loading
Loading