Replug organizations page without assume or user dropdown
Signed-off-by: Émile Ré <nemile.re@gmail.com>
This commit is contained in:
@@ -1,161 +0,0 @@
|
||||
/**
|
||||
* @generated SignedSource<<f441167b6ed9402b92fcdf6dbaa77170>>
|
||||
* @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;
|
||||
@@ -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<Organization[]>([]);
|
||||
const [isLoadingOrganizations, setIsLoadingOrganizations] = useState(true);
|
||||
const [invitations, setInvitations] = useState<Invitation[]>([]);
|
||||
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 (
|
||||
<>
|
||||
<div className="space-y-6 w-full py-6">
|
||||
<h1 className="text-3xl font-bold text-center">
|
||||
{__("Select an organization")}
|
||||
</h1>
|
||||
<div className="space-y-4 w-full">
|
||||
{invitations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-xl font-semibold">
|
||||
{__("Pending invitations")}
|
||||
</h2>
|
||||
{invitations.map((invitation) => (
|
||||
<InvitationCard
|
||||
key={invitation.id}
|
||||
invitation={invitation}
|
||||
onAccept={handleAcceptInvitation}
|
||||
isAccepting={isAccepting}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{organizations.length > 0 && (
|
||||
<div className="space-y-3">
|
||||
{invitations.length > 0 && (
|
||||
<h2 className="text-xl font-semibold">
|
||||
{__("Your organizations")}
|
||||
</h2>
|
||||
)}
|
||||
{organizations.length > 3 && (
|
||||
<div className="w-full">
|
||||
<Input
|
||||
icon={IconMagnifyingGlass}
|
||||
placeholder={__("Search organizations...")}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{filteredOrganizations.length === 0 ? (
|
||||
<div className="text-center text-txt-secondary py-4">
|
||||
{__("No organizations found")}
|
||||
</div>
|
||||
) : (
|
||||
filteredOrganizations.map((organization) => (
|
||||
<OrganizationCard
|
||||
key={organization.id}
|
||||
organization={organization}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<Card padded>
|
||||
<h2 className="text-xl font-semibold mb-1">
|
||||
{__("Create an organization")}
|
||||
</h2>
|
||||
<p className="text-txt-tertiary mb-4">
|
||||
{__("Add a new organization to your account")}
|
||||
</p>
|
||||
<Button
|
||||
to="/organizations/new"
|
||||
variant="quaternary"
|
||||
icon={IconPlusLarge}
|
||||
className="w-full"
|
||||
>
|
||||
{__("Create organization")}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
type InvitationCardProps = {
|
||||
invitation: Invitation;
|
||||
onAccept: (invitationId: string, organizationId: string) => void;
|
||||
isAccepting: boolean;
|
||||
};
|
||||
|
||||
function InvitationCard({ invitation, onAccept, isAccepting }: InvitationCardProps) {
|
||||
const { __ } = useTranslate();
|
||||
|
||||
return (
|
||||
<Card padded className="w-full">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1 space-y-1">
|
||||
<h3 className="text-lg font-semibold">
|
||||
{invitation.organization.name}
|
||||
</h3>
|
||||
<p className="text-sm text-txt-secondary">
|
||||
{__("Role")}: <span className="font-medium">{invitation.role}</span>
|
||||
</p>
|
||||
<p className="text-xs text-txt-tertiary">
|
||||
{__("Invited on")} {formatDate(invitation.createdAt)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onAccept(invitation.id, invitation.organization.id)}
|
||||
disabled={isAccepting}
|
||||
>
|
||||
{isAccepting ? __("Accepting...") : __("Accept invitation")}
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Badge variant="success" className="flex items-center gap-1">
|
||||
<IconCheckmark1 size={14} />
|
||||
{__("Authenticated")}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (isExpired) {
|
||||
return (
|
||||
<Badge variant="warning" className="flex items-center gap-1">
|
||||
<IconClock size={14} />
|
||||
{__("Session expired")}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (needsAuth) {
|
||||
return (
|
||||
<Badge variant="neutral" className="flex items-center gap-1">
|
||||
<IconLock size={14} />
|
||||
{__("Authentication required")}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<Card padded className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
{isSAMLUrl ? (
|
||||
<a
|
||||
href={targetUrl}
|
||||
className="flex items-center gap-4 hover:text-primary flex-1"
|
||||
>
|
||||
<Avatar
|
||||
src={organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="l"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
||||
{getAuthBadge()}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
to={targetUrl}
|
||||
className="flex items-center gap-4 hover:text-primary flex-1"
|
||||
>
|
||||
<Avatar
|
||||
src={organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="l"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
||||
{getAuthBadge()}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button asChild>
|
||||
{isSAMLUrl ? (
|
||||
<a href={targetUrl}>
|
||||
{getButtonText()}
|
||||
</a>
|
||||
) : (
|
||||
<Link to={targetUrl}>
|
||||
{getButtonText()}
|
||||
</Link>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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<OrganizationLayoutQuery>;
|
||||
}
|
||||
|
||||
export default function OrganizationLayout(props: OrganizationLayoutProps) {
|
||||
const { queryRef } = props;
|
||||
|
||||
const data = usePreloadedQuery<OrganizationLayoutQuery>(
|
||||
organizationLayoutQuery,
|
||||
queryRef,
|
||||
);
|
||||
|
||||
return (
|
||||
<PermissionsProvider>
|
||||
<Layout
|
||||
header={
|
||||
<>
|
||||
<div className="mr-auto">
|
||||
<OrganizationDropdown fKey={data.organization} />
|
||||
</div>
|
||||
{/* <Suspense fallback={<Skeleton className="w-32 h-8" />}>
|
||||
<UserDropdown organizationId={organizationId} />
|
||||
</Suspense> */}
|
||||
</>
|
||||
}
|
||||
sidebar={<Sidebar />}
|
||||
>
|
||||
<RelayEnvironmentProvider environment={consoleEnvironment}>
|
||||
<Outlet />
|
||||
</RelayEnvironmentProvider>
|
||||
</Layout>
|
||||
</PermissionsProvider>
|
||||
);
|
||||
}
|
||||
@@ -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>(
|
||||
organizationLayoutQuery,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({
|
||||
organizationId,
|
||||
});
|
||||
}, [loadQuery, organizationId]);
|
||||
|
||||
if (!queryRef) {
|
||||
return <Skeleton className="w-full h-screen" />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<Skeleton className="w-full h-screen" />}>
|
||||
<Layout queryRef={queryRef} />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -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<OrganizationsQuery>;
|
||||
queryRef: PreloadedQuery<OrganizationsPageQuery>;
|
||||
}
|
||||
|
||||
export default function Page(props: PageProps) {
|
||||
@@ -30,11 +27,12 @@ export default function Page(props: PageProps) {
|
||||
memberships: { edges: initialMemberships },
|
||||
pendingInvitations: { edges: invitations },
|
||||
},
|
||||
} = usePreloadedQuery<OrganizationsQuery>(organizationsQuery, queryRef);
|
||||
} = usePreloadedQuery<OrganizationsPageQuery>(
|
||||
organizationsPageQuery,
|
||||
queryRef,
|
||||
);
|
||||
|
||||
const memberships = useMemo<
|
||||
OrganizationsQuery$data["viewer"]["memberships"]["edges"]
|
||||
>(() => {
|
||||
const memberships = useMemo(() => {
|
||||
if (!search.trim()) {
|
||||
return initialMemberships;
|
||||
}
|
||||
|
||||
@@ -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>(organizationsQuery);
|
||||
export function OrganizationsPageQuery() {
|
||||
const [queryRef, loadQuery] = useQueryLoader<OrganizationsPageQuery>(
|
||||
organizationsPageQuery,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
loadQuery({});
|
||||
188
apps/console/src/pages/iam/organizations/__generated__/OrganizationLayoutQuery.graphql.ts
generated
Normal file
188
apps/console/src/pages/iam/organizations/__generated__/OrganizationLayoutQuery.graphql.ts
generated
Normal file
@@ -0,0 +1,188 @@
|
||||
/**
|
||||
* @generated SignedSource<<e99ea3ab9f033b6a5384d389dd964f06>>
|
||||
* @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;
|
||||
@@ -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<PropertyKey, never>;
|
||||
export type OrganizationsQuery$data = {
|
||||
export type OrganizationsPageQuery$variables = Record<PropertyKey, never>;
|
||||
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;
|
||||
@@ -1,42 +0,0 @@
|
||||
/**
|
||||
* @generated SignedSource<<b6a869ebbc0ded2da70f59b7856b906f>>
|
||||
* @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;
|
||||
@@ -10,7 +10,7 @@ const fragment = graphql`
|
||||
id
|
||||
role
|
||||
createdAt
|
||||
organization {
|
||||
organization @required(action: THROW) {
|
||||
id
|
||||
name
|
||||
}
|
||||
|
||||
@@ -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<MembershipCardFragment$key>(fragment, fKey);
|
||||
const isAuthenticated = !!activeSession;
|
||||
const { lastSession, organization } = useFragment<MembershipCardFragment$key>(
|
||||
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
|
||||
|
||||
@@ -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<OrganizationCardFragment$key>(
|
||||
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 (
|
||||
// <Badge variant="success" className="flex items-center gap-1">
|
||||
// <IconCheckmark1 size={14} />
|
||||
// {__("Authenticated")}
|
||||
// </Badge>
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (isExpired) {
|
||||
// return (
|
||||
// <Badge variant="warning" className="flex items-center gap-1">
|
||||
// <IconClock size={14} />
|
||||
// {__("Session expired")}
|
||||
// </Badge>
|
||||
// );
|
||||
// }
|
||||
|
||||
// if (needsAuth) {
|
||||
return (
|
||||
<Badge variant="neutral" className="flex items-center gap-1">
|
||||
<IconLock size={14} />
|
||||
{__("Authentication required")}
|
||||
</Badge>
|
||||
);
|
||||
// }
|
||||
|
||||
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 (
|
||||
<Card padded className="w-full">
|
||||
<div className="flex items-center justify-between">
|
||||
{isSAMLUrl ? (
|
||||
<a
|
||||
href={targetUrl}
|
||||
className="flex items-center gap-4 hover:text-primary flex-1"
|
||||
>
|
||||
<Avatar
|
||||
src={organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="l"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
||||
{getAuthBadge()}
|
||||
</div>
|
||||
</a>
|
||||
) : (
|
||||
<Link
|
||||
to={targetUrl}
|
||||
className="flex items-center gap-4 hover:text-primary flex-1"
|
||||
>
|
||||
<Avatar
|
||||
src={organization.logoUrl}
|
||||
name={organization.name}
|
||||
size="l"
|
||||
/>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h2 className="font-semibold text-xl">{organization.name}</h2>
|
||||
{getAuthBadge()}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
<div className="flex items-center gap-3">
|
||||
<Button asChild>
|
||||
{/* {isSAMLUrl ? (
|
||||
<a href={targetUrl}>{getButtonText()}</a>
|
||||
) : (
|
||||
<Link to={targetUrl}>{getButtonText()}</Link>
|
||||
)} */}
|
||||
<Link to={targetUrl}>LOGIN</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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>(
|
||||
organizationDropdownMenuQuery,
|
||||
);
|
||||
|
||||
const handleOpenMenu = useCallback(
|
||||
(open: boolean) => {
|
||||
if (open) loadQuery({});
|
||||
},
|
||||
[loadQuery],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Dropdown
|
||||
onOpenChange={handleOpenMenu}
|
||||
toggle={
|
||||
<Button
|
||||
className="-ml-3"
|
||||
variant="tertiary"
|
||||
iconAfter={IconChevronGrabberVertical}
|
||||
>
|
||||
{currentOrganization.name}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<div className="px-3 py-2">
|
||||
<Input
|
||||
icon={IconMagnifyingGlass}
|
||||
placeholder={__("Search organizations...")}
|
||||
value={search}
|
||||
onValueChange={setSearch}
|
||||
onKeyDown={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
<div className="max-h-150 overflow-y-auto scrollbar-thin scrollbar-thumb-gray-300 scrollbar-track-transparent hover:scrollbar-thumb-gray-400">
|
||||
{queryRef && (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="px-3 py-2 text-gray-500">
|
||||
{__("Loading organizations...")}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<OrganizationDropdownMenu search={search} queryRef={queryRef} />
|
||||
</Suspense>
|
||||
)}
|
||||
</div>
|
||||
<DropdownSeparator />
|
||||
</Dropdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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<OrganizationDropdownMenuQuery>;
|
||||
search: string;
|
||||
}
|
||||
|
||||
export function OrganizationDropdownMenu(props: OrganizationDropdownMenuProps) {
|
||||
const { queryRef, search } = props;
|
||||
|
||||
const {
|
||||
viewer: {
|
||||
memberships: { edges: initialMemberships },
|
||||
},
|
||||
} = usePreloadedQuery<OrganizationDropdownMenuQuery>(
|
||||
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 }) => (
|
||||
<OrganizationDropdownMenuItem fKey={node} key={node.id} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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<OrganizationDropdownMenuItemFragment$key>(fragment, fKey);
|
||||
|
||||
const isAuthenticated = !!lastSession;
|
||||
const isExpired =
|
||||
lastSession && parseDate(lastSession.expiresAt) >= new Date();
|
||||
|
||||
return (
|
||||
<DropdownItem key={id}>
|
||||
{/* TODO add link or anchor */}
|
||||
<Avatar name={organization.name} src={organization.logoUrl} />
|
||||
<span className="flex-1">{organization.name}</span>
|
||||
{isAuthenticated && (
|
||||
<IconCheckmark1 size={16} className="text-green-600" />
|
||||
)}
|
||||
{isExpired && <IconClock size={16} className="text-orange-600" />}
|
||||
{!lastSession && <IconLock size={16} className="text-gray-400" />}
|
||||
</DropdownItem>
|
||||
);
|
||||
}
|
||||
166
apps/console/src/pages/iam/organizations/_components/Sidebar.tsx
Normal file
166
apps/console/src/pages/iam/organizations/_components/Sidebar.tsx
Normal file
@@ -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 (
|
||||
<ul className="space-y-[2px]">
|
||||
{isAuthorized("Organization", "listMeetings") && (
|
||||
<SidebarItem
|
||||
label={__("Meetings")}
|
||||
icon={IconCalendar1}
|
||||
to={`${prefix}/meetings`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listTasks") && (
|
||||
<SidebarItem
|
||||
label={__("Tasks")}
|
||||
icon={IconInboxEmpty}
|
||||
to={`${prefix}/tasks`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listMeasures") && (
|
||||
<SidebarItem
|
||||
label={__("Measures")}
|
||||
icon={IconTodo}
|
||||
to={`${prefix}/measures`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listRisks") && (
|
||||
<SidebarItem
|
||||
label={__("Risks")}
|
||||
icon={IconFire3}
|
||||
to={`${prefix}/risks`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listFrameworks") && (
|
||||
<SidebarItem
|
||||
label={__("Frameworks")}
|
||||
icon={IconBank}
|
||||
to={`${prefix}/frameworks`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listPeople") && (
|
||||
<SidebarItem
|
||||
label={__("People")}
|
||||
icon={IconGroup1}
|
||||
to={`${prefix}/people`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listVendors") && (
|
||||
<SidebarItem
|
||||
label={__("Vendors")}
|
||||
icon={IconStore}
|
||||
to={`${prefix}/vendors`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listDocuments") && (
|
||||
<SidebarItem
|
||||
label={__("Documents")}
|
||||
icon={IconPageTextLine}
|
||||
to={`${prefix}/documents`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listAssets") && (
|
||||
<SidebarItem
|
||||
label={__("Assets")}
|
||||
icon={IconBox}
|
||||
to={`${prefix}/assets`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listData") && (
|
||||
<SidebarItem
|
||||
label={__("Data")}
|
||||
icon={IconListStack}
|
||||
to={`${prefix}/data`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listAudits") && (
|
||||
<SidebarItem
|
||||
label={__("Audits")}
|
||||
icon={IconMedal}
|
||||
to={`${prefix}/audits`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listNonconformities") && (
|
||||
<SidebarItem
|
||||
label={__("Nonconformities")}
|
||||
icon={IconCrossLargeX}
|
||||
to={`${prefix}/nonconformities`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listObligations") && (
|
||||
<SidebarItem
|
||||
label={__("Obligations")}
|
||||
icon={IconBook}
|
||||
to={`${prefix}/obligations`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listContinualImprovements") && (
|
||||
<SidebarItem
|
||||
label={__("Continual Improvements")}
|
||||
icon={IconRotateCw}
|
||||
to={`${prefix}/continual-improvements`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listProcessingActivities") && (
|
||||
<SidebarItem
|
||||
label={__("Processing Activities")}
|
||||
icon={IconCircleProgress}
|
||||
to={`${prefix}/processing-activities`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listSnapshots") && (
|
||||
<SidebarItem
|
||||
label={__("Snapshots")}
|
||||
icon={IconClock}
|
||||
to={`${prefix}/snapshots`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "getTrustCenter") && (
|
||||
<SidebarItem
|
||||
label={__("Trust Center")}
|
||||
icon={IconShield}
|
||||
to={`${prefix}/trust-center`}
|
||||
/>
|
||||
)}
|
||||
{isAuthorized("Organization", "listMembers") && (
|
||||
<SidebarItem
|
||||
label={__("Settings")}
|
||||
icon={IconSettingsGear2}
|
||||
to={`${prefix}/settings`}
|
||||
/>
|
||||
)}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @generated SignedSource<<5193da6ee4b5da0bed510c255c3407b1>>
|
||||
* @generated SignedSource<<acb8bfd444063a884a8a3a35eed8971f>>
|
||||
* @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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @generated SignedSource<<ef66c9eab961d5c3c28669629b82cfd6>>
|
||||
* @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;
|
||||
@@ -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<PropertyKey, never>;
|
||||
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;
|
||||
@@ -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<PermissionsResponse>({
|
||||
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<PermissionsResponse>;
|
||||
},
|
||||
});
|
||||
// const { data } = useSuspenseQuery<PermissionsResponse>({
|
||||
// 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<PermissionsResponse>;
|
||||
// },
|
||||
// });
|
||||
|
||||
// @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 (
|
||||
<PermissionsContext value={{ ...data, isAuthorized }}>
|
||||
<PermissionsContext
|
||||
value={{ permissions: {}, role: Role.OWNER, isAuthorized }}
|
||||
>
|
||||
{children}
|
||||
</PermissionsContext>
|
||||
);
|
||||
|
||||
// return (
|
||||
// <PermissionsContext value={{ ...data, isAuthorized }}>
|
||||
// {children}
|
||||
// </PermissionsContext>
|
||||
// );
|
||||
}
|
||||
|
||||
@@ -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: () => (
|
||||
<RelayEnvironmentProvider environment={connectEnvironment}>
|
||||
<OrganizationsQuery />
|
||||
<OrganizationsPageQuery />
|
||||
</RelayEnvironmentProvider>
|
||||
),
|
||||
},
|
||||
@@ -184,8 +184,8 @@ const routes = [
|
||||
{
|
||||
path: "/organizations/:organizationId",
|
||||
Component: () => (
|
||||
<RelayEnvironmentProvider environment={consoleEnvironment}>
|
||||
<MainLayout />
|
||||
<RelayEnvironmentProvider environment={connectEnvironment}>
|
||||
<OrganizationLayoutQuery />
|
||||
</RelayEnvironmentProvider>
|
||||
),
|
||||
ErrorBoundary: ErrorBoundary,
|
||||
|
||||
@@ -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 (
|
||||
<DropdownMenu.Root open={open}>
|
||||
<DropdownMenu.Root open={open} onOpenChange={onOpenChange}>
|
||||
{toggle && (
|
||||
<DropdownMenu.Trigger asChild>{toggle}</DropdownMenu.Trigger>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user