@@ -21,55 +21,16 @@ import {
|
||||
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>;
|
||||
}) {
|
||||
export default function AccessReviewLayout() {
|
||||
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
|
||||
@@ -90,13 +51,7 @@ export default function AccessReviewLayout({
|
||||
</TabLink>
|
||||
</Tabs>
|
||||
|
||||
<Outlet context={{
|
||||
organizationRef: organization,
|
||||
canCreateSource: organization.canCreateSource,
|
||||
canCreateCampaign: organization.canCreateCampaign,
|
||||
connectorProviderInfos: organization.connectorProviderInfos,
|
||||
}}
|
||||
/>
|
||||
<Outlet />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,30 +12,8 @@
|
||||
// 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";
|
||||
import AccessReviewLayout 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>
|
||||
);
|
||||
return <AccessReviewLayout />;
|
||||
}
|
||||
|
||||
@@ -25,16 +25,29 @@ import {
|
||||
Thead,
|
||||
Tr,
|
||||
} from "@probo/ui";
|
||||
import { graphql, usePaginationFragment } from "react-relay";
|
||||
import { useOutletContext } from "react-router";
|
||||
import type { PreloadedQuery } from "react-relay";
|
||||
import { graphql, usePaginationFragment, usePreloadedQuery } from "react-relay";
|
||||
|
||||
import type { AccessReviewCampaignsTabFragment$key } from "#/__generated__/core/AccessReviewCampaignsTabFragment.graphql";
|
||||
import type { AccessReviewCampaignsTabPaginationQuery } from "#/__generated__/core/AccessReviewCampaignsTabPaginationQuery.graphql";
|
||||
import type { AccessReviewCampaignsTabQuery } from "#/__generated__/core/AccessReviewCampaignsTabQuery.graphql";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import { statusBadgeVariant, statusLabel } from "../_components/accessReviewHelpers";
|
||||
import { CreateAccessReviewCampaignDialog } from "../dialogs/CreateAccessReviewCampaignDialog";
|
||||
|
||||
export const accessReviewCampaignsTabQuery = graphql`
|
||||
query AccessReviewCampaignsTabQuery($organizationId: ID!) {
|
||||
organization: node(id: $organizationId) {
|
||||
__typename
|
||||
... on Organization {
|
||||
canCreateCampaign: permission(action: "core:access-review-campaign:create")
|
||||
...AccessReviewCampaignsTabFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const campaignsFragment = graphql`
|
||||
fragment AccessReviewCampaignsTabFragment on Organization
|
||||
@refetchable(queryName: "AccessReviewCampaignsTabPaginationQuery")
|
||||
@@ -58,21 +71,24 @@ const campaignsFragment = graphql`
|
||||
name
|
||||
status
|
||||
createdAt
|
||||
startedAt
|
||||
completedAt
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export default function AccessReviewCampaignsTab() {
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<AccessReviewCampaignsTabQuery>;
|
||||
};
|
||||
|
||||
export default function AccessReviewCampaignsTab({ queryRef }: Props) {
|
||||
const { __, dateFormat } = useTranslate();
|
||||
const organizationId = useOrganizationId();
|
||||
const { organizationRef, canCreateCampaign } = useOutletContext<{
|
||||
organizationRef: AccessReviewCampaignsTabFragment$key;
|
||||
canCreateCampaign: boolean;
|
||||
}>();
|
||||
|
||||
const { organization } = usePreloadedQuery(accessReviewCampaignsTabQuery, queryRef);
|
||||
if (organization.__typename !== "Organization") {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const {
|
||||
data: { accessReviewCampaigns },
|
||||
@@ -82,12 +98,12 @@ export default function AccessReviewCampaignsTab() {
|
||||
} = usePaginationFragment<
|
||||
AccessReviewCampaignsTabPaginationQuery,
|
||||
AccessReviewCampaignsTabFragment$key
|
||||
>(campaignsFragment, organizationRef);
|
||||
>(campaignsFragment, organization);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-end">
|
||||
{canCreateCampaign && (
|
||||
{organization.canCreateCampaign && (
|
||||
<CreateAccessReviewCampaignDialog
|
||||
organizationId={organizationId}
|
||||
connectionId={accessReviewCampaigns.__id}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 { AccessReviewCampaignsTabQuery } from "#/__generated__/core/AccessReviewCampaignsTabQuery.graphql";
|
||||
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import AccessReviewCampaignsTab, { accessReviewCampaignsTabQuery } from "./AccessReviewCampaignsTab";
|
||||
|
||||
export default function AccessReviewCampaignsTabLoader() {
|
||||
const organizationId = useOrganizationId();
|
||||
const [queryRef, loadQuery] = useQueryLoader<AccessReviewCampaignsTabQuery>(accessReviewCampaignsTabQuery);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ organizationId });
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
if (!queryRef) return <PageSkeleton />;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<PageSkeleton />}>
|
||||
<AccessReviewCampaignsTab queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -131,10 +131,6 @@ export const campaignDetailPageQuery = graphql`
|
||||
id
|
||||
name
|
||||
status
|
||||
createdAt
|
||||
startedAt
|
||||
completedAt
|
||||
pendingEntryCount
|
||||
scopeSources {
|
||||
id
|
||||
source {
|
||||
|
||||
@@ -37,19 +37,29 @@ import { useMutation } from "react-relay";
|
||||
import { Link } from "react-router";
|
||||
import { graphql } from "relay-runtime";
|
||||
|
||||
import type { AccessReviewLayoutQuery$data } from "#/__generated__/core/AccessReviewLayoutQuery.graphql";
|
||||
import type { AddAccessSourceDialogConnectorProviderInfoFragment$data } from "#/__generated__/core/AddAccessSourceDialogConnectorProviderInfoFragment.graphql";
|
||||
import type { AddAccessSourceDialogCreateAPIKeyConnectorMutation } from "#/__generated__/core/AddAccessSourceDialogCreateAPIKeyConnectorMutation.graphql";
|
||||
import type { AddAccessSourceDialogCreateClientCredentialsConnectorMutation } from "#/__generated__/core/AddAccessSourceDialogCreateClientCredentialsConnectorMutation.graphql";
|
||||
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
|
||||
|
||||
import { createAccessSourceMutation } from "./CreateAccessSourceDialog";
|
||||
|
||||
type OrganizationData = Extract<
|
||||
AccessReviewLayoutQuery$data["organization"],
|
||||
{ readonly __typename: "Organization" }
|
||||
>;
|
||||
export const addAccessSourceDialogConnectorProviderInfoFragment = graphql`
|
||||
fragment AddAccessSourceDialogConnectorProviderInfoFragment on ConnectorProviderInfo @relay(plural: true) {
|
||||
provider
|
||||
displayName
|
||||
oauthConfigured
|
||||
apiKeySupported
|
||||
clientCredentialsSupported
|
||||
extraSettings {
|
||||
key
|
||||
label
|
||||
required
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export type ProviderInfo = OrganizationData["connectorProviderInfos"][number];
|
||||
export type ProviderInfo = AddAccessSourceDialogConnectorProviderInfoFragment$data[number];
|
||||
|
||||
type Props = {
|
||||
children: ReactNode;
|
||||
|
||||
@@ -26,18 +26,36 @@ import {
|
||||
useToast,
|
||||
} from "@probo/ui";
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { graphql, useMutation, usePaginationFragment } from "react-relay";
|
||||
import { useOutletContext, useSearchParams } from "react-router";
|
||||
import type { PreloadedQuery } from "react-relay";
|
||||
import { graphql, useFragment, useMutation, usePaginationFragment, usePreloadedQuery } from "react-relay";
|
||||
import { useSearchParams } from "react-router";
|
||||
|
||||
import type { AccessReviewSourcesTabFragment$key } from "#/__generated__/core/AccessReviewSourcesTabFragment.graphql";
|
||||
import type { AccessReviewSourcesTabPaginationQuery } from "#/__generated__/core/AccessReviewSourcesTabPaginationQuery.graphql";
|
||||
import type { AccessReviewSourcesTabQuery } from "#/__generated__/core/AccessReviewSourcesTabQuery.graphql";
|
||||
import type { AddAccessSourceDialogConnectorProviderInfoFragment$key } from "#/__generated__/core/AddAccessSourceDialogConnectorProviderInfoFragment.graphql";
|
||||
import type { CreateAccessSourceDialogMutation } from "#/__generated__/core/CreateAccessSourceDialogMutation.graphql";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import { AccessSourceRow } from "../_components/AccessSourceRow";
|
||||
import { AddAccessSourceDialog, type ProviderInfo } from "../dialogs/AddAccessSourceDialog";
|
||||
import { AddAccessSourceDialog, addAccessSourceDialogConnectorProviderInfoFragment } from "../dialogs/AddAccessSourceDialog";
|
||||
import { createAccessSourceMutation } from "../dialogs/CreateAccessSourceDialog";
|
||||
|
||||
export const accessReviewSourcesTabQuery = graphql`
|
||||
query AccessReviewSourcesTabQuery($organizationId: ID!) {
|
||||
organization: node(id: $organizationId) {
|
||||
__typename
|
||||
... on Organization {
|
||||
canCreateSource: permission(action: "core:access-source:create")
|
||||
connectorProviderInfos {
|
||||
...AddAccessSourceDialogConnectorProviderInfoFragment
|
||||
}
|
||||
...AccessReviewSourcesTabFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const sourcesFragment = graphql`
|
||||
fragment AccessReviewSourcesTabFragment on Organization
|
||||
@refetchable(queryName: "AccessReviewSourcesTabPaginationQuery")
|
||||
@@ -62,7 +80,6 @@ const sourcesFragment = graphql`
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
name
|
||||
connectorId
|
||||
connector {
|
||||
provider
|
||||
@@ -74,17 +91,26 @@ const sourcesFragment = graphql`
|
||||
}
|
||||
`;
|
||||
|
||||
export default function AccessReviewSourcesTab() {
|
||||
type Props = {
|
||||
queryRef: PreloadedQuery<AccessReviewSourcesTabQuery>;
|
||||
};
|
||||
|
||||
export default function AccessReviewSourcesTab({ queryRef }: Props) {
|
||||
const { __ } = useTranslate();
|
||||
const { toast } = useToast();
|
||||
const organizationId = useOrganizationId();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const processedConnectorIdRef = useRef<string | null>(null);
|
||||
const { organizationRef, canCreateSource, connectorProviderInfos } = useOutletContext<{
|
||||
organizationRef: AccessReviewSourcesTabFragment$key;
|
||||
canCreateSource: boolean;
|
||||
connectorProviderInfos: ReadonlyArray<ProviderInfo>;
|
||||
}>();
|
||||
|
||||
const { organization } = usePreloadedQuery(accessReviewSourcesTabQuery, queryRef);
|
||||
if (organization.__typename !== "Organization") {
|
||||
throw new Error("Organization not found");
|
||||
}
|
||||
|
||||
const connectorProviderInfos = useFragment<AddAccessSourceDialogConnectorProviderInfoFragment$key>(
|
||||
addAccessSourceDialogConnectorProviderInfoFragment,
|
||||
organization.connectorProviderInfos,
|
||||
);
|
||||
|
||||
const {
|
||||
data: { accessSources },
|
||||
@@ -94,7 +120,7 @@ export default function AccessReviewSourcesTab() {
|
||||
} = usePaginationFragment<
|
||||
AccessReviewSourcesTabPaginationQuery,
|
||||
AccessReviewSourcesTabFragment$key
|
||||
>(sourcesFragment, organizationRef);
|
||||
>(sourcesFragment, organization);
|
||||
|
||||
const existingSourceProviders = useMemo(
|
||||
() =>
|
||||
@@ -211,7 +237,7 @@ export default function AccessReviewSourcesTab() {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-end">
|
||||
{canCreateSource && (
|
||||
{organization.canCreateSource && (
|
||||
<AddAccessSourceDialog
|
||||
organizationId={organizationId}
|
||||
connectionId={accessSources.__id}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
// 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 { AccessReviewSourcesTabQuery } from "#/__generated__/core/AccessReviewSourcesTabQuery.graphql";
|
||||
import { PageSkeleton } from "#/components/skeletons/PageSkeleton";
|
||||
import { useOrganizationId } from "#/hooks/useOrganizationId";
|
||||
|
||||
import AccessReviewSourcesTab, { accessReviewSourcesTabQuery } from "./AccessReviewSourcesTab";
|
||||
|
||||
export default function AccessReviewSourcesTabLoader() {
|
||||
const organizationId = useOrganizationId();
|
||||
const [queryRef, loadQuery] = useQueryLoader<AccessReviewSourcesTabQuery>(accessReviewSourcesTabQuery);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({ organizationId });
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
if (!queryRef) return <PageSkeleton />;
|
||||
|
||||
return (
|
||||
<Suspense fallback={<PageSkeleton />}>
|
||||
<AccessReviewSourcesTab queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -15,14 +15,14 @@ export const accessReviewRoutes = [
|
||||
index: true,
|
||||
Fallback: PageSkeleton,
|
||||
Component: lazy(
|
||||
() => import("#/pages/organizations/access-reviews/campaigns/AccessReviewCampaignsTab"),
|
||||
() => import("#/pages/organizations/access-reviews/campaigns/AccessReviewCampaignsTabLoader"),
|
||||
),
|
||||
},
|
||||
{
|
||||
path: "sources",
|
||||
Fallback: PageSkeleton,
|
||||
Component: lazy(
|
||||
() => import("#/pages/organizations/access-reviews/sources/AccessReviewSourcesTab"),
|
||||
() => import("#/pages/organizations/access-reviews/sources/AccessReviewSourcesTabLoader"),
|
||||
),
|
||||
},
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user