Add SAML support

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-10-29 18:42:12 +01:00
parent 3018a3e691
commit 2766f8e423
97 changed files with 14824 additions and 2812 deletions

View File

@@ -30,17 +30,16 @@ import {
DropdownItem,
IconChevronGrabberVertical,
IconPlusLarge,
IconChevronDown,
Avatar,
IconPeopleAdd,
Badge,
IconLock,
} from "@probo/ui";
import { useTranslate } from "@probo/i18n";
import { graphql } from "relay-runtime";
import { useLazyLoadQuery, usePaginationFragment } from "react-relay";
import { useLazyLoadQuery } from "react-relay";
import type { MainLayoutQuery as MainLayoutQueryType } from "./__generated__/MainLayoutQuery.graphql";
import type { MainLayout_OrganizationSelector_viewer$key } from "./__generated__/MainLayout_OrganizationSelector_viewer.graphql";
import { Suspense, useState } from "react";
import { Suspense, useState, useEffect } from "react";
import { useToast } from "@probo/ui";
import { ErrorBoundary } from "react-error-boundary";
import { PageError } from "/components/PageError";
@@ -54,7 +53,9 @@ const MainLayoutQuery = graphql`
fullName
email
}
...MainLayout_OrganizationSelector_viewer
invitations(first: 1, filter: {statuses: [PENDING]}) {
totalCount
}
}
organization: node(id: $organizationId) {
... on Organization {
@@ -66,33 +67,6 @@ const MainLayoutQuery = graphql`
}
`;
const OrganizationSelectorFragment = graphql`
fragment MainLayout_OrganizationSelector_viewer on Viewer
@refetchable(queryName: "MainLayoutOrganizationSelectorPaginationQuery")
@argumentDefinitions(
first: { type: "Int", defaultValue: 25 }
after: { type: "CursorKey" }
) {
organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC})
@connection(key: "MainLayout_OrganizationSelector_organizations") {
edges {
node {
id
name
logoUrl
}
}
pageInfo {
hasNextPage
endCursor
}
}
invitations(first: 1, filter: {statuses: [PENDING]}) {
totalCount
}
}
`;
/**
* Site layout with a header and a sidebar
*/
@@ -228,7 +202,7 @@ function UserDropdown({ organizationId }: { organizationId: string }) {
) => {
e.preventDefault();
fetch(buildEndpoint("/api/console/v1/auth/logout"), {
fetch(buildEndpoint("/auth/logout"), {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@@ -271,6 +245,19 @@ function UserDropdown({ organizationId }: { organizationId: string }) {
);
}
interface Organization {
id: string;
name: string;
logoUrl?: string | null;
authenticationMethod: string;
authStatus: "authenticated" | "unauthenticated" | "expired";
loginUrl: string;
}
interface OrganizationsResponse {
organizations: Organization[];
}
function OrganizationSelectorWrapper({ organizationId }: { organizationId: string }) {
const data = useLazyLoadQuery<MainLayoutQueryType>(MainLayoutQuery, { organizationId });
return <OrganizationSelector viewer={data.viewer} currentOrganization={data.organization} />;
@@ -280,31 +267,55 @@ function OrganizationSelector({
viewer,
currentOrganization
}: {
viewer: MainLayout_OrganizationSelector_viewer$key;
viewer: MainLayoutQueryType["response"]["viewer"];
currentOrganization: MainLayoutQueryType["response"]["organization"];
}) {
const [isLoadingMore, setIsLoadingMore] = useState(false);
const [organizations, setOrganizations] = useState<Organization[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { __ } = useTranslate();
const { data, loadNext, hasNext } = usePaginationFragment(
OrganizationSelectorFragment,
viewer
);
const pendingInvitationsCount = viewer.invitations.totalCount;
const organizations = data.organizations.edges.map((edge) => edge.node);
const pendingInvitationsCount = data.invitations.totalCount;
useEffect(() => {
const fetchOrganizations = async () => {
try {
setIsLoading(true);
const response = await fetch('/auth/organizations', {
credentials: 'include',
});
const handleLoadMore = (e?: React.MouseEvent) => {
e?.preventDefault();
e?.stopPropagation();
if (!response.ok) {
throw new Error('Failed to fetch organizations');
}
if (hasNext && !isLoadingMore) {
setIsLoadingMore(true);
loadNext(25, {
onComplete: () => setIsLoadingMore(false),
});
}
};
const data: OrganizationsResponse = await response.json();
setOrganizations(data.organizations);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Unknown error');
console.error('Failed to fetch organizations:', err);
} finally {
setIsLoading(false);
}
};
fetchOrganizations();
}, []);
if (error) {
return (
<div className="flex items-center gap-1">
<Button
className="-ml-3"
variant="tertiary"
disabled
>
{__("Error loading organizations")}
</Button>
</div>
);
}
return (
<div className="flex items-center gap-1">
@@ -314,39 +325,70 @@ function OrganizationSelector({
className="-ml-3"
variant="tertiary"
iconAfter={IconChevronGrabberVertical}
disabled={isLoading}
>
{currentOrganization?.name || ""}
{isLoading ? __("Loading...") : (currentOrganization?.name || "")}
</Button>
}
>
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
{organizations.map((organization) => (
<DropdownItem
asChild
key={organization.id}
>
<Link to={`/organizations/${organization.id}`}>
<Avatar src={organization.logoUrl} name={organization.name} />
{organization.name}
</Link>
</DropdownItem>
))}
{hasNext && (
<div className="px-3 py-1 flex justify-center">
<Button
variant="tertiary"
onClick={handleLoadMore}
onMouseDown={(e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
}}
className="mx-auto"
icon={IconChevronDown}
disabled={isLoadingMore}
>
{isLoadingMore ? __("Loading...") : __("Show More")}
</Button>
{isLoading ? (
<div className="px-3 py-2 text-gray-500">
{__("Loading organizations...")}
</div>
) : organizations.length === 0 ? (
<div className="px-3 py-2 text-gray-500">
{__("No organizations found")}
</div>
) : (
organizations.map((organization) => {
const isAuthenticated = organization.authStatus === "authenticated";
const isExpired = organization.authStatus === "expired";
const needsAuth = organization.authStatus === "unauthenticated";
const targetUrl = isAuthenticated
? `/organizations/${organization.id}`
: organization.loginUrl;
const isSAMLUrl = targetUrl.includes('/auth/saml/');
return (
<DropdownItem
asChild
key={organization.id}
>
{isSAMLUrl ? (
<a href={targetUrl} className="flex items-center gap-2">
<Avatar name={organization.name} src={organization.logoUrl} />
<span className="flex-1">{organization.name}</span>
{isAuthenticated && (
<IconCheckmark1 size={16} className="text-green-600" />
)}
{isExpired && (
<IconClock size={16} className="text-orange-600" />
)}
{needsAuth && (
<IconLock size={16} className="text-gray-400" />
)}
</a>
) : (
<Link to={targetUrl} className="flex items-center gap-2">
<Avatar name={organization.name} src={organization.logoUrl} />
<span className="flex-1">{organization.name}</span>
{isAuthenticated && (
<IconCheckmark1 size={16} className="text-green-600" />
)}
{isExpired && (
<IconClock size={16} className="text-orange-600" />
)}
{needsAuth && (
<IconLock size={16} className="text-gray-400" />
)}
</Link>
)}
</DropdownItem>
);
})
)}
</div>
<DropdownSeparator />

View File

@@ -1,263 +0,0 @@
/**
* @generated SignedSource<<3a99643b92330af6920aac4c2286376f>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MainLayoutOrganizationSelectorPaginationQuery$variables = {
after?: any | null | undefined;
first?: number | null | undefined;
};
export type MainLayoutOrganizationSelectorPaginationQuery$data = {
readonly viewer: {
readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">;
};
};
export type MainLayoutOrganizationSelectorPaginationQuery = {
response: MainLayoutOrganizationSelectorPaginationQuery$data;
variables: MainLayoutOrganizationSelectorPaginationQuery$variables;
};
const node: ConcreteRequest = (function(){
var v0 = [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": 25,
"kind": "LocalArgument",
"name": "first"
}
],
v1 = {
"kind": "Variable",
"name": "after",
"variableName": "after"
},
v2 = {
"kind": "Variable",
"name": "first",
"variableName": "first"
},
v3 = [
(v1/*: any*/),
(v2/*: any*/),
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "NAME"
}
}
],
v4 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
};
return {
"fragment": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Fragment",
"metadata": null,
"name": "MainLayoutOrganizationSelectorPaginationQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"args": [
(v1/*: any*/),
(v2/*: any*/)
],
"kind": "FragmentSpread",
"name": "MainLayout_OrganizationSelector_viewer"
}
],
"storageKey": null
}
],
"type": "Query",
"abstractKey": null
},
"kind": "Request",
"operation": {
"argumentDefinitions": (v0/*: any*/),
"kind": "Operation",
"name": "MainLayoutOrganizationSelectorPaginationQuery",
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Viewer",
"kind": "LinkedField",
"name": "viewer",
"plural": false,
"selections": [
{
"alias": null,
"args": (v3/*: any*/),
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "organizations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v4/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": (v3/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "MainLayout_OrganizationSelector_organizations",
"kind": "LinkedHandle",
"name": "organizations"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"statuses": [
"PENDING"
]
}
},
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "invitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
},
(v4/*: any*/)
],
"storageKey": null
}
]
},
"params": {
"cacheID": "d51b49a50afe664ee9903c1bb265ab79",
"id": null,
"metadata": {},
"name": "MainLayoutOrganizationSelectorPaginationQuery",
"operationKind": "query",
"text": "query MainLayoutOrganizationSelectorPaginationQuery(\n $after: CursorKey\n $first: Int = 25\n) {\n viewer {\n ...MainLayout_OrganizationSelector_viewer_2HEEH6\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer_2HEEH6 on Viewer {\n organizations(first: $first, after: $after, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n}\n"
}
};
})();
(node as any).hash = "3e00f1a6f8089fc59144807a07fd1bdf";
export default node;

View File

@@ -1,5 +1,5 @@
/**
* @generated SignedSource<<1a7aa899a9b6251477122c87ae1c6431>>
* @generated SignedSource<<26b3620e6aed7f97ffb1710be1eb267a>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -9,7 +9,6 @@
// @ts-nocheck
import { ConcreteRequest } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MainLayoutQuery$variables = {
organizationId: string;
};
@@ -21,11 +20,13 @@ export type MainLayoutQuery$data = {
};
readonly viewer: {
readonly id: string;
readonly invitations: {
readonly totalCount: number;
};
readonly user: {
readonly email: string;
readonly fullName: string;
};
readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">;
};
};
export type MainLayoutQuery = {
@@ -62,48 +63,59 @@ v3 = {
"name": "email",
"storageKey": null
},
v4 = [
v4 = {
"alias": null,
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"statuses": [
"PENDING"
]
}
},
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "invitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
},
v5 = [
{
"kind": "Variable",
"name": "id",
"variableName": "organizationId"
}
],
v5 = {
v6 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
v6 = {
v7 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
v7 = [
{
"kind": "Literal",
"name": "first",
"value": 25
},
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "NAME"
}
}
],
v8 = {
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
};
return {
"fragment": {
@@ -134,17 +146,13 @@ return {
],
"storageKey": null
},
{
"args": null,
"kind": "FragmentSpread",
"name": "MainLayout_OrganizationSelector_viewer"
}
(v4/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v4/*: any*/),
"args": (v5/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
@@ -154,8 +162,8 @@ return {
"kind": "InlineFragment",
"selections": [
(v1/*: any*/),
(v5/*: any*/),
(v6/*: any*/)
(v6/*: any*/),
(v7/*: any*/)
],
"type": "Organization",
"abstractKey": null
@@ -196,137 +204,31 @@ return {
],
"storageKey": null
},
{
"alias": null,
"args": (v7/*: any*/),
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "organizations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v1/*: any*/),
(v5/*: any*/),
(v6/*: any*/),
(v8/*: any*/)
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "organizations(first:25,orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
},
{
"alias": null,
"args": (v7/*: any*/),
"filters": [
"orderBy"
],
"handle": "connection",
"key": "MainLayout_OrganizationSelector_organizations",
"kind": "LinkedHandle",
"name": "organizations"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"statuses": [
"PENDING"
]
}
},
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "invitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
}
(v4/*: any*/)
],
"storageKey": null
},
{
"alias": "organization",
"args": (v4/*: any*/),
"args": (v5/*: any*/),
"concreteType": null,
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
(v8/*: any*/),
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
},
(v1/*: any*/),
{
"kind": "InlineFragment",
"selections": [
(v5/*: any*/),
(v6/*: any*/)
(v6/*: any*/),
(v7/*: any*/)
],
"type": "Organization",
"abstractKey": null
@@ -337,16 +239,16 @@ return {
]
},
"params": {
"cacheID": "6de25e54419d82cb6465c903a0afdd78",
"cacheID": "a8f9f58d27677c55b5a217617db83e27",
"id": null,
"metadata": {},
"name": "MainLayoutQuery",
"operationKind": "query",
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n ...MainLayout_OrganizationSelector_viewer\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n\nfragment MainLayout_OrganizationSelector_viewer on Viewer {\n organizations(first: 25, orderBy: {field: NAME, direction: ASC}) {\n edges {\n node {\n id\n name\n logoUrl\n __typename\n }\n cursor\n }\n pageInfo {\n hasNextPage\n endCursor\n }\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n}\n"
"text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\n user {\n fullName\n email\n id\n }\n invitations(first: 1, filter: {statuses: [PENDING]}) {\n totalCount\n }\n }\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n id\n name\n logoUrl\n }\n id\n }\n}\n"
}
};
})();
(node as any).hash = "aaaca58a896839aa85ce7c2fcf75de39";
(node as any).hash = "17986fcea321c4567d86584d1a9f89c1";
export default node;

View File

@@ -1,226 +0,0 @@
/**
* @generated SignedSource<<b0e167541047d61efc6d30382c062d0d>>
* @lightSyntaxTransform
* @nogrep
*/
/* tslint:disable */
/* eslint-disable */
// @ts-nocheck
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type MainLayout_OrganizationSelector_viewer$data = {
readonly invitations: {
readonly totalCount: number;
};
readonly organizations: {
readonly edges: ReadonlyArray<{
readonly node: {
readonly id: string;
readonly logoUrl: string | null | undefined;
readonly name: string;
};
}>;
readonly pageInfo: {
readonly endCursor: any | null | undefined;
readonly hasNextPage: boolean;
};
};
readonly " $fragmentType": "MainLayout_OrganizationSelector_viewer";
};
export type MainLayout_OrganizationSelector_viewer$key = {
readonly " $data"?: MainLayout_OrganizationSelector_viewer$data;
readonly " $fragmentSpreads": FragmentRefs<"MainLayout_OrganizationSelector_viewer">;
};
import MainLayoutOrganizationSelectorPaginationQuery_graphql from './MainLayoutOrganizationSelectorPaginationQuery.graphql';
const node: ReaderFragment = (function(){
var v0 = [
"organizations"
];
return {
"argumentDefinitions": [
{
"defaultValue": null,
"kind": "LocalArgument",
"name": "after"
},
{
"defaultValue": 25,
"kind": "LocalArgument",
"name": "first"
}
],
"kind": "Fragment",
"metadata": {
"connection": [
{
"count": "first",
"cursor": "after",
"direction": "forward",
"path": (v0/*: any*/)
}
],
"refetch": {
"connection": {
"forward": {
"count": "first",
"cursor": "after"
},
"backward": null,
"path": (v0/*: any*/)
},
"fragmentPathInResult": [
"viewer"
],
"operation": MainLayoutOrganizationSelectorPaginationQuery_graphql
}
},
"name": "MainLayout_OrganizationSelector_viewer",
"selections": [
{
"alias": "organizations",
"args": [
{
"kind": "Literal",
"name": "orderBy",
"value": {
"direction": "ASC",
"field": "NAME"
}
}
],
"concreteType": "OrganizationConnection",
"kind": "LinkedField",
"name": "__MainLayout_OrganizationSelector_organizations_connection",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "OrganizationEdge",
"kind": "LinkedField",
"name": "edges",
"plural": true,
"selections": [
{
"alias": null,
"args": null,
"concreteType": "Organization",
"kind": "LinkedField",
"name": "node",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "id",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "name",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "logoUrl",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "__typename",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "cursor",
"storageKey": null
}
],
"storageKey": null
},
{
"alias": null,
"args": null,
"concreteType": "PageInfo",
"kind": "LinkedField",
"name": "pageInfo",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "hasNextPage",
"storageKey": null
},
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "endCursor",
"storageKey": null
}
],
"storageKey": null
}
],
"storageKey": "__MainLayout_OrganizationSelector_organizations_connection(orderBy:{\"direction\":\"ASC\",\"field\":\"NAME\"})"
},
{
"alias": null,
"args": [
{
"kind": "Literal",
"name": "filter",
"value": {
"statuses": [
"PENDING"
]
}
},
{
"kind": "Literal",
"name": "first",
"value": 1
}
],
"concreteType": "InvitationConnection",
"kind": "LinkedField",
"name": "invitations",
"plural": false,
"selections": [
{
"alias": null,
"args": null,
"kind": "ScalarField",
"name": "totalCount",
"storageKey": null
}
],
"storageKey": "invitations(filter:{\"statuses\":[\"PENDING\"]},first:1)"
}
],
"type": "Viewer",
"abstractKey": null
};
})();
(node as any).hash = "3e00f1a6f8089fc59144807a07fd1bdf";
export default node;