From 42182004095eff05ab6ca93934d67c0ec28812d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 19 Dec 2025 19:46:20 +0100 Subject: [PATCH] Replug organizations page without assume or user dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Émile Ré --- .../__generated__/MainLayoutQuery.graphql.ts | 161 -------- apps/console/src/pages/OrganizationsPage.tsx | 364 ------------------ .../iam/organizations/OrganizationLayout.tsx | 48 +++ .../organizations/OrganizationLayoutQuery.tsx | 47 +++ .../iam/organizations/OrganizationsPage.tsx | 18 +- ...nsQuery.tsx => OrganizationsPageQuery.tsx} | 31 +- .../OrganizationLayoutQuery.graphql.ts | 188 +++++++++ ...l.ts => OrganizationsPageQuery.graphql.ts} | 194 ++++++---- .../sessionIsExpiredFragment.graphql.ts | 42 -- .../_components/InvitationCard.tsx | 2 +- .../_components/MembershipCard.tsx | 14 +- .../_components/OrganizationCard.tsx | 126 ------ .../_components/OrganizationDropdown.tsx | 89 +++++ .../_components/OrganizationDropdownMenu.tsx | 59 +++ .../OrganizationDropdownMenuItem.tsx | 50 +++ .../iam/organizations/_components/Sidebar.tsx | 166 ++++++++ .../InvitationCardFragment.graphql.ts | 42 +- .../MembershipCardFragment.graphql.ts | 60 +-- .../OrganizationCardFragment.graphql.ts | 58 --- .../OrganizationDropdownFragment.graphql.ts | 42 ++ ...izationDropdownMenuItemFragment.graphql.ts | 101 +++++ .../OrganizationDropdownMenuQuery.graphql.ts | 263 +++++++++++++ .../src/providers/PermissionsProvider.tsx | 44 ++- apps/console/src/routes.tsx | 10 +- packages/ui/src/Atoms/Dropdown/Dropdown.tsx | 5 +- 25 files changed, 1286 insertions(+), 938 deletions(-) delete mode 100644 apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts delete mode 100644 apps/console/src/pages/OrganizationsPage.tsx create mode 100644 apps/console/src/pages/iam/organizations/OrganizationLayout.tsx create mode 100644 apps/console/src/pages/iam/organizations/OrganizationLayoutQuery.tsx rename apps/console/src/pages/iam/organizations/{OrganizationsQuery.tsx => OrganizationsPageQuery.tsx} (56%) create mode 100644 apps/console/src/pages/iam/organizations/__generated__/OrganizationLayoutQuery.graphql.ts rename apps/console/src/pages/iam/organizations/__generated__/{OrganizationsQuery.graphql.ts => OrganizationsPageQuery.graphql.ts} (58%) delete mode 100644 apps/console/src/pages/iam/organizations/__generated__/sessionIsExpiredFragment.graphql.ts delete mode 100644 apps/console/src/pages/iam/organizations/_components/OrganizationCard.tsx create mode 100644 apps/console/src/pages/iam/organizations/_components/OrganizationDropdown.tsx create mode 100644 apps/console/src/pages/iam/organizations/_components/OrganizationDropdownMenu.tsx create mode 100644 apps/console/src/pages/iam/organizations/_components/OrganizationDropdownMenuItem.tsx create mode 100644 apps/console/src/pages/iam/organizations/_components/Sidebar.tsx delete mode 100644 apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationCardFragment.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownFragment.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownMenuItemFragment.graphql.ts create mode 100644 apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownMenuQuery.graphql.ts diff --git a/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts b/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts deleted file mode 100644 index 6db37dbe4..000000000 --- a/apps/console/src/layouts/__generated__/MainLayoutQuery.graphql.ts +++ /dev/null @@ -1,161 +0,0 @@ -/** - * @generated SignedSource<> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ConcreteRequest } from 'relay-runtime'; -export type MainLayoutQuery$variables = { - organizationId: string; -}; -export type MainLayoutQuery$data = { - readonly organization: { - readonly id?: string; - readonly logoUrl?: string | null | undefined; - readonly name?: string; - }; - readonly viewer: { - readonly id: string; - }; -}; -export type MainLayoutQuery = { - response: MainLayoutQuery$data; - variables: MainLayoutQuery$variables; -}; - -const node: ConcreteRequest = (function(){ -var v0 = [ - { - "defaultValue": null, - "kind": "LocalArgument", - "name": "organizationId" - } -], -v1 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "id", - "storageKey": null -}, -v2 = { - "alias": null, - "args": null, - "concreteType": "Viewer", - "kind": "LinkedField", - "name": "viewer", - "plural": false, - "selections": [ - (v1/*: any*/) - ], - "storageKey": null -}, -v3 = [ - { - "kind": "Variable", - "name": "id", - "variableName": "organizationId" - } -], -v4 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null -}, -v5 = { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "logoUrl", - "storageKey": null -}; -return { - "fragment": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Fragment", - "metadata": null, - "name": "MainLayoutQuery", - "selections": [ - (v2/*: any*/), - { - "alias": "organization", - "args": (v3/*: any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "kind": "InlineFragment", - "selections": [ - (v1/*: any*/), - (v4/*: any*/), - (v5/*: any*/) - ], - "type": "Organization", - "abstractKey": null - } - ], - "storageKey": null - } - ], - "type": "Query", - "abstractKey": null - }, - "kind": "Request", - "operation": { - "argumentDefinitions": (v0/*: any*/), - "kind": "Operation", - "name": "MainLayoutQuery", - "selections": [ - (v2/*: any*/), - { - "alias": "organization", - "args": (v3/*: any*/), - "concreteType": null, - "kind": "LinkedField", - "name": "node", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "__typename", - "storageKey": null - }, - (v1/*: any*/), - { - "kind": "InlineFragment", - "selections": [ - (v4/*: any*/), - (v5/*: any*/) - ], - "type": "Organization", - "abstractKey": null - } - ], - "storageKey": null - } - ] - }, - "params": { - "cacheID": "2ec49ff40720bacec29e7b6b1bf1408b", - "id": null, - "metadata": {}, - "name": "MainLayoutQuery", - "operationKind": "query", - "text": "query MainLayoutQuery(\n $organizationId: ID!\n) {\n viewer {\n id\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 = "d958a2acbd9d13698b9c2b350b05d5a0"; - -export default node; diff --git a/apps/console/src/pages/OrganizationsPage.tsx b/apps/console/src/pages/OrganizationsPage.tsx deleted file mode 100644 index 6c78df177..000000000 --- a/apps/console/src/pages/OrganizationsPage.tsx +++ /dev/null @@ -1,364 +0,0 @@ -import { useTranslate } from "@probo/i18n"; -import { useEffect, useMemo, useState } from "react"; -import { Link, useNavigate } from "react-router"; -import { - Avatar, - Button, - Card, - IconPlusLarge, - IconCheckmark1, - IconLock, - IconClock, - IconMagnifyingGlass, - Badge, - Input, -} from "@probo/ui"; -import { usePageTitle } from "@probo/hooks"; -import { formatDate } from "@probo/helpers"; - -interface Organization { - id: string; - name: string; - logoUrl?: string | null; - authenticationMethod: string; - authStatus: "authenticated" | "unauthenticated" | "expired"; - loginUrl: string; -} - -interface Invitation { - id: string; - email: string; - fullName: string; - role: string; - expiresAt: string; - acceptedAt?: string | null; - createdAt: string; - organization: { - id: string; - name: string; - }; -} - -export default function OrganizationsPage() { - const { __ } = useTranslate(); - const navigate = useNavigate(); - - const [organizations, setOrganizations] = useState([]); - const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true); - const [invitations, setInvitations] = useState([]); - const [isLoadingInvitations, setIsLoadingInvitations] = useState(true); - const [isAccepting, setIsAccepting] = useState(false); - const [search, setSearch] = useState(""); - - const filteredOrganizations = useMemo(() => { - if (!search.trim()) { - return organizations; - } - return organizations.filter((org) => - org.name.toLowerCase().includes(search.toLowerCase()) - ); - }, [organizations, search]); - - // Fetch organizations from REST endpoint - useEffect(() => { - const fetchOrganizations = async () => { - try { - const response = await fetch('/connect/organizations', { - credentials: 'include', - }); - - if (!response.ok) { - throw new Error('Failed to fetch organizations'); - } - - const data: { organizations: Organization[] } = await response.json(); - setOrganizations(data.organizations); - } catch (err) { - console.error('Failed to fetch organizations:', err); - } finally { - setIsLoadingOrganizations(false); - } - }; - - fetchOrganizations(); - }, []); - - // Fetch pending invitations from REST endpoint - useEffect(() => { - const fetchInvitations = async () => { - try { - const response = await fetch('/connect/invitations', { - credentials: 'include', - }); - - if (!response.ok) { - throw new Error('Failed to fetch invitations'); - } - - const data: { invitations: Invitation[] } = await response.json(); - setInvitations(data.invitations); - } catch (err) { - console.error('Failed to fetch invitations:', err); - } finally { - setIsLoadingInvitations(false); - } - }; - - fetchInvitations(); - }, []); - - const handleAcceptInvitation = async (invitationId: string, organizationId: string) => { - setIsAccepting(true); - try { - const response = await fetch('/connect/invitations/accept', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - credentials: 'include', - body: JSON.stringify({ invitationId }), - }); - - if (!response.ok) { - throw new Error('Failed to accept invitation'); - } - - // Navigate to the organization after successful acceptance - navigate(`/organizations/${organizationId}`); - } catch (err) { - console.error('Failed to accept invitation:', err); - alert(__('Failed to accept invitation')); - } finally { - setIsAccepting(false); - } - }; - - usePageTitle(__("Select an organization")); - - useEffect(() => { - // Only auto-navigate once both organizations and invitations are loaded - if (!isLoadingOrganizations && !isLoadingInvitations) { - if (organizations.length === 1 && invitations.length === 0) { - navigate(`/organizations/${organizations[0].id}`); - } else if (organizations.length === 0 && invitations.length === 0) { - navigate("/organizations/new"); - } - } - }, [organizations, invitations, isLoadingOrganizations, isLoadingInvitations, navigate]); - - return ( - <> -
-

- {__("Select an organization")} -

-
- {invitations.length > 0 && ( -
-

- {__("Pending invitations")} -

- {invitations.map((invitation) => ( - - ))} -
- )} - {organizations.length > 0 && ( -
- {invitations.length > 0 && ( -

- {__("Your organizations")} -

- )} - {organizations.length > 3 && ( -
- -
- )} - {filteredOrganizations.length === 0 ? ( -
- {__("No organizations found")} -
- ) : ( - filteredOrganizations.map((organization) => ( - - )) - )} -
- )} - -

- {__("Create an organization")} -

-

- {__("Add a new organization to your account")} -

- -
-
-
- - ); -} - -type InvitationCardProps = { - invitation: Invitation; - onAccept: (invitationId: string, organizationId: string) => void; - isAccepting: boolean; -}; - -function InvitationCard({ invitation, onAccept, isAccepting }: InvitationCardProps) { - const { __ } = useTranslate(); - - return ( - -
-
-

- {invitation.organization.name} -

-

- {__("Role")}: {invitation.role} -

-

- {__("Invited on")} {formatDate(invitation.createdAt)} -

-
- -
-
- ); -} - -type OrganizationCardProps = { - organization: Organization; -}; - -function OrganizationCard({ organization }: OrganizationCardProps) { - const { __ } = useTranslate(); - - const isAuthenticated = organization.authStatus === "authenticated"; - const isExpired = organization.authStatus === "expired"; - const needsAuth = organization.authStatus === "unauthenticated"; - - // Determine target URL and button text based on auth status - const targetUrl = isAuthenticated - ? `/organizations/${organization.id}` - : organization.loginUrl; - - const getAuthBadge = () => { - if (isAuthenticated) { - return ( - - - {__("Authenticated")} - - ); - } - - if (isExpired) { - return ( - - - {__("Session expired")} - - ); - } - - if (needsAuth) { - return ( - - - {__("Authentication required")} - - ); - } - - return null; - }; - - const getButtonText = () => { - if (isAuthenticated) return __("Select"); - if (organization.authenticationMethod === "saml") return __("Login with SAML"); - return __("Login"); - }; - - // Check if the URL is a backend SAML endpoint - const isSAMLUrl = targetUrl.includes('/connect/saml/'); - - return ( - -
- {isSAMLUrl ? ( - - -
-

{organization.name}

- {getAuthBadge()} -
-
- ) : ( - - -
-

{organization.name}

- {getAuthBadge()} -
- - )} -
- -
-
-
- ); -} diff --git a/apps/console/src/pages/iam/organizations/OrganizationLayout.tsx b/apps/console/src/pages/iam/organizations/OrganizationLayout.tsx new file mode 100644 index 000000000..847a5fbb6 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/OrganizationLayout.tsx @@ -0,0 +1,48 @@ +import { + RelayEnvironmentProvider, + usePreloadedQuery, + type PreloadedQuery, +} from "react-relay"; +import { organizationLayoutQuery } from "./OrganizationLayoutQuery"; +import { Outlet } from "react-router"; +import { Layout } from "@probo/ui"; +import { Sidebar } from "./_components/Sidebar"; +import { OrganizationDropdown } from "./_components/OrganizationDropdown"; +import { consoleEnvironment } from "/environments"; +import type { OrganizationLayoutQuery } from "./__generated__/OrganizationLayoutQuery.graphql"; +import { PermissionsProvider } from "/providers/PermissionsProvider"; + +interface OrganizationLayoutProps { + queryRef: PreloadedQuery; +} + +export default function OrganizationLayout(props: OrganizationLayoutProps) { + const { queryRef } = props; + + const data = usePreloadedQuery( + organizationLayoutQuery, + queryRef, + ); + + return ( + + +
+ +
+ {/* }> + + */} + + } + sidebar={} + > + + + +
+
+ ); +} diff --git a/apps/console/src/pages/iam/organizations/OrganizationLayoutQuery.tsx b/apps/console/src/pages/iam/organizations/OrganizationLayoutQuery.tsx new file mode 100644 index 000000000..55d6a302a --- /dev/null +++ b/apps/console/src/pages/iam/organizations/OrganizationLayoutQuery.tsx @@ -0,0 +1,47 @@ +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 { OrganizationLayoutQuery } from "./__generated__/OrganizationLayoutQuery.graphql"; + +const Layout = lazy(() => import("./OrganizationLayout")); + +export const organizationLayoutQuery = graphql` + query OrganizationLayoutQuery($organizationId: ID!) { + organization: node(id: $organizationId) @required(action: THROW) { + ... on Organization { + ...OrganizationDropdownFragment + } + } + viewer { + pendingInvitations { + totalCount + } + } + } +`; + +export function OrganizationLayoutQuery() { + const organizationId = useOrganizationId(); + const [queryRef, loadQuery] = useQueryLoader( + organizationLayoutQuery, + ); + + useEffect(() => { + loadQuery({ + organizationId, + }); + }, [loadQuery, organizationId]); + + if (!queryRef) { + return ; + } + + return ( + }> + + + ); +} diff --git a/apps/console/src/pages/iam/organizations/OrganizationsPage.tsx b/apps/console/src/pages/iam/organizations/OrganizationsPage.tsx index 7ef6b83e0..b9e1cf07e 100644 --- a/apps/console/src/pages/iam/organizations/OrganizationsPage.tsx +++ b/apps/console/src/pages/iam/organizations/OrganizationsPage.tsx @@ -9,15 +9,12 @@ import { } from "@probo/ui"; import { useMemo, useState } from "react"; import { InvitationCard } from "./_components/InvitationCard"; -import type { - OrganizationsQuery, - OrganizationsQuery$data, -} from "./__generated__/OrganizationsQuery.graphql"; -import { organizationsQuery } from "./OrganizationsQuery"; +import { organizationsPageQuery } from "./OrganizationsPageQuery"; import { MembershipCard } from "./_components/MembershipCard"; +import type { OrganizationsPageQuery } from "./__generated__/OrganizationsPageQuery.graphql"; interface PageProps { - queryRef: PreloadedQuery; + queryRef: PreloadedQuery; } export default function Page(props: PageProps) { @@ -30,11 +27,12 @@ export default function Page(props: PageProps) { memberships: { edges: initialMemberships }, pendingInvitations: { edges: invitations }, }, - } = usePreloadedQuery(organizationsQuery, queryRef); + } = usePreloadedQuery( + organizationsPageQuery, + queryRef, + ); - const memberships = useMemo< - OrganizationsQuery$data["viewer"]["memberships"]["edges"] - >(() => { + const memberships = useMemo(() => { if (!search.trim()) { return initialMemberships; } diff --git a/apps/console/src/pages/iam/organizations/OrganizationsQuery.tsx b/apps/console/src/pages/iam/organizations/OrganizationsPageQuery.tsx similarity index 56% rename from apps/console/src/pages/iam/organizations/OrganizationsQuery.tsx rename to apps/console/src/pages/iam/organizations/OrganizationsPageQuery.tsx index 68b3208cf..09d9578a1 100644 --- a/apps/console/src/pages/iam/organizations/OrganizationsQuery.tsx +++ b/apps/console/src/pages/iam/organizations/OrganizationsPageQuery.tsx @@ -1,24 +1,22 @@ import { lazy } from "@probo/react-lazy"; import { useQueryLoader } from "react-relay"; import { graphql } from "relay-runtime"; -import type { OrganizationsQuery } from "./__generated__/OrganizationsQuery.graphql"; import { Suspense, useEffect } from "react"; import { CenteredLayoutSkeleton } from "@probo/ui"; +import type { OrganizationsPageQuery } from "./__generated__/OrganizationsPageQuery.graphql"; const Page = lazy(() => import("./OrganizationsPage")); -export const organizationsQuery = graphql` - query OrganizationsQuery { +export const organizationsPageQuery = graphql` + query OrganizationsPageQuery { viewer @required(action: THROW) { - memberships( - first: 1000 - orderBy: { direction: DESC, field: CREATED_AT } - ) { - edges { - node { + memberships(first: 1000, orderBy: { direction: DESC, field: CREATED_AT }) + @required(action: THROW) { + edges @required(action: THROW) { + node @required(action: THROW) { id ...MembershipCardFragment - organization { + organization @required(action: THROW) { name } } @@ -27,9 +25,9 @@ export const organizationsQuery = graphql` pendingInvitations( first: 1000 orderBy: { direction: DESC, field: CREATED_AT } - ) { - edges { - node { + ) @required(action: THROW) { + edges @required(action: THROW) { + node @required(action: THROW) { id ...InvitationCardFragment } @@ -39,9 +37,10 @@ export const organizationsQuery = graphql` } `; -export function OrganizationsQuery() { - const [queryRef, loadQuery] = - useQueryLoader(organizationsQuery); +export function OrganizationsPageQuery() { + const [queryRef, loadQuery] = useQueryLoader( + organizationsPageQuery, + ); useEffect(() => { loadQuery({}); diff --git a/apps/console/src/pages/iam/organizations/__generated__/OrganizationLayoutQuery.graphql.ts b/apps/console/src/pages/iam/organizations/__generated__/OrganizationLayoutQuery.graphql.ts new file mode 100644 index 000000000..6f5492644 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/__generated__/OrganizationLayoutQuery.graphql.ts @@ -0,0 +1,188 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrganizationLayoutQuery$variables = { + organizationId: string; +}; +export type OrganizationLayoutQuery$data = { + readonly organization: { + readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdownFragment">; + }; + readonly viewer: { + readonly pendingInvitations: { + readonly totalCount: number | null | undefined; + } | null | undefined; + } | null | undefined; +}; +export type OrganizationLayoutQuery = { + response: OrganizationLayoutQuery$data; + variables: OrganizationLayoutQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "defaultValue": null, + "kind": "LocalArgument", + "name": "organizationId" + } +], +v1 = [ + { + "kind": "Variable", + "name": "id", + "variableName": "organizationId" + } +], +v2 = { + "alias": null, + "args": null, + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "pendingInvitations", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "totalCount", + "storageKey": null + } + ], + "storageKey": null +}, +v3 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Fragment", + "metadata": null, + "name": "OrganizationLayoutQuery", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "kind": "InlineFragment", + "selections": [ + { + "args": null, + "kind": "FragmentSpread", + "name": "OrganizationDropdownFragment" + } + ], + "type": "Organization", + "abstractKey": null + } + ], + "storageKey": null + }, + "action": "THROW" + }, + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": (v0/*: any*/), + "kind": "Operation", + "name": "OrganizationLayoutQuery", + "selections": [ + { + "alias": "organization", + "args": (v1/*: any*/), + "concreteType": null, + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "__typename", + "storageKey": null + }, + { + "kind": "InlineFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "Organization", + "abstractKey": null + }, + (v3/*: any*/) + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + (v2/*: any*/), + (v3/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "b991c1a406cc9c0df5c67099285c31a8", + "id": null, + "metadata": {}, + "name": "OrganizationLayoutQuery", + "operationKind": "query", + "text": "query OrganizationLayoutQuery(\n $organizationId: ID!\n) {\n organization: node(id: $organizationId) {\n __typename\n ... on Organization {\n ...OrganizationDropdownFragment\n }\n id\n }\n viewer {\n pendingInvitations {\n totalCount\n }\n id\n }\n}\n\nfragment OrganizationDropdownFragment on Organization {\n name\n}\n" + } +}; +})(); + +(node as any).hash = "2e8a1c50dc999d218830ab3df10cdeb5"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/__generated__/OrganizationsQuery.graphql.ts b/apps/console/src/pages/iam/organizations/__generated__/OrganizationsPageQuery.graphql.ts similarity index 58% rename from apps/console/src/pages/iam/organizations/__generated__/OrganizationsQuery.graphql.ts rename to apps/console/src/pages/iam/organizations/__generated__/OrganizationsPageQuery.graphql.ts index c20ec6b6a..54418d327 100644 --- a/apps/console/src/pages/iam/organizations/__generated__/OrganizationsQuery.graphql.ts +++ b/apps/console/src/pages/iam/organizations/__generated__/OrganizationsPageQuery.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<97119b2ea90f368155a13a32941b99d3>> + * @generated SignedSource<<6ac0b24d615666de1a5ea1c751095e6d>> * @lightSyntaxTransform * @nogrep */ @@ -10,8 +10,8 @@ import { ConcreteRequest } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; -export type OrganizationsQuery$variables = Record; -export type OrganizationsQuery$data = { +export type OrganizationsPageQuery$variables = Record; +export type OrganizationsPageQuery$data = { readonly viewer: { readonly memberships: { readonly edges: ReadonlyArray<{ @@ -34,9 +34,9 @@ export type OrganizationsQuery$data = { }; }; }; -export type OrganizationsQuery = { - response: OrganizationsQuery$data; - variables: OrganizationsQuery$variables; +export type OrganizationsPageQuery = { + response: OrganizationsPageQuery$data; + variables: OrganizationsPageQuery$variables; }; const node: ConcreteRequest = (function(){ @@ -74,7 +74,7 @@ return { "argumentDefinitions": [], "kind": "Fragment", "metadata": null, - "name": "OrganizationsQuery", + "name": "OrganizationsPageQuery", "selections": [ { "kind": "RequiredField", @@ -87,94 +87,122 @@ return { "plural": false, "selections": [ { - "alias": null, - "args": (v0/*: any*/), - "concreteType": "MembershipConnection", - "kind": "LinkedField", - "name": "memberships", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "MembershipEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { + "kind": "RequiredField", + "field": { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "memberships", + "plural": false, + "selections": [ + { + "kind": "RequiredField", + "field": { "alias": null, "args": null, - "concreteType": "Membership", + "concreteType": "MembershipEdge", "kind": "LinkedField", - "name": "node", - "plural": false, + "name": "edges", + "plural": true, "selections": [ - (v1/*: any*/), { - "args": null, - "kind": "FragmentSpread", - "name": "MembershipCardFragment" - }, - { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "organization", - "plural": false, - "selections": [ - (v2/*: any*/) - ], - "storageKey": null + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "MembershipCardFragment" + }, + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "storageKey": null + }, + "action": "THROW" } ], "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + "action": "THROW" + } + ], + "storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + "action": "THROW" }, { - "alias": null, - "args": (v0/*: any*/), - "concreteType": "InvitationConnection", - "kind": "LinkedField", - "name": "pendingInvitations", - "plural": false, - "selections": [ - { - "alias": null, - "args": null, - "concreteType": "InvitationEdge", - "kind": "LinkedField", - "name": "edges", - "plural": true, - "selections": [ - { + "kind": "RequiredField", + "field": { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "InvitationConnection", + "kind": "LinkedField", + "name": "pendingInvitations", + "plural": false, + "selections": [ + { + "kind": "RequiredField", + "field": { "alias": null, "args": null, - "concreteType": "Invitation", + "concreteType": "InvitationEdge", "kind": "LinkedField", - "name": "node", - "plural": false, + "name": "edges", + "plural": true, "selections": [ - (v1/*: any*/), { - "args": null, - "kind": "FragmentSpread", - "name": "InvitationCardFragment" + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Invitation", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "args": null, + "kind": "FragmentSpread", + "name": "InvitationCardFragment" + } + ], + "storageKey": null + }, + "action": "THROW" } ], "storageKey": null - } - ], - "storageKey": null - } - ], - "storageKey": "pendingInvitations(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + "action": "THROW" + } + ], + "storageKey": "pendingInvitations(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + "action": "THROW" } ], "storageKey": null @@ -189,7 +217,7 @@ return { "operation": { "argumentDefinitions": [], "kind": "Operation", - "name": "OrganizationsQuery", + "name": "OrganizationsPageQuery", "selections": [ { "alias": null, @@ -229,7 +257,7 @@ return { "args": null, "concreteType": "Session", "kind": "LinkedField", - "name": "activeSession", + "name": "lastSession", "plural": false, "selections": [ (v1/*: any*/), @@ -340,16 +368,16 @@ return { ] }, "params": { - "cacheID": "5dad37a0c652055f81de5bd2c09995c2", + "cacheID": "146deb87c5d32960212f323b4f5c792c", "id": null, "metadata": {}, - "name": "OrganizationsQuery", + "name": "OrganizationsPageQuery", "operationKind": "query", - "text": "query OrganizationsQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...MembershipCardFragment\n organization {\n name\n id\n }\n }\n }\n }\n pendingInvitations(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationCardFragment\n }\n }\n }\n id\n }\n}\n\nfragment InvitationCardFragment on Invitation {\n id\n role\n createdAt\n organization {\n id\n name\n }\n}\n\nfragment MembershipCardFragment on Membership {\n activeSession {\n id\n expiresAt\n }\n organization {\n id\n name\n logoUrl\n }\n}\n" + "text": "query OrganizationsPageQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...MembershipCardFragment\n organization {\n name\n id\n }\n }\n }\n }\n pendingInvitations(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n ...InvitationCardFragment\n }\n }\n }\n id\n }\n}\n\nfragment InvitationCardFragment on Invitation {\n id\n role\n createdAt\n organization {\n id\n name\n }\n}\n\nfragment MembershipCardFragment on Membership {\n lastSession {\n id\n expiresAt\n }\n organization {\n id\n name\n logoUrl\n }\n}\n" } }; })(); -(node as any).hash = "dc4b2e28600292c73ea31b5cb450dba7"; +(node as any).hash = "7c03287e592920408306cac9bfa7fc81"; export default node; diff --git a/apps/console/src/pages/iam/organizations/__generated__/sessionIsExpiredFragment.graphql.ts b/apps/console/src/pages/iam/organizations/__generated__/sessionIsExpiredFragment.graphql.ts deleted file mode 100644 index db071e59f..000000000 --- a/apps/console/src/pages/iam/organizations/__generated__/sessionIsExpiredFragment.graphql.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * @generated SignedSource<> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ReaderFragment } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; -export type sessionIsExpiredFragment$data = { - readonly expiresAt: any; - readonly " $fragmentType": "sessionIsExpiredFragment"; -}; -export type sessionIsExpiredFragment$key = { - readonly " $data"?: sessionIsExpiredFragment$data; - readonly " $fragmentSpreads": FragmentRefs<"sessionIsExpiredFragment">; -}; - -const node: ReaderFragment = { - "argumentDefinitions": [], - "kind": "Fragment", - "metadata": null, - "name": "sessionIsExpiredFragment", - "selections": [ - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "expiresAt", - "storageKey": null - } - ], - "type": "Session", - "abstractKey": null -}; - -(node as any).hash = "ef7bc8bbfa6d2573b0df09b5b13b00b2"; - -export default node; diff --git a/apps/console/src/pages/iam/organizations/_components/InvitationCard.tsx b/apps/console/src/pages/iam/organizations/_components/InvitationCard.tsx index b4c0440a9..627616931 100644 --- a/apps/console/src/pages/iam/organizations/_components/InvitationCard.tsx +++ b/apps/console/src/pages/iam/organizations/_components/InvitationCard.tsx @@ -10,7 +10,7 @@ const fragment = graphql` id role createdAt - organization { + organization @required(action: THROW) { id name } diff --git a/apps/console/src/pages/iam/organizations/_components/MembershipCard.tsx b/apps/console/src/pages/iam/organizations/_components/MembershipCard.tsx index afcdca5a6..1bae9a417 100644 --- a/apps/console/src/pages/iam/organizations/_components/MembershipCard.tsx +++ b/apps/console/src/pages/iam/organizations/_components/MembershipCard.tsx @@ -16,11 +16,11 @@ import { parseDate } from "@probo/helpers"; const fragment = graphql` fragment MembershipCardFragment on Membership { - activeSession { + lastSession { id expiresAt } - organization { + organization @required(action: THROW) { id name logoUrl @@ -36,11 +36,13 @@ export function MembershipCard(props: MembershipCardProps) { const { fKey } = props; const { __ } = useTranslate(); - const { activeSession, organization } = - useFragment(fragment, fKey); - const isAuthenticated = !!activeSession; + const { lastSession, organization } = useFragment( + fragment, + fKey, + ); + const isAuthenticated = !!lastSession; const isExpired = - activeSession && parseDate(activeSession.expiresAt) >= new Date(); + lastSession && parseDate(lastSession.expiresAt) >= new Date(); // Determine target URL and button text based on auth status // const targetUrl = isAuthenticated diff --git a/apps/console/src/pages/iam/organizations/_components/OrganizationCard.tsx b/apps/console/src/pages/iam/organizations/_components/OrganizationCard.tsx deleted file mode 100644 index 347b03a41..000000000 --- a/apps/console/src/pages/iam/organizations/_components/OrganizationCard.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { useTranslate } from "@probo/i18n"; -import { Avatar, Badge, Button, Card, IconLock } from "@probo/ui"; -import { Link } from "react-router"; -import { graphql } from "relay-runtime"; -import { useFragment } from "react-relay"; -import type { OrganizationCardFragment$key } from "./__generated__/OrganizationCardFragment.graphql"; - -const fragment = graphql` - fragment OrganizationCardFragment on Organization { - id - name - logoUrl - } -`; - -interface OrganizationCardProps { - fKey: OrganizationCardFragment$key; -} - -export function OrganizationCard(props: OrganizationCardProps) { - const { fKey } = props; - const { __ } = useTranslate(); - - const organization = useFragment( - fragment, - fKey, - ); - // const isAuthenticated = organization.authStatus === "authenticated"; - // const isExpired = organization.authStatus === "expired"; - // const needsAuth = organization.authStatus === "unauthenticated"; - - // Determine target URL and button text based on auth status - // const targetUrl = isAuthenticated - // ? `/organizations/${organization.id}` - // : organization.loginUrl; - const targetUrl = `/organizations/${organization.id}`; - - const getAuthBadge = () => { - // if (isAuthenticated) { - // return ( - // - // - // {__("Authenticated")} - // - // ); - // } - - // if (isExpired) { - // return ( - // - // - // {__("Session expired")} - // - // ); - // } - - // if (needsAuth) { - return ( - - - {__("Authentication required")} - - ); - // } - - return null; - }; - - // const getButtonText = () => { - // if (isAuthenticated) return __("Select"); - // if (organization.authenticationMethod === "saml") - // return __("Login with SAML"); - // return __("Login"); - // }; - - // Check if the URL is a backend SAML endpoint - const isSAMLUrl = targetUrl.includes("/connect/saml/"); - - return ( - -
- {isSAMLUrl ? ( - - -
-

{organization.name}

- {getAuthBadge()} -
-
- ) : ( - - -
-

{organization.name}

- {getAuthBadge()} -
- - )} -
- -
-
-
- ); -} diff --git a/apps/console/src/pages/iam/organizations/_components/OrganizationDropdown.tsx b/apps/console/src/pages/iam/organizations/_components/OrganizationDropdown.tsx new file mode 100644 index 000000000..141d49930 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/OrganizationDropdown.tsx @@ -0,0 +1,89 @@ +import { useFragment, useQueryLoader } from "react-relay"; +import { graphql } from "relay-runtime"; +import type { OrganizationDropdownFragment$key } from "./__generated__/OrganizationDropdownFragment.graphql"; +import { + Button, + Dropdown, + DropdownSeparator, + IconChevronGrabberVertical, + IconMagnifyingGlass, + Input, +} from "@probo/ui"; +import { Suspense, useCallback, useState } from "react"; +import { useTranslate } from "@probo/i18n"; +import { + OrganizationDropdownMenu, + organizationDropdownMenuQuery, +} from "./OrganizationDropdownMenu"; +import type { OrganizationDropdownMenuQuery } from "./__generated__/OrganizationDropdownMenuQuery.graphql"; + +const fragment = graphql` + fragment OrganizationDropdownFragment on Organization { + name + } +`; + +export function OrganizationDropdown(props: { + fKey: OrganizationDropdownFragment$key; +}) { + const { fKey } = props; + + const { __ } = useTranslate(); + const [search, setSearch] = useState(""); + + const currentOrganization = useFragment(fragment, fKey); + const [queryRef, loadQuery] = useQueryLoader( + organizationDropdownMenuQuery, + ); + + const handleOpenMenu = useCallback( + (open: boolean) => { + if (open) loadQuery({}); + }, + [loadQuery], + ); + + return ( +
+ + {currentOrganization.name} + + } + > +
+ { + e.stopPropagation(); + }} + autoFocus + /> +
+
+ {queryRef && ( + + {__("Loading organizations...")} +
+ } + > + + + )} +
+ + + + ); +} diff --git a/apps/console/src/pages/iam/organizations/_components/OrganizationDropdownMenu.tsx b/apps/console/src/pages/iam/organizations/_components/OrganizationDropdownMenu.tsx new file mode 100644 index 000000000..fb6aaf71a --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/OrganizationDropdownMenu.tsx @@ -0,0 +1,59 @@ +import { graphql, usePreloadedQuery, type PreloadedQuery } from "react-relay"; +import type { OrganizationDropdownMenuQuery } from "./__generated__/OrganizationDropdownMenuQuery.graphql"; +import { useMemo } from "react"; +import { OrganizationDropdownMenuItem } from "./OrganizationDropdownMenuItem"; + +export const organizationDropdownMenuQuery = graphql` + query OrganizationDropdownMenuQuery { + 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 + organization @required(action: THROW) { + name + } + ...OrganizationDropdownMenuItemFragment + } + } + } + } + } +`; + +interface OrganizationDropdownMenuProps { + queryRef: PreloadedQuery; + search: string; +} + +export function OrganizationDropdownMenu(props: OrganizationDropdownMenuProps) { + const { queryRef, search } = props; + + const { + viewer: { + memberships: { edges: initialMemberships }, + }, + } = usePreloadedQuery( + organizationDropdownMenuQuery, + queryRef, + ); + + const memberships = useMemo(() => { + if (!search) { + return initialMemberships; + } + + return initialMemberships.filter(({ node: { organization } }) => + organization.name.toLowerCase().includes(search.toLowerCase()), + ); + }, [initialMemberships, search]); + + return ( + <> + {memberships.map(({ node }) => ( + + ))} + + ); +} diff --git a/apps/console/src/pages/iam/organizations/_components/OrganizationDropdownMenuItem.tsx b/apps/console/src/pages/iam/organizations/_components/OrganizationDropdownMenuItem.tsx new file mode 100644 index 000000000..a3ca3ed27 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/OrganizationDropdownMenuItem.tsx @@ -0,0 +1,50 @@ +import { + Avatar, + DropdownItem, + IconCheckmark1, + IconClock, + IconLock, +} from "@probo/ui"; +import { graphql } from "relay-runtime"; +import type { OrganizationDropdownMenuItemFragment$key } from "./__generated__/OrganizationDropdownMenuItemFragment.graphql"; +import { useFragment } from "react-relay"; +import { parseDate } from "@probo/helpers"; + +const fragment = graphql` + fragment OrganizationDropdownMenuItemFragment on Membership { + id + lastSession { + id + expiresAt + } + organization @required(action: THROW) { + logoUrl + name + } + } +`; + +export function OrganizationDropdownMenuItem(props: { + fKey: OrganizationDropdownMenuItemFragment$key; +}) { + const { fKey } = props; + const { id, lastSession, organization } = + useFragment(fragment, fKey); + + const isAuthenticated = !!lastSession; + const isExpired = + lastSession && parseDate(lastSession.expiresAt) >= new Date(); + + return ( + + {/* TODO add link or anchor */} + + {organization.name} + {isAuthenticated && ( + + )} + {isExpired && } + {!lastSession && } + + ); +} diff --git a/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx new file mode 100644 index 000000000..cf232e688 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/Sidebar.tsx @@ -0,0 +1,166 @@ +import { use } from "react"; +import { PermissionsContext } from "/providers/PermissionsContext"; +import { + IconBank, + IconBook, + IconBox, + IconCalendar1, + IconCircleProgress, + IconClock, + IconCrossLargeX, + IconFire3, + IconGroup1, + IconInboxEmpty, + IconListStack, + IconMedal, + IconPageTextLine, + IconRotateCw, + IconSettingsGear2, + IconShield, + IconStore, + IconTodo, + SidebarItem, +} from "@probo/ui"; +import { useTranslate } from "@probo/i18n"; +import { useOrganizationId } from "/hooks/useOrganizationId"; + +export function Sidebar() { + const { __ } = useTranslate(); + const organizationId = useOrganizationId(); + + const { isAuthorized } = use(PermissionsContext); + + const prefix = `/organizations/${organizationId}`; + + console.log(isAuthorized("Organization", "listMeetings")); + return ( +
    + {isAuthorized("Organization", "listMeetings") && ( + + )} + {isAuthorized("Organization", "listTasks") && ( + + )} + {isAuthorized("Organization", "listMeasures") && ( + + )} + {isAuthorized("Organization", "listRisks") && ( + + )} + {isAuthorized("Organization", "listFrameworks") && ( + + )} + {isAuthorized("Organization", "listPeople") && ( + + )} + {isAuthorized("Organization", "listVendors") && ( + + )} + {isAuthorized("Organization", "listDocuments") && ( + + )} + {isAuthorized("Organization", "listAssets") && ( + + )} + {isAuthorized("Organization", "listData") && ( + + )} + {isAuthorized("Organization", "listAudits") && ( + + )} + {isAuthorized("Organization", "listNonconformities") && ( + + )} + {isAuthorized("Organization", "listObligations") && ( + + )} + {isAuthorized("Organization", "listContinualImprovements") && ( + + )} + {isAuthorized("Organization", "listProcessingActivities") && ( + + )} + {isAuthorized("Organization", "listSnapshots") && ( + + )} + {isAuthorized("Organization", "getTrustCenter") && ( + + )} + {isAuthorized("Organization", "listMembers") && ( + + )} +
+ ); +} diff --git a/apps/console/src/pages/iam/organizations/_components/__generated__/InvitationCardFragment.graphql.ts b/apps/console/src/pages/iam/organizations/_components/__generated__/InvitationCardFragment.graphql.ts index c583b27cd..d6a1d2b60 100644 --- a/apps/console/src/pages/iam/organizations/_components/__generated__/InvitationCardFragment.graphql.ts +++ b/apps/console/src/pages/iam/organizations/_components/__generated__/InvitationCardFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<5193da6ee4b5da0bed510c255c3407b1>> + * @generated SignedSource<> * @lightSyntaxTransform * @nogrep */ @@ -56,23 +56,27 @@ return { "storageKey": null }, { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "organization", - "plural": false, - "selections": [ - (v0/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - } - ], - "storageKey": null + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + }, + "action": "THROW" } ], "type": "Invitation", @@ -80,6 +84,6 @@ return { }; })(); -(node as any).hash = "1f61b5f07abc69ad33c880bc39cd0e96"; +(node as any).hash = "57aedeabc62e474c6e5b995af1d31888"; export default node; diff --git a/apps/console/src/pages/iam/organizations/_components/__generated__/MembershipCardFragment.graphql.ts b/apps/console/src/pages/iam/organizations/_components/__generated__/MembershipCardFragment.graphql.ts index c3f9e3df2..8c535c4a3 100644 --- a/apps/console/src/pages/iam/organizations/_components/__generated__/MembershipCardFragment.graphql.ts +++ b/apps/console/src/pages/iam/organizations/_components/__generated__/MembershipCardFragment.graphql.ts @@ -1,5 +1,5 @@ /** - * @generated SignedSource<<30ba0cd2d4738694d58bf346410d4d68>> + * @generated SignedSource<<4660f07fbf06d0125ce01cd54019f389>> * @lightSyntaxTransform * @nogrep */ @@ -11,7 +11,7 @@ import { ReaderFragment } from 'relay-runtime'; import { FragmentRefs } from "relay-runtime"; export type MembershipCardFragment$data = { - readonly activeSession: { + readonly lastSession: { readonly expiresAt: any; readonly id: string; } | null | undefined; @@ -46,7 +46,7 @@ return { "args": null, "concreteType": "Session", "kind": "LinkedField", - "name": "activeSession", + "name": "lastSession", "plural": false, "selections": [ (v0/*: any*/), @@ -61,30 +61,34 @@ return { "storageKey": null }, { - "alias": null, - "args": null, - "concreteType": "Organization", - "kind": "LinkedField", - "name": "organization", - "plural": false, - "selections": [ - (v0/*: any*/), - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "name", - "storageKey": null - }, - { - "alias": null, - "args": null, - "kind": "ScalarField", - "name": "logoUrl", - "storageKey": null - } - ], - "storageKey": null + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null + } + ], + "storageKey": null + }, + "action": "THROW" } ], "type": "Membership", @@ -92,6 +96,6 @@ return { }; })(); -(node as any).hash = "2145f6030b486447df7b7d9ff798ec06"; +(node as any).hash = "5a2532aed755a94df49c2f9ce0929a1f"; export default node; diff --git a/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationCardFragment.graphql.ts b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationCardFragment.graphql.ts deleted file mode 100644 index 270f86f2b..000000000 --- a/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationCardFragment.graphql.ts +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @generated SignedSource<<06aa55abfa21c09c85bb308f813565d8>> - * @lightSyntaxTransform - * @nogrep - */ - -/* tslint:disable */ -/* eslint-disable */ -// @ts-nocheck - -import { ReaderFragment } from 'relay-runtime'; -import { FragmentRefs } from "relay-runtime"; -export type OrganizationCardFragment$data = { - readonly id: string; - readonly logoUrl: string | null | undefined; - readonly name: string; - readonly " $fragmentType": "OrganizationCardFragment"; -}; -export type OrganizationCardFragment$key = { - readonly " $data"?: OrganizationCardFragment$data; - readonly " $fragmentSpreads": FragmentRefs<"OrganizationCardFragment">; -}; - -const node: ReaderFragment = { - "argumentDefinitions": [], - "kind": "Fragment", - "metadata": null, - "name": "OrganizationCardFragment", - "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 - } - ], - "type": "Organization", - "abstractKey": null -}; - -(node as any).hash = "a8b9f4a515650db79f41507fb52f7b96"; - -export default node; diff --git a/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownFragment.graphql.ts b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownFragment.graphql.ts new file mode 100644 index 000000000..cc9adbee3 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownFragment.graphql.ts @@ -0,0 +1,42 @@ +/** + * @generated SignedSource<<06f02e43b64c316083067f5b3978a08a>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrganizationDropdownFragment$data = { + readonly name: string; + readonly " $fragmentType": "OrganizationDropdownFragment"; +}; +export type OrganizationDropdownFragment$key = { + readonly " $data"?: OrganizationDropdownFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdownFragment">; +}; + +const node: ReaderFragment = { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "OrganizationDropdownFragment", + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "type": "Organization", + "abstractKey": null +}; + +(node as any).hash = "9f19581f86736e6345912284668a8f25"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownMenuItemFragment.graphql.ts b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownMenuItemFragment.graphql.ts new file mode 100644 index 000000000..98855f313 --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownMenuItemFragment.graphql.ts @@ -0,0 +1,101 @@ +/** + * @generated SignedSource<> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ReaderFragment } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrganizationDropdownMenuItemFragment$data = { + readonly id: string; + readonly lastSession: { + readonly expiresAt: any; + readonly id: string; + } | null | undefined; + readonly organization: { + readonly logoUrl: string | null | undefined; + readonly name: string; + }; + readonly " $fragmentType": "OrganizationDropdownMenuItemFragment"; +}; +export type OrganizationDropdownMenuItemFragment$key = { + readonly " $data"?: OrganizationDropdownMenuItemFragment$data; + readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdownMenuItemFragment">; +}; + +const node: ReaderFragment = (function(){ +var v0 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}; +return { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "OrganizationDropdownMenuItemFragment", + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Session", + "kind": "LinkedField", + "name": "lastSession", + "plural": false, + "selections": [ + (v0/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + } + ], + "storageKey": null + }, + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null + }, + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "Membership", + "abstractKey": null +}; +})(); + +(node as any).hash = "6be7f721fd63d6d758b479e58c68b754"; + +export default node; diff --git a/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownMenuQuery.graphql.ts b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownMenuQuery.graphql.ts new file mode 100644 index 000000000..cf4aabd9a --- /dev/null +++ b/apps/console/src/pages/iam/organizations/_components/__generated__/OrganizationDropdownMenuQuery.graphql.ts @@ -0,0 +1,263 @@ +/** + * @generated SignedSource<<2bde2fba4e4897436af08d9dd700b265>> + * @lightSyntaxTransform + * @nogrep + */ + +/* tslint:disable */ +/* eslint-disable */ +// @ts-nocheck + +import { ConcreteRequest } from 'relay-runtime'; +import { FragmentRefs } from "relay-runtime"; +export type OrganizationDropdownMenuQuery$variables = Record; +export type OrganizationDropdownMenuQuery$data = { + readonly viewer: { + readonly memberships: { + readonly edges: ReadonlyArray<{ + readonly node: { + readonly id: string; + readonly organization: { + readonly name: string; + }; + readonly " $fragmentSpreads": FragmentRefs<"OrganizationDropdownMenuItemFragment">; + }; + }>; + }; + }; +}; +export type OrganizationDropdownMenuQuery = { + response: OrganizationDropdownMenuQuery$data; + variables: OrganizationDropdownMenuQuery$variables; +}; + +const node: ConcreteRequest = (function(){ +var v0 = [ + { + "kind": "Literal", + "name": "first", + "value": 1000 + }, + { + "kind": "Literal", + "name": "orderBy", + "value": { + "direction": "DESC", + "field": "CREATED_AT" + } + } +], +v1 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "id", + "storageKey": null +}, +v2 = { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "name", + "storageKey": null +}; +return { + "fragment": { + "argumentDefinitions": [], + "kind": "Fragment", + "metadata": null, + "name": "OrganizationDropdownMenuQuery", + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "memberships", + "plural": false, + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "MembershipEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "kind": "RequiredField", + "field": { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v2/*: any*/) + ], + "storageKey": null + }, + "action": "THROW" + }, + { + "args": null, + "kind": "FragmentSpread", + "name": "OrganizationDropdownMenuItemFragment" + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + "action": "THROW" + } + ], + "storageKey": null + }, + "action": "THROW" + } + ], + "type": "Query", + "abstractKey": null + }, + "kind": "Request", + "operation": { + "argumentDefinitions": [], + "kind": "Operation", + "name": "OrganizationDropdownMenuQuery", + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Identity", + "kind": "LinkedField", + "name": "viewer", + "plural": false, + "selections": [ + { + "alias": null, + "args": (v0/*: any*/), + "concreteType": "MembershipConnection", + "kind": "LinkedField", + "name": "memberships", + "plural": false, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "MembershipEdge", + "kind": "LinkedField", + "name": "edges", + "plural": true, + "selections": [ + { + "alias": null, + "args": null, + "concreteType": "Membership", + "kind": "LinkedField", + "name": "node", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "concreteType": "Organization", + "kind": "LinkedField", + "name": "organization", + "plural": false, + "selections": [ + (v2/*: any*/), + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "logoUrl", + "storageKey": null + } + ], + "storageKey": null + }, + { + "alias": null, + "args": null, + "concreteType": "Session", + "kind": "LinkedField", + "name": "lastSession", + "plural": false, + "selections": [ + (v1/*: any*/), + { + "alias": null, + "args": null, + "kind": "ScalarField", + "name": "expiresAt", + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": null + } + ], + "storageKey": "memberships(first:1000,orderBy:{\"direction\":\"DESC\",\"field\":\"CREATED_AT\"})" + }, + (v1/*: any*/) + ], + "storageKey": null + } + ] + }, + "params": { + "cacheID": "8338998501d59b0036d1eaf98dff1ffb", + "id": null, + "metadata": {}, + "name": "OrganizationDropdownMenuQuery", + "operationKind": "query", + "text": "query OrganizationDropdownMenuQuery {\n viewer {\n memberships(first: 1000, orderBy: {direction: DESC, field: CREATED_AT}) {\n edges {\n node {\n id\n organization {\n name\n id\n }\n ...OrganizationDropdownMenuItemFragment\n }\n }\n }\n id\n }\n}\n\nfragment OrganizationDropdownMenuItemFragment on Membership {\n id\n lastSession {\n id\n expiresAt\n }\n organization {\n logoUrl\n name\n id\n }\n}\n" + } +}; +})(); + +(node as any).hash = "27c82735b5dcef01422d87312c127519"; + +export default node; diff --git a/apps/console/src/providers/PermissionsProvider.tsx b/apps/console/src/providers/PermissionsProvider.tsx index 957d19ed9..0cfb3f18b 100644 --- a/apps/console/src/providers/PermissionsProvider.tsx +++ b/apps/console/src/providers/PermissionsProvider.tsx @@ -1,31 +1,41 @@ -import { useSuspenseQuery } from "@tanstack/react-query"; import { type PropsWithChildren } from "react"; -import { useOrganizationId } from "/hooks/useOrganizationId"; -import { PermissionsContext, type PermissionsResponse } from "./PermissionsContext"; +import { PermissionsContext } from "./PermissionsContext"; +import { Role } from "@probo/helpers"; export function PermissionsProvider(props: PropsWithChildren) { const { children } = props; - const organizationId = useOrganizationId(); + // const organizationId = useOrganizationId(); - const { data } = useSuspenseQuery({ - queryKey: ["permissions", organizationId], - queryFn: async () => { - const response = await fetch(`/authz/${organizationId}/permissions`, { credentials: "include" }); - if (!response.ok) { - throw new Error("Failed to fetch permissions"); - } - return response.json() as Promise; - }, - }); + // const { data } = useSuspenseQuery({ + // queryKey: ["permissions", organizationId], + // queryFn: async () => { + // const response = await fetch(`/authz/${organizationId}/permissions`, { credentials: "include" }); + // if (!response.ok) { + // throw new Error("Failed to fetch permissions"); + // } + // return response.json() as Promise; + // }, + // }); + // @ts-expect-error wip refactor + // eslint-disable-next-line @typescript-eslint/no-unused-vars const isAuthorized = (entity: string, action: string) => { - return data.permissions[entity]?.[action] ?? false; - } + // return data.permissions[entity]?.[action] ?? false; + return true; + }; return ( - + {children} ); + + // return ( + // + // {children} + // + // ); } diff --git a/apps/console/src/routes.tsx b/apps/console/src/routes.tsx index ba70522ec..c52244f1f 100644 --- a/apps/console/src/routes.tsx +++ b/apps/console/src/routes.tsx @@ -4,7 +4,6 @@ import { redirect, useRouteError, } from "react-router"; -import { MainLayout } from "./layouts/MainLayout"; import { EmployeeLayout } from "./layouts/EmployeeLayout"; import { AuthLayout, CenteredLayout, CenteredLayoutSkeleton } from "@probo/ui"; import { PageSkeleton } from "./components/skeletons/PageSkeleton.tsx"; @@ -49,7 +48,8 @@ import { UnAuthenticatedError, UnauthorizedError, } from "@probo/relay"; -import { OrganizationsQuery } from "./pages/iam/organizations/OrganizationsQuery.tsx"; +import { OrganizationsPageQuery } from "./pages/iam/organizations/OrganizationsPageQuery.tsx"; +import { OrganizationLayoutQuery } from "./pages/iam/organizations/OrganizationLayoutQuery.tsx"; /** * Top level error boundary @@ -121,7 +121,7 @@ const routes = [ index: true, Component: () => ( - + ), }, @@ -184,8 +184,8 @@ const routes = [ { path: "/organizations/:organizationId", Component: () => ( - - + + ), ErrorBoundary: ErrorBoundary, diff --git a/packages/ui/src/Atoms/Dropdown/Dropdown.tsx b/packages/ui/src/Atoms/Dropdown/Dropdown.tsx index 4ac450589..4153f1df2 100644 --- a/packages/ui/src/Atoms/Dropdown/Dropdown.tsx +++ b/packages/ui/src/Atoms/Dropdown/Dropdown.tsx @@ -10,15 +10,16 @@ type Props = PropsWithChildren<{ toggle?: ReactNode; className?: string; open?: boolean; + onOpenChange?: (open: boolean) => void; }>; export const dropdown = tv({ base: "z-50 p-2 shadow-mid min-w-[8rem] bg-level-1 overflow-y-auto overflow-x-hidden rounded-2xl border-border-low", }); -export function Dropdown({ children, toggle, className, open }: Props) { +export function Dropdown({ children, toggle, className, open, onOpenChange }: Props) { return ( - + {toggle && ( {toggle} )}