mirror of
https://github.com/hatchet-dev/hatchet.git
synced 2026-05-12 21:28:50 -05:00
Fix: Show tenant invite modal correctly (#3815)
* feat: show first tenant as expanded, re-enable tenant invite * fix: check control plane * chore: lint * fix: copilot comments * fix: tsc * fix: add comment back * chore: lint * fix: separator logic * fix: action rendering, use simpletable * fix: I hate useEffect
This commit is contained in:
@@ -84,7 +84,7 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { AxiosError } from 'axios';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
const OFFICE_HOURS_URL = 'https://hatchet.run/office-hours';
|
||||
|
||||
@@ -115,6 +115,7 @@ function formatTimeoutMs(ms: number): string {
|
||||
// FIXME: remove this once we migrate everyone to the control plane
|
||||
type OrganizationTenantWithRegion = OrganizationTenant & {
|
||||
region?: ControlPlaneOrganizationTenant['region'];
|
||||
canManage?: boolean;
|
||||
};
|
||||
|
||||
export function CloudOrganizationSettings({ orgId }: { orgId: string }) {
|
||||
@@ -154,6 +155,7 @@ export function CloudOrganizationSettings({ orgId }: { orgId: string }) {
|
||||
const [tenantToArchive, setTenantToArchive] =
|
||||
useState<OrganizationTenantWithRegion | null>(null);
|
||||
const [expandedTenantIds, setExpandedTenantIds] = useState<string[]>([]);
|
||||
const autoExpandedTenantId = useRef<string | null>(null);
|
||||
const [editedName, setEditedName] = useState('');
|
||||
const [isEditingName, setIsEditingName] = useState(false);
|
||||
const [editedTimeout, setEditedTimeout] = useState('');
|
||||
@@ -244,6 +246,28 @@ export function CloudOrganizationSettings({ orgId }: { orgId: string }) {
|
||||
[org?.tenants, organization?.tenants],
|
||||
);
|
||||
|
||||
// showing the first tenant as open, to make clearer that:
|
||||
// 1. tenants can expand
|
||||
// 2. you can add members to tenants from here
|
||||
useEffect(() => {
|
||||
const firstVisibleTenantId = visibleTenants[0]?.id;
|
||||
|
||||
if (!firstVisibleTenantId) {
|
||||
autoExpandedTenantId.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoExpandedTenantId.current === firstVisibleTenantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoExpandedTenantId.current = firstVisibleTenantId;
|
||||
|
||||
if (!expandedTenantIds.length) {
|
||||
setExpandedTenantIds([firstVisibleTenantId]);
|
||||
}
|
||||
}, [visibleTenants, expandedTenantIds.length]);
|
||||
|
||||
const handleSaveName = () => {
|
||||
const trimmedName = editedName.trim();
|
||||
|
||||
@@ -1092,6 +1116,7 @@ export function OssOrganizationSettings() {
|
||||
const [tenantToArchive, setTenantToArchive] =
|
||||
useState<OrganizationTenantWithRegion | null>(null);
|
||||
const [expandedTenantIds, setExpandedTenantIds] = useState<string[]>([]);
|
||||
const autoExpandedTenantId = useRef<string | null>(null);
|
||||
|
||||
const visibleTenants = useMemo(
|
||||
() =>
|
||||
@@ -1105,12 +1130,37 @@ export function OssOrganizationSettings() {
|
||||
name: m.tenant.name,
|
||||
status: TenantStatusType.ACTIVE,
|
||||
slug: m.tenant.slug,
|
||||
canManage:
|
||||
m.role === TenantMemberRole.OWNER ||
|
||||
m.role === TenantMemberRole.ADMIN,
|
||||
};
|
||||
})
|
||||
.filter((t): t is OrganizationTenantWithRegion => t !== null) || [],
|
||||
[tenantMemberships],
|
||||
);
|
||||
|
||||
// showing the first tenant as open, to make clearer that:
|
||||
// 1. tenants can expand
|
||||
// 2. you can add members to tenants from here
|
||||
useEffect(() => {
|
||||
const firstVisibleTenantId = visibleTenants[0]?.id;
|
||||
|
||||
if (!firstVisibleTenantId) {
|
||||
autoExpandedTenantId.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (autoExpandedTenantId.current === firstVisibleTenantId) {
|
||||
return;
|
||||
}
|
||||
|
||||
autoExpandedTenantId.current = firstVisibleTenantId;
|
||||
|
||||
if (!expandedTenantIds.length) {
|
||||
setExpandedTenantIds([firstVisibleTenantId]);
|
||||
}
|
||||
}, [visibleTenants, expandedTenantIds.length]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex-grow">
|
||||
<div className="mx-auto px-4 py-8 sm:px-6 lg:px-8">
|
||||
@@ -1199,7 +1249,7 @@ function TenantsSection({
|
||||
onValueChange={setExpandedTenantIds}
|
||||
className="space-y-3 rounded-md border bg-background p-3"
|
||||
>
|
||||
{tenants.map((tenant) => (
|
||||
{tenants.map((tenant, ix) => (
|
||||
<div key={tenant.id}>
|
||||
<TenantAccordionItem
|
||||
key={tenant.id}
|
||||
@@ -1208,7 +1258,7 @@ function TenantsSection({
|
||||
onArchive={onArchive}
|
||||
canManageOrganization={canManageOrganization}
|
||||
/>
|
||||
<Separator className="my-3 last:hidden" />
|
||||
{ix < tenants.length - 1 && <Separator className="my-4" />}
|
||||
</div>
|
||||
))}
|
||||
</Accordion>
|
||||
@@ -1243,10 +1293,17 @@ function TenantAccordionItem({
|
||||
...tenantInviteListQuery(tenant.id),
|
||||
enabled: isExpanded,
|
||||
});
|
||||
const { isCloudEnabled } = useUserUniverse();
|
||||
const { isControlPlaneEnabled } = useControlPlane();
|
||||
|
||||
const tenantMembers = membersQuery.data?.rows || [];
|
||||
const tenantInvites = invitesQuery.data?.rows || [];
|
||||
|
||||
const canManageTenantMembers =
|
||||
canManageOrganization ||
|
||||
// if both cloud and the control plane are disabled, we're on the OSS and tenant admins / owners can invite members to their tenants
|
||||
(!(isCloudEnabled || isControlPlaneEnabled) && Boolean(tenant.canManage));
|
||||
|
||||
return (
|
||||
<AccordionItem value={tenant.id} className="overflow-hidden bg-background">
|
||||
<div className="flex items-center justify-between gap-2 px-3 py-2">
|
||||
@@ -1278,7 +1335,7 @@ function TenantAccordionItem({
|
||||
<div className="space-y-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-medium">Members</h4>
|
||||
{canManageOrganization && (
|
||||
{canManageTenantMembers && (
|
||||
<Button
|
||||
onClick={() =>
|
||||
globalEmitter.emit('create-tenant-invite', {
|
||||
@@ -1305,7 +1362,7 @@ function TenantAccordionItem({
|
||||
<TenantMemberList
|
||||
tenantId={tenant.id}
|
||||
members={tenantMembers}
|
||||
canManage={canManageOrganization}
|
||||
canManage={canManageTenantMembers}
|
||||
onMembersChanged={() => membersQuery.refetch()}
|
||||
/>
|
||||
) : (
|
||||
@@ -1338,62 +1395,59 @@ function TenantMemberList({
|
||||
onMembersChanged: () => void;
|
||||
}) {
|
||||
const [memberToEdit, setMemberToEdit] = useState<TenantMember | null>(null);
|
||||
|
||||
const gridCols = canManage
|
||||
? 'grid-cols-[minmax(0,1.2fr)_minmax(0,1.6fr)_140px_140px_72px]'
|
||||
: 'grid-cols-[minmax(0,1.2fr)_minmax(0,1.6fr)_140px_140px]';
|
||||
const columns = useMemo(
|
||||
() => [
|
||||
{
|
||||
columnLabel: 'Name',
|
||||
cellRenderer: (member: TenantMember) => (
|
||||
<span className="font-medium">{member.user.name}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
columnLabel: 'Email',
|
||||
cellRenderer: (member: TenantMember) => (
|
||||
<span className="font-mono text-sm">{member.user.email}</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
columnLabel: 'Role',
|
||||
cellRenderer: (member: TenantMember) => (
|
||||
<Badge variant="outline">{member.role}</Badge>
|
||||
),
|
||||
},
|
||||
{
|
||||
columnLabel: 'Joined',
|
||||
cellRenderer: (member: TenantMember) => (
|
||||
<RelativeDate date={member.metadata.createdAt} />
|
||||
),
|
||||
},
|
||||
...(canManage
|
||||
? [
|
||||
{
|
||||
columnLabel: 'Actions',
|
||||
cellRenderer: (member: TenantMember) => (
|
||||
<TenantMemberActions
|
||||
member={member}
|
||||
tenantId={tenantId}
|
||||
onEditRoleClick={setMemberToEdit}
|
||||
onChangePasswordClick={() => {}}
|
||||
onDeleteSuccess={onMembersChanged}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
[canManage, onMembersChanged, tenantId],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rounded-md border border-border/70">
|
||||
<div
|
||||
className={`hidden ${gridCols} gap-3 border-b border-border/70 bg-muted/20 px-4 py-2 text-xs font-medium uppercase tracking-wide text-muted-foreground md:grid`}
|
||||
>
|
||||
<span>Name</span>
|
||||
<span>Email</span>
|
||||
<span>Role</span>
|
||||
<span>Joined</span>
|
||||
{canManage && <span className="text-right">Actions</span>}
|
||||
</div>
|
||||
<div>
|
||||
{members.map((member) => (
|
||||
<div
|
||||
key={member.metadata.id}
|
||||
className={`grid ${gridCols} gap-3 border-b border-border/50 px-4 py-3 last:border-b-0 md:items-center`}
|
||||
>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground md:hidden">Name</p>
|
||||
<p className="font-medium">{member.user.name}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground md:hidden">Email</p>
|
||||
<p className="font-mono text-sm">{member.user.email}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground md:hidden">Role</p>
|
||||
<Badge variant="outline">{member.role}</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted-foreground md:hidden">
|
||||
Joined
|
||||
</p>
|
||||
<RelativeDate date={member.metadata.createdAt} />
|
||||
</div>
|
||||
{canManage && (
|
||||
<div className="flex justify-end">
|
||||
<TenantMemberActions
|
||||
member={member}
|
||||
tenantId={tenantId}
|
||||
onEditRoleClick={setMemberToEdit}
|
||||
onChangePasswordClick={() => {}}
|
||||
onDeleteSuccess={onMembersChanged}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<SimpleTable
|
||||
columns={columns}
|
||||
data={members}
|
||||
rowKey={(member) => member.metadata.id}
|
||||
/>
|
||||
|
||||
{memberToEdit && (
|
||||
<TenantMemberUpdateDialog
|
||||
|
||||
Reference in New Issue
Block a user