diff --git a/apps/console/src/pages/iam/auth/AuthLayout.tsx b/apps/console/src/pages/iam/auth/AuthLayout.tsx
new file mode 100644
index 000000000..d352fc61a
--- /dev/null
+++ b/apps/console/src/pages/iam/auth/AuthLayout.tsx
@@ -0,0 +1,22 @@
+import { Logo } from "@probo/ui";
+import { Outlet } from "react-router";
+import { IAMRelayProvider } from "/providers/IAMRelayProvider";
+
+export default function () {
+ return (
+
+ );
+}
diff --git a/apps/console/src/pages/iam/auth/SignInPage.tsx b/apps/console/src/pages/iam/auth/SignInPage.tsx
index d035c99c3..e48df1fa2 100644
--- a/apps/console/src/pages/iam/auth/SignInPage.tsx
+++ b/apps/console/src/pages/iam/auth/SignInPage.tsx
@@ -16,7 +16,7 @@ const signInMutation = graphql`
}
`;
-// TODO initial screen + SAML login
+// TODO initial screen + SAML login + after sign in URL state
export default function SignInPage() {
const { __ } = useTranslate();
diff --git a/apps/console/src/pages/iam/memberships/MembershipLayout.tsx b/apps/console/src/pages/iam/memberships/MembershipLayout.tsx
index 70ca1504d..22de039a2 100644
--- a/apps/console/src/pages/iam/memberships/MembershipLayout.tsx
+++ b/apps/console/src/pages/iam/memberships/MembershipLayout.tsx
@@ -1,5 +1,4 @@
-import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
-import { membershipLayoutQuery } from "./MembershipLayoutQuery";
+import { graphql, usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { Link, Outlet } from "react-router";
import { Badge, Button, IconPeopleAdd, Layout, Skeleton } from "@probo/ui";
import { Sidebar } from "./_components/Sidebar";
@@ -11,7 +10,24 @@ import { Suspense } from "react";
import { useTranslate } from "@probo/i18n";
import { CoreRelayProvider } from "/providers/CoreRelayProvider";
-export default function MembershipLayout(props: {
+export const membershipLayoutQuery = graphql`
+ query MembershipLayoutQuery($organizationId: ID!) {
+ organization: node(id: $organizationId) @required(action: THROW) {
+ ... on Organization {
+ ...MembershipsDropdown_organizationFragment
+ ...SessionDropdownFragment
+ }
+ }
+ viewer @required(action: THROW) {
+ ...MembershipsDropdown_viewerFragment
+ pendingInvitations @required(action: THROW) {
+ totalCount @required(action: THROW)
+ }
+ }
+ }
+`;
+
+export function MembershipLayout(props: {
queryRef: PreloadedQuery;
}) {
const { queryRef } = props;
@@ -47,7 +63,7 @@ export default function MembershipLayout(props: {
)}
}>
-
+
>
}
diff --git a/apps/console/src/pages/iam/memberships/MembershipLayoutLoader.tsx b/apps/console/src/pages/iam/memberships/MembershipLayoutLoader.tsx
new file mode 100644
index 000000000..4e7433964
--- /dev/null
+++ b/apps/console/src/pages/iam/memberships/MembershipLayoutLoader.tsx
@@ -0,0 +1,38 @@
+import { useQueryLoader } from "react-relay";
+import { Suspense, useEffect } from "react";
+import { Skeleton } from "@probo/ui";
+import { useOrganizationId } from "/hooks/useOrganizationId";
+import type { MembershipLayoutQuery } from "./__generated__/MembershipLayoutQuery.graphql";
+import { MembershipLayout, membershipLayoutQuery } from "./MembershipLayout";
+import { IAMRelayProvider } from "/providers/IAMRelayProvider";
+
+function MembershipLayoutLoader() {
+ const organizationId = useOrganizationId();
+ const [queryRef, loadQuery] = useQueryLoader(
+ membershipLayoutQuery,
+ );
+
+ useEffect(() => {
+ loadQuery({
+ organizationId,
+ });
+ }, [loadQuery, organizationId]);
+
+ if (!queryRef) {
+ return ;
+ }
+
+ return (
+ }>
+
+
+ );
+}
+
+export default function () {
+ return (
+
+
+
+ );
+}
diff --git a/apps/console/src/pages/iam/memberships/MembershipLayoutQuery.tsx b/apps/console/src/pages/iam/memberships/MembershipLayoutQuery.tsx
deleted file mode 100644
index 471ef0533..000000000
--- a/apps/console/src/pages/iam/memberships/MembershipLayoutQuery.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import { lazy } from "@probo/react-lazy";
-import { useQueryLoader } from "react-relay";
-import { graphql } from "relay-runtime";
-import { Suspense, useEffect } from "react";
-import { Skeleton } from "@probo/ui";
-import { useOrganizationId } from "/hooks/useOrganizationId";
-import type { MembershipLayoutQuery } from "./__generated__/MembershipLayoutQuery.graphql";
-
-const Layout = lazy(() => import("./MembershipLayout"));
-
-export const membershipLayoutQuery = graphql`
- query MembershipLayoutQuery($organizationId: ID!) {
- organization: node(id: $organizationId) @required(action: THROW) {
- ... on Organization {
- ...MembershipsDropdown_organizationFragment
- }
- }
- viewer @required(action: THROW) {
- ...MembershipsDropdown_viewerFragment
- pendingInvitations @required(action: THROW) {
- totalCount @required(action: THROW)
- }
- ...SessionDropdownFragment @arguments(organizationId: $organizationId)
- }
- }
-`;
-
-export function MembershipLayoutQuery() {
- const organizationId = useOrganizationId();
- const [queryRef, loadQuery] = useQueryLoader(
- membershipLayoutQuery,
- );
-
- useEffect(() => {
- loadQuery({
- organizationId,
- });
- }, [loadQuery, organizationId]);
-
- if (!queryRef) {
- return ;
- }
-
- return (
- }>
-
-
- );
-}
diff --git a/apps/console/src/pages/iam/memberships/MembershipsPage.tsx b/apps/console/src/pages/iam/memberships/MembershipsPage.tsx
index 9876edf82..0fba51228 100644
--- a/apps/console/src/pages/iam/memberships/MembershipsPage.tsx
+++ b/apps/console/src/pages/iam/memberships/MembershipsPage.tsx
@@ -1,4 +1,4 @@
-import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
+import { graphql, usePreloadedQuery, type PreloadedQuery } from "react-relay";
import { useTranslate } from "@probo/i18n";
import {
Button,
@@ -9,15 +9,42 @@ import {
} from "@probo/ui";
import { useMemo, useState } from "react";
import { InvitationCard } from "./_components/InvitationCard";
-import { membershipsPageQuery } from "./MembershipsPageQuery";
import { MembershipCard } from "./_components/MembershipCard";
import type { MembershipsPageQuery } from "./__generated__/MembershipsPageQuery.graphql";
-interface PageProps {
- queryRef: PreloadedQuery;
-}
+export const membershipsPageQuery = graphql`
+ query MembershipsPageQuery {
+ viewer @required(action: THROW) {
+ memberships(first: 1000, orderBy: { direction: DESC, field: CREATED_AT })
+ @required(action: THROW) {
+ edges @required(action: THROW) {
+ node @required(action: THROW) {
+ id
+ ...MembershipCardFragment
+ organization @required(action: THROW) {
+ name
+ }
+ }
+ }
+ }
+ pendingInvitations(
+ first: 1000
+ orderBy: { direction: DESC, field: CREATED_AT }
+ ) @required(action: THROW) {
+ edges @required(action: THROW) {
+ node @required(action: THROW) {
+ id
+ ...InvitationCardFragment
+ }
+ }
+ }
+ }
+ }
+`;
-export default function MembershipsPage(props: PageProps) {
+export function MembershipsPage(props: {
+ queryRef: PreloadedQuery;
+}) {
const { __ } = useTranslate();
const [search, setSearch] = useState("");
diff --git a/apps/console/src/pages/iam/memberships/MembershipsPageLoader.tsx b/apps/console/src/pages/iam/memberships/MembershipsPageLoader.tsx
new file mode 100644
index 000000000..66695c4e3
--- /dev/null
+++ b/apps/console/src/pages/iam/memberships/MembershipsPageLoader.tsx
@@ -0,0 +1,33 @@
+import { useQueryLoader } from "react-relay";
+import { Suspense, useEffect } from "react";
+import { CenteredLayoutSkeleton } from "@probo/ui";
+import type { MembershipsPageQuery } from "./__generated__/MembershipsPageQuery.graphql";
+import { MembershipsPage, membershipsPageQuery } from "./MembershipsPage";
+import { IAMRelayProvider } from "/providers/IAMRelayProvider";
+
+function MembershipsPageLoader() {
+ const [queryRef, loadQuery] =
+ useQueryLoader(membershipsPageQuery);
+
+ useEffect(() => {
+ loadQuery({});
+ }, [loadQuery]);
+
+ if (!queryRef) {
+ return ;
+ }
+
+ return (
+ }>
+
+
+ );
+}
+
+export default function () {
+ return (
+
+
+
+ );
+}
diff --git a/apps/console/src/pages/iam/memberships/MembershipsPageQuery.tsx b/apps/console/src/pages/iam/memberships/MembershipsPageQuery.tsx
deleted file mode 100644
index cd33d8b6d..000000000
--- a/apps/console/src/pages/iam/memberships/MembershipsPageQuery.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-import { lazy } from "@probo/react-lazy";
-import { useQueryLoader } from "react-relay";
-import { graphql } from "relay-runtime";
-import { Suspense, useEffect } from "react";
-import { CenteredLayoutSkeleton } from "@probo/ui";
-import type { MembershipsPageQuery } from "./__generated__/MembershipsPageQuery.graphql";
-
-const Page = lazy(() => import("./MembershipsPage"));
-
-export const membershipsPageQuery = graphql`
- query MembershipsPageQuery {
- viewer @required(action: THROW) {
- memberships(first: 1000, orderBy: { direction: DESC, field: CREATED_AT })
- @required(action: THROW) {
- edges @required(action: THROW) {
- node @required(action: THROW) {
- id
- ...MembershipCardFragment
- organization @required(action: THROW) {
- name
- }
- }
- }
- }
- pendingInvitations(
- first: 1000
- orderBy: { direction: DESC, field: CREATED_AT }
- ) @required(action: THROW) {
- edges @required(action: THROW) {
- node @required(action: THROW) {
- id
- ...InvitationCardFragment
- }
- }
- }
- }
- }
-`;
-
-export function MembershipsPageQuery() {
- const [queryRef, loadQuery] =
- useQueryLoader(membershipsPageQuery);
-
- useEffect(() => {
- loadQuery({});
- }, [loadQuery]);
-
- if (!queryRef) {
- return ;
- }
-
- return (
- }>
-
-
- );
-}
diff --git a/apps/console/src/pages/iam/memberships/__generated__/MembershipLayoutQuery.graphql.ts b/apps/console/src/pages/iam/memberships/__generated__/MembershipLayoutQuery.graphql.ts
index 477ec2648..2f4229d0b 100644
--- a/apps/console/src/pages/iam/memberships/__generated__/MembershipLayoutQuery.graphql.ts
+++ b/apps/console/src/pages/iam/memberships/__generated__/MembershipLayoutQuery.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<<4b0d5d271f19e23939fc2b392aa79fbc>>
+ * @generated SignedSource<<5ad02f9e0c07f5e9900005150f93d6fe>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -15,13 +15,13 @@ export type MembershipLayoutQuery$variables = {
};
export type MembershipLayoutQuery$data = {
readonly organization: {
- readonly " $fragmentSpreads": FragmentRefs<"MembershipsDropdown_organizationFragment">;
+ readonly " $fragmentSpreads": FragmentRefs<"MembershipsDropdown_organizationFragment" | "SessionDropdownFragment">;
};
readonly viewer: {
readonly pendingInvitations: {
readonly totalCount: number;
};
- readonly " $fragmentSpreads": FragmentRefs<"MembershipsDropdown_viewerFragment" | "SessionDropdownFragment">;
+ readonly " $fragmentSpreads": FragmentRefs<"MembershipsDropdown_viewerFragment">;
};
};
export type MembershipLayoutQuery = {
@@ -51,14 +51,7 @@ v2 = {
"name": "totalCount",
"storageKey": null
},
-v3 = [
- {
- "kind": "Variable",
- "name": "organizationId",
- "variableName": "organizationId"
- }
-],
-v4 = {
+v3 = {
"alias": null,
"args": null,
"kind": "ScalarField",
@@ -89,6 +82,11 @@ return {
"args": null,
"kind": "FragmentSpread",
"name": "MembershipsDropdown_organizationFragment"
+ },
+ {
+ "args": null,
+ "kind": "FragmentSpread",
+ "name": "SessionDropdownFragment"
}
],
"type": "Organization",
@@ -133,11 +131,6 @@ return {
"storageKey": null
},
"action": "THROW"
- },
- {
- "args": (v3/*: any*/),
- "kind": "FragmentSpread",
- "name": "SessionDropdownFragment"
}
],
"storageKey": null
@@ -178,12 +171,62 @@ return {
"kind": "ScalarField",
"name": "name",
"storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "Membership",
+ "kind": "LinkedField",
+ "name": "viewerMembership",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "Identity",
+ "kind": "LinkedField",
+ "name": "identity",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "email",
+ "storageKey": null
+ },
+ (v3/*: any*/)
+ ],
+ "storageKey": null
+ },
+ {
+ "alias": null,
+ "args": null,
+ "concreteType": "MembershipProfile",
+ "kind": "LinkedField",
+ "name": "profile",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "fullName",
+ "storageKey": null
+ },
+ (v3/*: any*/)
+ ],
+ "storageKey": null
+ },
+ (v3/*: any*/)
+ ],
+ "storageKey": null
}
],
"type": "Organization",
"abstractKey": null
},
- (v4/*: any*/)
+ (v3/*: any*/)
],
"storageKey": null
},
@@ -207,56 +250,23 @@ return {
],
"storageKey": null
},
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "email",
- "storageKey": null
- },
- {
- "alias": null,
- "args": (v3/*: any*/),
- "concreteType": "IdentityProfile",
- "kind": "LinkedField",
- "name": "profileFor",
- "plural": false,
- "selections": [
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "firstName",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "lastName",
- "storageKey": null
- },
- (v4/*: any*/)
- ],
- "storageKey": null
- },
- (v4/*: any*/)
+ (v3/*: any*/)
],
"storageKey": null
}
]
},
"params": {
- "cacheID": "a849dd96b5088ddc9a3bbab50273c411",
+ "cacheID": "1fcebe9376b82798d829d98e2bdabaf3",
"id": null,
"metadata": {},
"name": "MembershipLayoutQuery",
"operationKind": "query",
- "text": "query MembershipLayoutQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...MembershipsDropdown_organizationFragment\n }\n id\n }\n viewer {\n ...MembershipsDropdown_viewerFragment\n pendingInvitations {\n totalCount\n }\n ...SessionDropdownFragment_4xMPKw\n id\n }\n}\n\nfragment MembershipsDropdown_organizationFragment on Organization {\n name\n}\n\nfragment MembershipsDropdown_viewerFragment on Identity {\n pendingInvitations {\n totalCount\n }\n}\n\nfragment SessionDropdownFragment_4xMPKw on Identity {\n email\n profileFor(organizationId: $organizationId) {\n firstName\n lastName\n id\n }\n}\n"
+ "text": "query MembershipLayoutQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...MembershipsDropdown_organizationFragment\n ...SessionDropdownFragment\n }\n id\n }\n viewer {\n ...MembershipsDropdown_viewerFragment\n pendingInvitations {\n totalCount\n }\n id\n }\n}\n\nfragment MembershipsDropdown_organizationFragment on Organization {\n name\n}\n\nfragment MembershipsDropdown_viewerFragment on Identity {\n pendingInvitations {\n totalCount\n }\n}\n\nfragment SessionDropdownFragment on Organization {\n viewerMembership {\n identity {\n email\n id\n }\n profile {\n fullName\n id\n }\n id\n }\n}\n"
}
};
})();
-(node as any).hash = "e28f1f5e510386718b39cd547d4ce59f";
+(node as any).hash = "c703bc4dce3307a22e42b5fa528537b2";
export default node;
diff --git a/apps/console/src/pages/iam/memberships/_components/SessionDropdown.tsx b/apps/console/src/pages/iam/memberships/_components/SessionDropdown.tsx
index 0792fd3a9..802c814ab 100644
--- a/apps/console/src/pages/iam/memberships/_components/SessionDropdown.tsx
+++ b/apps/console/src/pages/iam/memberships/_components/SessionDropdown.tsx
@@ -17,12 +17,14 @@ import { useTranslate } from "@probo/i18n";
import type { SessionDropdownFragment$key } from "./__generated__/SessionDropdownFragment.graphql";
export const fragment = graphql`
- fragment SessionDropdownFragment on Identity
- @argumentDefinitions(organizationId: { type: "ID!" }) {
- email
- profileFor(organizationId: $organizationId) {
- firstName
- lastName
+ fragment SessionDropdownFragment on Organization {
+ viewerMembership @required(action: THROW) {
+ identity @required(action: THROW) {
+ email
+ }
+ profile @required(action: THROW) {
+ fullName
+ }
}
}
`;
@@ -43,8 +45,12 @@ export function SessionDropdown(props: { fKey: SessionDropdownFragment$key }) {
const { isAuthorized } = use(PermissionsContext);
const { toast } = useToast();
- const { email, profileFor: profile } =
- useFragment(fragment, fKey);
+ const {
+ viewerMembership: {
+ identity: { email },
+ profile: { fullName },
+ },
+ } = useFragment(fragment, fKey);
const [signOut] = useMutation(signOutMutation);
const handleLogout: React.MouseEventHandler = (e) => {
@@ -65,11 +71,6 @@ export function SessionDropdown(props: { fKey: SessionDropdownFragment$key }) {
});
};
- // FIXME: user should always have a default profile
- const fullName = profile?.firstName
- ? `${profile.firstName} ${profile.lastName}`
- : email;
-
return (
{isAuthorized("Organization", "deleteOrganization") && (
diff --git a/apps/console/src/pages/iam/memberships/_components/__generated__/SessionDropdownFragment.graphql.ts b/apps/console/src/pages/iam/memberships/_components/__generated__/SessionDropdownFragment.graphql.ts
index 7a82b79a8..63504446e 100644
--- a/apps/console/src/pages/iam/memberships/_components/__generated__/SessionDropdownFragment.graphql.ts
+++ b/apps/console/src/pages/iam/memberships/_components/__generated__/SessionDropdownFragment.graphql.ts
@@ -1,5 +1,5 @@
/**
- * @generated SignedSource<>
+ * @generated SignedSource<<0d0906bc941900bb200a6a9824aaacc4>>
* @lightSyntaxTransform
* @nogrep
*/
@@ -11,11 +11,14 @@
import { ReaderFragment } from 'relay-runtime';
import { FragmentRefs } from "relay-runtime";
export type SessionDropdownFragment$data = {
- readonly email: any;
- readonly profileFor: {
- readonly firstName: string | null | undefined;
- readonly lastName: string | null | undefined;
- } | null | undefined;
+ readonly viewerMembership: {
+ readonly identity: {
+ readonly email: any;
+ };
+ readonly profile: {
+ readonly fullName: string;
+ };
+ };
readonly " $fragmentType": "SessionDropdownFragment";
};
export type SessionDropdownFragment$key = {
@@ -24,60 +27,75 @@ export type SessionDropdownFragment$key = {
};
const node: ReaderFragment = {
- "argumentDefinitions": [
- {
- "defaultValue": null,
- "kind": "LocalArgument",
- "name": "organizationId"
- }
- ],
+ "argumentDefinitions": [],
"kind": "Fragment",
"metadata": null,
"name": "SessionDropdownFragment",
"selections": [
{
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "email",
- "storageKey": null
- },
- {
- "alias": null,
- "args": [
- {
- "kind": "Variable",
- "name": "organizationId",
- "variableName": "organizationId"
- }
- ],
- "concreteType": "IdentityProfile",
- "kind": "LinkedField",
- "name": "profileFor",
- "plural": false,
- "selections": [
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "firstName",
- "storageKey": null
- },
- {
- "alias": null,
- "args": null,
- "kind": "ScalarField",
- "name": "lastName",
- "storageKey": null
- }
- ],
- "storageKey": null
+ "kind": "RequiredField",
+ "field": {
+ "alias": null,
+ "args": null,
+ "concreteType": "Membership",
+ "kind": "LinkedField",
+ "name": "viewerMembership",
+ "plural": false,
+ "selections": [
+ {
+ "kind": "RequiredField",
+ "field": {
+ "alias": null,
+ "args": null,
+ "concreteType": "Identity",
+ "kind": "LinkedField",
+ "name": "identity",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "email",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ "action": "THROW"
+ },
+ {
+ "kind": "RequiredField",
+ "field": {
+ "alias": null,
+ "args": null,
+ "concreteType": "MembershipProfile",
+ "kind": "LinkedField",
+ "name": "profile",
+ "plural": false,
+ "selections": [
+ {
+ "alias": null,
+ "args": null,
+ "kind": "ScalarField",
+ "name": "fullName",
+ "storageKey": null
+ }
+ ],
+ "storageKey": null
+ },
+ "action": "THROW"
+ }
+ ],
+ "storageKey": null
+ },
+ "action": "THROW"
}
],
- "type": "Identity",
+ "type": "Organization",
"abstractKey": null
};
-(node as any).hash = "de1d0f069e5648898cab6a56a394d86f";
+(node as any).hash = "c283fcefcab15b977b44ece82f7cf450";
export default node;
diff --git a/apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx b/apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx
index 886a1d560..9f15e9ef8 100644
--- a/apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx
+++ b/apps/console/src/pages/iam/organizations/NewOrganizationPage.tsx
@@ -5,6 +5,7 @@ import type { FormEventHandler } from "react";
import { useNavigate } from "react-router";
import { formatError, type GraphQLError } from "@probo/helpers";
import type { NewOrganizationPageMutation } from "./__generated__/NewOrganizationPageMutation.graphql";
+import { IAMRelayProvider } from "/providers/IAMRelayProvider";
const createOrganizationMutation = graphql`
mutation NewOrganizationPageMutation($input: CreateOrganizationInput!) {
@@ -17,7 +18,7 @@ const createOrganizationMutation = graphql`
}
`;
-export default function Page() {
+function NewOrganizationPage() {
const navigate = useNavigate();
const { toast } = useToast();
const { __ } = useTranslate();
@@ -97,3 +98,11 @@ export default function Page() {
);
}
+
+export default function () {
+ return (
+
+
+
+ );
+}
diff --git a/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPage.tsx b/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPage.tsx
index 9426a1dc2..05f0191ec 100644
--- a/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPage.tsx
+++ b/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPage.tsx
@@ -7,11 +7,23 @@ import { useMutationWithToasts } from "/hooks/useMutationWithToasts";
import { graphql } from "relay-runtime";
import { usePreloadedQuery, type PreloadedQuery } from "react-relay";
import type { GeneralSettingsPageQuery } from "./__generated__/GeneralSettingsPageQuery.graphql";
-import { generalSettingsPageQuery } from "./GeneralSettingsPageQuery";
import type { GeneralSettingsPage_deleteMutation } from "./__generated__/GeneralSettingsPage_deleteMutation.graphql";
import { DeleteOrganizationDialog } from "/components/organizations/DeleteOrganizationDialog";
import { OrganizationForm } from "./_components/OrganizationForm";
+export const generalSettingsPageQuery = graphql`
+ query GeneralSettingsPageQuery($organizationId: ID!) {
+ organization: node(id: $organizationId) @required(action: THROW) {
+ __typename
+ ... on Organization {
+ id
+ name @required(action: THROW)
+ ...OrganizationFormFragment
+ }
+ }
+ }
+`;
+
const deleteOrganizationMutation = graphql`
mutation GeneralSettingsPage_deleteMutation(
$input: DeleteOrganizationInput!
diff --git a/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPageQuery.tsx b/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPageLoader.tsx
similarity index 60%
rename from apps/console/src/pages/iam/organizations/settings/GeneralSettingsPageQuery.tsx
rename to apps/console/src/pages/iam/organizations/settings/GeneralSettingsPageLoader.tsx
index 0a71d55e7..1c09b9157 100644
--- a/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPageQuery.tsx
+++ b/apps/console/src/pages/iam/organizations/settings/GeneralSettingsPageLoader.tsx
@@ -1,25 +1,14 @@
-import { graphql } from "relay-runtime";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useQueryLoader } from "react-relay";
import { useEffect } from "react";
-import { GeneralSettingsPage } from "./GeneralSettingsPage";
+import {
+ GeneralSettingsPage,
+ generalSettingsPageQuery,
+} from "./GeneralSettingsPage";
import type { GeneralSettingsPageQuery } from "./__generated__/GeneralSettingsPageQuery.graphql";
import { IAMRelayProvider } from "/providers/IAMRelayProvider";
-export const generalSettingsPageQuery = graphql`
- query GeneralSettingsPageQuery($organizationId: ID!) {
- organization: node(id: $organizationId) @required(action: THROW) {
- __typename
- ... on Organization {
- id
- name @required(action: THROW)
- ...OrganizationFormFragment
- }
- }
- }
-`;
-
-function GeneralSettingsPageQuery() {
+function GeneralSettingsPageLoader() {
const organizationId = useOrganizationId();
const [queryRef, loadQuery] = useQueryLoader(
generalSettingsPageQuery,
@@ -41,7 +30,7 @@ function GeneralSettingsPageQuery() {
export default function () {
return (
-
+
);
}
diff --git a/apps/console/src/pages/iam/organizations/settings/SettingsLayout.tsx b/apps/console/src/pages/iam/organizations/settings/SettingsLayout.tsx
index d5981ebf5..e1e6fbc8d 100644
--- a/apps/console/src/pages/iam/organizations/settings/SettingsLayout.tsx
+++ b/apps/console/src/pages/iam/organizations/settings/SettingsLayout.tsx
@@ -11,7 +11,7 @@ import { Outlet } from "react-router";
import { useOrganizationId } from "/hooks/useOrganizationId";
import { useTranslate } from "@probo/i18n";
-export default function SettingsLayout() {
+export default function () {
const organizationId = useOrganizationId();
const { __ } = useTranslate();
diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx
index 129baa987..7f4e9fbf2 100644
--- a/apps/console/src/routes.tsx
+++ b/apps/console/src/routes.tsx
@@ -5,7 +5,7 @@ import {
useRouteError,
} from "react-router";
import { EmployeeLayout } from "./layouts/EmployeeLayout";
-import { AuthLayout, CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui";
+import { CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui";
import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx";
import { loadQuery } from "react-relay";
import { riskRoutes } from "./routes/riskRoutes.ts";
@@ -46,13 +46,6 @@ import {
UnAuthenticatedError,
UnauthorizedError,
} from "@probo/relay";
-import { MembershipsPageQuery } from "./pages/iam/memberships/MembershipsPageQuery.tsx";
-import { MembershipLayoutQuery } from "./pages/iam/memberships/MembershipLayoutQuery.tsx";
-import { IAMRelayProvider } from "./providers/IAMRelayProvider.tsx";
-
-const NewOrganizationPage = lazy(
- () => import("./pages/iam/organizations/NewOrganizationPage"),
-);
/**
* Top level error boundary
@@ -78,11 +71,7 @@ function ErrorBoundary({ error: propsError }: { error?: string }) {
const routes = [
{
path: "/auth",
- Component: () => (
-
-
-
- ),
+ Component: lazy(() => import("./pages/iam/auth/AuthLayout")),
children: [
{
path: "login",
@@ -118,18 +107,14 @@ const routes = [
children: [
{
index: true,
- Component: () => (
-
-
-
+ Component: lazy(
+ () => import("./pages/iam/memberships/MembershipsPageLoader"),
),
},
{
path: "organizations/new",
- Component: () => (
-
-
-
+ Component: lazy(
+ () => import("./pages/iam/organizations/NewOrganizationPage"),
),
},
{
@@ -184,10 +169,8 @@ const routes = [
},
{
path: "/organizations/:organizationId",
- Component: () => (
-
-
-
+ Component: lazy(
+ () => import("./pages/iam/memberships/MembershipLayoutLoader.tsx"),
),
ErrorBoundary: ErrorBoundary,
children: [
@@ -222,7 +205,7 @@ const routes = [
path: "general",
Component: lazy(
() =>
- import("./pages/iam/organizations/settings/GeneralSettingsPageQuery.tsx"),
+ import("./pages/iam/organizations/settings/GeneralSettingsPageLoader.tsx"),
),
},
// {
diff --git a/packages/ui/src/Layouts/AuthLayout.tsx b/packages/ui/src/Layouts/AuthLayout.tsx
deleted file mode 100644
index 930b200c3..000000000
--- a/packages/ui/src/Layouts/AuthLayout.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { Outlet } from "react-router";
-import { Logo } from "../Atoms/Logo/Logo";
-
-export function AuthLayout() {
- return (
-
- );
-}
diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts
index 967159602..a8bf59f25 100644
--- a/packages/ui/src/index.ts
+++ b/packages/ui/src/index.ts
@@ -1,6 +1,5 @@
// Layouts
export { Layout, Drawer } from "./Layouts/Layout";
-export { AuthLayout } from "./Layouts/AuthLayout";
export { ErrorLayout } from "./Layouts/ErrorLayout";
export {
CenteredLayout,
diff --git a/pkg/iam/account_service.go b/pkg/iam/account_service.go
index a3b20ae13..a96b3c9e4 100644
--- a/pkg/iam/account_service.go
+++ b/pkg/iam/account_service.go
@@ -16,6 +16,7 @@ package iam
import (
"context"
+ "errors"
"fmt"
"time"
@@ -715,6 +716,41 @@ func (s AccountService) ListOrganizations(ctx context.Context, identityID gid.GI
return organizations, nil
}
+func (s AccountService) GetMembershipForOrganization(
+ ctx context.Context,
+ identityID gid.GID,
+ organizationID gid.GID,
+) (*coredata.Membership, error) {
+ membership := &coredata.Membership{}
+
+ err := s.pg.WithTx(
+ ctx,
+ func(tx pg.Conn) error {
+ identity := &coredata.Identity{}
+
+ if err := identity.LoadByID(ctx, tx, identityID); err != nil {
+ if errors.Is(err, coredata.ErrResourceNotFound) {
+ return NewIdentityNotFoundError(identityID)
+ }
+
+ return fmt.Errorf("cannot load identity %q: %w", identityID, err)
+ }
+
+ if err := membership.LoadByIdentityInOrganization(ctx, tx, identityID, organizationID); err != nil {
+ return fmt.Errorf("cannot load membership: %w", err)
+ }
+
+ return nil
+ },
+ )
+
+ if err != nil {
+ return nil, err
+ }
+
+ return membership, nil
+}
+
func (s AccountService) GetProfileForMembership(ctx context.Context, membershipID gid.GID) (*coredata.MembershipProfile, error) {
var (
scope = coredata.NewScopeFromObjectID(membershipID)
diff --git a/pkg/server/api/connect/v1/schema.graphql b/pkg/server/api/connect/v1/schema.graphql
index fc6a85aef..f39a2f447 100644
--- a/pkg/server/api/connect/v1/schema.graphql
+++ b/pkg/server/api/connect/v1/schema.graphql
@@ -208,6 +208,8 @@ type Organization implements Node {
last: Int
before: CursorKey
): SAMLConfigurationConnection @goField(forceResolver: true)
+
+ viewerMembership: Membership @goField(forceResolver: true)
}
enum MembershipRole
diff --git a/pkg/server/api/connect/v1/schema/schema.go b/pkg/server/api/connect/v1/schema/schema.go
index 091e73fb3..fcf80cc07 100644
--- a/pkg/server/api/connect/v1/schema/schema.go
+++ b/pkg/server/api/connect/v1/schema/schema.go
@@ -237,6 +237,7 @@ type ComplexityRoot struct {
Name func(childComplexity int) int
SamlConfigurations func(childComplexity int, first *int, after *page.CursorKey, last *int, before *page.CursorKey) int
UpdatedAt func(childComplexity int) int
+ ViewerMembership func(childComplexity int) int
WebsiteURL func(childComplexity int) int
}
@@ -477,6 +478,7 @@ type OrganizationResolver interface {
Members(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.MembershipOrderBy) (*types.MembershipConnection, error)
Invitations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, status *coredata.InvitationStatus) (*types.InvitationConnection, error)
SamlConfigurations(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey) (*types.SAMLConfigurationConnection, error)
+ ViewerMembership(ctx context.Context, obj *types.Organization) (*types.Membership, error)
}
type PersonalAPIKeyConnectionResolver interface {
TotalCount(ctx context.Context, obj *types.PersonalAPIKeyConnection) (*int, error)
@@ -1282,6 +1284,12 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin
}
return e.complexity.Organization.UpdatedAt(childComplexity), true
+ case "Organization.viewerMembership":
+ if e.complexity.Organization.ViewerMembership == nil {
+ break
+ }
+
+ return e.complexity.Organization.ViewerMembership(childComplexity), true
case "Organization.websiteUrl":
if e.complexity.Organization.WebsiteURL == nil {
break
@@ -2189,6 +2197,8 @@ type Organization implements Node {
last: Int
before: CursorKey
): SAMLConfigurationConnection @goField(forceResolver: true)
+
+ viewerMembership: Membership @goField(forceResolver: true)
}
enum MembershipRole
@@ -3621,6 +3631,8 @@ func (ec *executionContext) fieldContext_CreateOrganizationPayload_organization(
return ec.fieldContext_Organization_invitations(ctx, field)
case "samlConfigurations":
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ case "viewerMembership":
+ return ec.fieldContext_Organization_viewerMembership(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
},
@@ -3870,6 +3882,8 @@ func (ec *executionContext) fieldContext_DeleteOrganizationHorizontalLogoPayload
return ec.fieldContext_Organization_invitations(ctx, field)
case "samlConfigurations":
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ case "viewerMembership":
+ return ec.fieldContext_Organization_viewerMembership(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
},
@@ -4639,6 +4653,8 @@ func (ec *executionContext) fieldContext_Invitation_organization(_ context.Conte
return ec.fieldContext_Organization_invitations(ctx, field)
case "samlConfigurations":
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ case "viewerMembership":
+ return ec.fieldContext_Organization_viewerMembership(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
},
@@ -5071,6 +5087,8 @@ func (ec *executionContext) fieldContext_Membership_organization(_ context.Conte
return ec.fieldContext_Organization_invitations(ctx, field)
case "samlConfigurations":
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ case "viewerMembership":
+ return ec.fieldContext_Organization_viewerMembership(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
},
@@ -7562,6 +7580,53 @@ func (ec *executionContext) fieldContext_Organization_samlConfigurations(ctx con
return fc, nil
}
+func (ec *executionContext) _Organization_viewerMembership(ctx context.Context, field graphql.CollectedField, obj *types.Organization) (ret graphql.Marshaler) {
+ return graphql.ResolveField(
+ ctx,
+ ec.OperationContext,
+ field,
+ ec.fieldContext_Organization_viewerMembership,
+ func(ctx context.Context) (any, error) {
+ return ec.resolvers.Organization().ViewerMembership(ctx, obj)
+ },
+ nil,
+ ec.marshalOMembership2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembership,
+ true,
+ false,
+ )
+}
+
+func (ec *executionContext) fieldContext_Organization_viewerMembership(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
+ fc = &graphql.FieldContext{
+ Object: "Organization",
+ Field: field,
+ IsMethod: true,
+ IsResolver: true,
+ Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
+ switch field.Name {
+ case "id":
+ return ec.fieldContext_Membership_id(ctx, field)
+ case "createdAt":
+ return ec.fieldContext_Membership_createdAt(ctx, field)
+ case "identity":
+ return ec.fieldContext_Membership_identity(ctx, field)
+ case "profile":
+ return ec.fieldContext_Membership_profile(ctx, field)
+ case "organization":
+ return ec.fieldContext_Membership_organization(ctx, field)
+ case "role":
+ return ec.fieldContext_Membership_role(ctx, field)
+ case "permissions":
+ return ec.fieldContext_Membership_permissions(ctx, field)
+ case "lastSession":
+ return ec.fieldContext_Membership_lastSession(ctx, field)
+ }
+ return nil, fmt.Errorf("no field named %q was found under type Membership", field.Name)
+ },
+ }
+ return fc, nil
+}
+
func (ec *executionContext) _OrganizationSessionCreated_session(ctx context.Context, field graphql.CollectedField, obj *types.OrganizationSessionCreated) (ret graphql.Marshaler) {
return graphql.ResolveField(
ctx,
@@ -7975,6 +8040,8 @@ func (ec *executionContext) fieldContext_Permission_organization(_ context.Conte
return ec.fieldContext_Organization_invitations(ctx, field)
case "samlConfigurations":
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ case "viewerMembership":
+ return ec.fieldContext_Organization_viewerMembership(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
},
@@ -8332,6 +8399,8 @@ func (ec *executionContext) fieldContext_PersonalAPIKey_organizations(_ context.
return ec.fieldContext_Organization_invitations(ctx, field)
case "samlConfigurations":
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ case "viewerMembership":
+ return ec.fieldContext_Organization_viewerMembership(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
},
@@ -10561,6 +10630,8 @@ func (ec *executionContext) fieldContext_UpdateOrganizationPayload_organization(
return ec.fieldContext_Organization_invitations(ctx, field)
case "samlConfigurations":
return ec.fieldContext_Organization_samlConfigurations(ctx, field)
+ case "viewerMembership":
+ return ec.fieldContext_Organization_viewerMembership(ctx, field)
}
return nil, fmt.Errorf("no field named %q was found under type Organization", field.Name)
},
@@ -15059,6 +15130,39 @@ func (ec *executionContext) _Organization(ctx context.Context, sel ast.Selection
continue
}
+ out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
+ case "viewerMembership":
+ field := field
+
+ innerFunc := func(ctx context.Context, _ *graphql.FieldSet) (res graphql.Marshaler) {
+ defer func() {
+ if r := recover(); r != nil {
+ ec.Error(ctx, ec.Recover(ctx, r))
+ }
+ }()
+ res = ec._Organization_viewerMembership(ctx, field, obj)
+ return res
+ }
+
+ if field.Deferrable != nil {
+ dfs, ok := deferred[field.Deferrable.Label]
+ di := 0
+ if ok {
+ dfs.AddField(field)
+ di = len(dfs.Values) - 1
+ } else {
+ dfs = graphql.NewFieldSet([]graphql.CollectedField{field})
+ deferred[field.Deferrable.Label] = dfs
+ }
+ dfs.Concurrently(di, func(ctx context.Context) graphql.Marshaler {
+ return innerFunc(ctx, dfs)
+ })
+
+ // don't run the out.Concurrently() call below
+ out.Values[i] = graphql.Null
+ continue
+ }
+
out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
default:
panic("unknown field " + strconv.Quote(field.Name))
@@ -18731,6 +18835,13 @@ func (ec *executionContext) marshalOInviteMemberPayload2ᚖgoᚗproboᚗincᚋpr
return ec._InviteMemberPayload(ctx, sel, v)
}
+func (ec *executionContext) marshalOMembership2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembership(ctx context.Context, sel ast.SelectionSet, v *types.Membership) graphql.Marshaler {
+ if v == nil {
+ return graphql.Null
+ }
+ return ec._Membership(ctx, sel, v)
+}
+
func (ec *executionContext) marshalOMembershipConnection2ᚖgoᚗproboᚗincᚋproboᚋpkgᚋserverᚋapiᚋconnectᚋv1ᚋtypesᚐMembershipConnection(ctx context.Context, sel ast.SelectionSet, v *types.MembershipConnection) graphql.Marshaler {
if v == nil {
return graphql.Null
diff --git a/pkg/server/api/connect/v1/types/types.go b/pkg/server/api/connect/v1/types/types.go
index 59565e1e9..730a9ed31 100644
--- a/pkg/server/api/connect/v1/types/types.go
+++ b/pkg/server/api/connect/v1/types/types.go
@@ -239,6 +239,7 @@ type Organization struct {
Members *MembershipConnection `json:"members,omitempty"`
Invitations *InvitationConnection `json:"invitations,omitempty"`
SamlConfigurations *SAMLConfigurationConnection `json:"samlConfigurations,omitempty"`
+ ViewerMembership *Membership `json:"viewerMembership,omitempty"`
}
func (Organization) IsNode() {}
diff --git a/pkg/server/api/connect/v1/v1_resolver.go b/pkg/server/api/connect/v1/v1_resolver.go
index 0b24d056d..f43cb28b0 100644
--- a/pkg/server/api/connect/v1/v1_resolver.go
+++ b/pkg/server/api/connect/v1/v1_resolver.go
@@ -1013,6 +1013,19 @@ func (r *organizationResolver) SamlConfigurations(ctx context.Context, obj *type
return types.NewSAMLConfigurationConnection(page, r, obj.ID), nil
}
+// ViewerMembership is the resolver for the viewerMembership field.
+func (r *organizationResolver) ViewerMembership(ctx context.Context, obj *types.Organization) (*types.Membership, error) {
+ identity := IdentityFromContext(ctx)
+
+ membership, err := r.iam.AccountService.GetMembershipForOrganization(ctx, identity.ID, obj.ID)
+ if err != nil {
+ r.logger.ErrorCtx(ctx, "cannot get membership for organization", log.Error(err))
+ return nil, gqlutils.InternalServerError(ctx)
+ }
+
+ return types.NewMembership(membership), nil
+}
+
// TotalCount is the resolver for the totalCount field.
func (r *personalAPIKeyConnectionResolver) TotalCount(ctx context.Context, obj *types.PersonalAPIKeyConnection) (*int, error) {
switch obj.Resolver.(type) {