Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,59 +1,115 @@
import { cn } from "@/lib/utils";
import { Page2, Tag } from "@unkey/icons";
import { Key2, Page2, Tag } from "@unkey/icons";

export const AssignedItemsCell = ({
items,
totalCount,
value,
isSelected = false,
type,
kind,
}: {
items: string[];
totalCount?: number;
value?: string;
isSelected?: boolean;
type: "roles" | "slug";
kind: "roles" | "keys" | "permissions" | "slug";
}) => {
const hasMore = totalCount && totalCount > items.length;
const icon =
type === "roles" ? <Tag size="md-regular" /> : <Page2 className="size-3" size="md-regular" />;
const getIcon = () => {
switch (kind) {
case "roles":
return <Tag size="md-regular" className="opacity-50" />;
case "keys":
return <Key2 size="md-regular" className="opacity-50" />;
case "slug":
return <Page2 size="md-regular" className="opacity-50" />;
default:
throw new Error(`Invalid type: ${kind}`);
}
};

const getDisplayText = (count: number) => {
if (count === 1) {
switch (kind) {
case "roles":
return "Role";
case "keys":
return "Key";
case "permissions":
return "Permission";
default:
throw new Error(`Invalid type: ${kind}`);
}
}

switch (kind) {
case "roles":
return "Roles";
case "keys":
return "Keys";
case "permissions":
return "Permissions";
default:
throw new Error(`Invalid type: ${kind}`);
}
};

const itemClassName = cn(
"font-mono rounded-md py-[2px] px-1.5 items-center w-fit flex gap-2 transition-all duration-100 border border-dashed text-grayA-12",
isSelected ? "bg-grayA-4 border-grayA-7" : "bg-grayA-3 border-grayA-6 group-hover:bg-grayA-4",
);

const emptyClassName = cn(
"rounded-md py-[2px] px-1.5 items-center w-fit flex gap-2 transition-all duration-100 border border-dashed bg-grayA-2 ",
"rounded-md py-[2px] px-1.5 items-center w-fit flex gap-2 transition-all duration-100 border border-dashed bg-grayA-2",
isSelected ? "border-grayA-7 text-grayA-9" : "border-grayA-6 text-grayA-8",
);

if (items.length === 0) {
if (kind === "slug") {
if (!value) {
return (
<div className="flex flex-col gap-1 py-2 max-w-[200px] animate-in fade-in slide-in-from-top-2 duration-300">
<div className={emptyClassName}>
{getIcon()}
<span className="text-grayA-9 text-xs">No slug</span>
</div>
</div>
);
}

return (
<div className="flex flex-col gap-1 py-2 max-w-[200px] animate-in fade-in slide-in-from-top-2 duration-300">
<div
className={cn(itemClassName, "animate-in fade-in slide-in-from-left-2")}
style={{ animationDelay: "50ms" }}
>
{getIcon()}
<div className="text-grayA-11 text-xs max-w-[150px] truncate" title={value}>
{value}
</div>
</div>
</div>
);
}

if (!totalCount) {
return (
<div className="flex flex-col gap-1 py-1 max-w-[200px]">
<div className="flex flex-col gap-1 py-2 max-w-[200px] animate-in fade-in slide-in-from-top-2 duration-300">
<div className={emptyClassName}>
{icon}
{getIcon()}
<span className="text-grayA-9 text-xs">None assigned</span>
</div>
</div>
);
}

return (
<div className="flex flex-col gap-1 py-2 max-w-[200px]">
{items.map((item) => (
<div className={itemClassName} key={item}>
{icon}
<span className="text-grayA-11 text-xs max-w-[150px] truncate" title={item}>
{item}
</span>
</div>
))}
{hasMore && (
<div className={itemClassName}>
<span className="text-grayA-9 text-xs max-w-[150px] truncate">
{totalCount - items.length} more permissions...
</span>
<div className="flex flex-col gap-1 py-2 max-w-[200px] animate-in fade-in slide-in-from-top-2 duration-300">
<div
className={cn(itemClassName, "animate-in fade-in slide-in-from-left-2")}
style={{ animationDelay: "50ms" }}
>
{getIcon()}
<div className="text-grayA-11 text-xs max-w-[150px] truncate">
{totalCount} {getDisplayText(totalCount)}
</div>
)}
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { cn } from "@/lib/utils";
import { ChartActivity2, Dots, Page2, Tag } from "@unkey/icons";
import { ChartActivity2, Dots, Key2, Page2, Tag } from "@unkey/icons";

export const RoleColumnSkeleton = () => (
<div className="flex flex-col items-start px-[18px] py-[6px]">
Expand Down Expand Up @@ -27,19 +27,18 @@ export const SlugColumnSkeleton = () => (
export const AssignedKeysColumnSkeleton = () => (
<div className="flex flex-col gap-1 py-2 max-w-[200px]">
<div className="rounded-md py-[2px] px-1.5 items-center w-fit flex gap-2 border border-dashed bg-grayA-3 border-grayA-6 animate-pulse h-[22px]">
<Page2 size="md-regular" className="opacity-50" />
<Tag size="md-regular" className="opacity-50" />
<div className="h-2 w-16 bg-grayA-3 rounded animate-pulse" />
</div>
<div className="rounded-md py-[2px] px-1.5 items-center w-fit flex gap-2 border border-dashed bg-grayA-3 border-grayA-6 animate-pulse h-[22px]">
<Page2 size="md-regular" className="opacity-50" />
<div className="h-2 w-12 bg-grayA-3 rounded animate-pulse" />
</div>
</div>
);

export const AssignedToKeysColumnSkeleton = () => (
<div className="px-1.5 rounded-md flex gap-2 items-center max-w-min h-[22px] bg-grayA-3 border-none animate-pulse">
<div className="h-2 w-16 bg-grayA-3 rounded animate-pulse" />
<div className="flex flex-col gap-1 py-2 max-w-[200px]">
<div className="rounded-md py-[2px] px-1.5 items-center w-fit flex gap-2 border border-dashed bg-grayA-3 border-grayA-6 animate-pulse h-[22px]">
<Key2 size="md-regular" className="opacity-50" />
<div className="h-2 w-16 bg-grayA-3 rounded animate-pulse" />
</div>
</div>
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { VirtualTable } from "@/components/virtual-table/index";
import type { Column } from "@/components/virtual-table/types";
import type { Permission } from "@/lib/trpc/routers/authorization/permissions/query";
import { BookBookmark, Page2 } from "@unkey/icons";
import { Badge, Button, Checkbox, Empty } from "@unkey/ui";
import { Button, Checkbox, Empty } from "@unkey/ui";
import { cn } from "@unkey/ui/src/lib/utils";
import { useCallback, useMemo, useState } from "react";
import { PermissionsTableActions } from "./components/actions/keys-table-action.popover.constants";
Expand All @@ -19,7 +19,7 @@ import {
SlugColumnSkeleton,
} from "./components/skeletons";
import { usePermissionsListQuery } from "./hooks/use-permissions-list-query";
import { STATUS_STYLES, getRowClassName } from "./utils/get-row-class";
import { getRowClassName } from "./utils/get-row-class";

export const PermissionsList = () => {
const { permissions, isLoading, isLoadingMore, loadMore, totalCount, hasMore } =
Expand Down Expand Up @@ -104,8 +104,8 @@ export const PermissionsList = () => {
width: "20%",
render: (permission) => (
<AssignedItemsCell
type="slug"
items={[permission.slug]}
kind="slug"
value={permission.slug}
isSelected={permission.permissionId === selectedPermission?.permissionId}
/>
),
Expand All @@ -116,9 +116,8 @@ export const PermissionsList = () => {
width: "20%",
render: (permission) => (
<AssignedItemsCell
type="roles"
items={permission.assignedRoles.items}
totalCount={permission.assignedRoles.totalCount}
kind="roles"
totalCount={permission.totalConnectedRoles}
isSelected={permission.permissionId === selectedPermission?.permissionId}
/>
),
Expand All @@ -127,32 +126,13 @@ export const PermissionsList = () => {
key: "assigned_to_keys",
header: "Assigned to Keys",
width: "20%",
render: (permission) => {
const keyCount = permission.totalConnectedKeys;

const getKeyText = (count: number): string => {
if (count === 0) {
return "None assigned";
}
if (count === 1) {
return "1 key";
}
return `${count} keys`;
};

return (
<Badge
className={cn(
"px-1.5 rounded-md flex gap-2 items-center max-w-min h-[22px] border-none cursor-pointer",
permission.permissionId === selectedPermission?.permissionId
? STATUS_STYLES.badge.selected
: STATUS_STYLES.badge.default,
)}
>
{getKeyText(keyCount)}
</Badge>
);
},
render: (permission) => (
<AssignedItemsCell
kind="keys"
totalCount={permission.totalConnectedKeys}
isSelected={permission.permissionId === selectedPermission?.permissionId}
/>
),
},
{
key: "last_updated",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export const useUpsertPermission = (
const permission = trpc.authorization.permissions.upsert.useMutation({
onSuccess(data) {
trpcUtils.authorization.permissions.invalidate();
trpcUtils.authorization.roles.invalidate();
// Show success toast
toast.success(data.isUpdate ? "Permission Updated" : "Permission Created", {
description: data.message,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { ActionComponentProps } from "@/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/keys-table-action.popover";
import { ConfirmPopover } from "@/components/confirmation-popover";
import type { Roles } from "@/lib/trpc/routers/authorization/roles/query";
import type { RoleBasic } from "@/lib/trpc/routers/authorization/roles/query";
import { zodResolver } from "@hookform/resolvers/zod";
import { TriangleWarning2 } from "@unkey/icons";
import { Button, DialogContainer, FormCheckbox } from "@unkey/ui";
Expand All @@ -18,7 +18,7 @@ const deleteRoleFormSchema = z.object({

type DeleteRoleFormValues = z.infer<typeof deleteRoleFormSchema>;

type DeleteRoleProps = { roleDetails: Roles } & ActionComponentProps;
type DeleteRoleProps = { roleDetails: RoleBasic } & ActionComponentProps;

export const DeleteRole = ({ roleDetails, isOpen, onClose }: DeleteRoleProps) => {
const [isConfirmPopoverOpen, setIsConfirmPopoverOpen] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import type { Roles } from "@/lib/trpc/routers/authorization/roles/query";
import type { RoleBasic } from "@/lib/trpc/routers/authorization/roles/query";
import { useEffect } from "react";
import { toast } from "sonner";
import { UpsertRoleDialog } from "../../../../upsert-role";
import { useFetchConnectedKeysAndPerms } from "./hooks/use-fetch-connected-keys-and-perms";
import { useFetchConnectedKeysAndPermsData } from "./hooks/use-fetch-connected-keys-and-perms";

export const EditRole = ({
role,
isOpen,
onClose,
}: {
role: Roles;
role: RoleBasic;
isOpen: boolean;
onClose: () => void;
}) => {
const { permissions, keys, error } = useFetchConnectedKeysAndPerms(role.roleId);
const { permissions, keys, error } = useFetchConnectedKeysAndPermsData(role.roleId);

useEffect(() => {
if (error) {
Expand Down Expand Up @@ -42,7 +42,6 @@ export const EditRole = ({
}
}
}, [error]);

return (
<UpsertRoleDialog
existingRole={{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,24 @@
"use client";
import { useRoleLimits } from "@/app/(app)/authorization/roles/components/table/hooks/use-role-limits";
import { trpc } from "@/lib/trpc/client";

export const useFetchConnectedKeysAndPerms = (roleId: string) => {
const { data, isLoading, error, refetch } =
trpc.authorization.roles.connectedKeysAndPerms.useQuery(
{
roleId,
},
{
enabled: Boolean(roleId),
},
);
export const useFetchConnectedKeysAndPermsData = (roleId: string) => {
const { calculateLimits } = useRoleLimits(roleId);
const { shouldPrefetch } = calculateLimits();

const query = trpc.authorization.roles.connectedKeysAndPerms.useQuery(
{ roleId },
{
enabled: shouldPrefetch && Boolean(roleId),
staleTime: 5 * 60 * 1000,
},
);

return {
keys: data?.keys || [],
permissions: data?.permissions || [],
isLoading,
error,
refetch,
keys: query.data?.keys || [],
permissions: query.data?.permissions || [],
hasData: Boolean(query.data),
isLoading: query.isLoading,
isError: query.isError,
error: query.error,
};
};
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Roles } from "@/lib/trpc/routers/authorization/roles/query";
import type { RoleBasic } from "@/lib/trpc/routers/authorization/roles/query";
import { Key2 } from "@unkey/icons";
import { InfoTooltip } from "@unkey/ui";

export const RoleInfo = ({ roleDetails }: { roleDetails: Roles }) => {
export const RoleInfo = ({ roleDetails }: { roleDetails: RoleBasic }) => {
return (
<div className="flex gap-5 items-center bg-white dark:bg-black border border-grayA-5 rounded-xl py-5 pl-[18px] pr-[26px]">
<div className="bg-grayA-5 text-gray-12 size-5 flex items-center justify-center rounded ">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
} from "@/app/(app)/apis/[apiId]/keys/[keyAuthId]/_components/components/table/components/actions/keys-table-action.popover";
import { toast } from "@/components/ui/toaster";
import { trpc } from "@/lib/trpc/client";
import type { Roles } from "@/lib/trpc/routers/authorization/roles/query";
import type { RoleBasic } from "@/lib/trpc/routers/authorization/roles/query";
import { Clone, PenWriting3, Trash } from "@unkey/icons";
import dynamic from "next/dynamic";
import { MAX_KEYS_FETCH_LIMIT } from "../../../upsert-role/components/assign-key/hooks/use-fetch-keys";
Expand All @@ -24,7 +24,7 @@ const KeysTableActionPopover = dynamic(
);

type RolesTableActionsProps = {
role: Roles;
role: RoleBasic;
};

export const RolesTableActions = ({ role }: RolesTableActionsProps) => {
Expand All @@ -35,7 +35,7 @@ export const RolesTableActions = ({ role }: RolesTableActionsProps) => {
};

const getRolesTableActionItems = (
role: Roles,
role: RoleBasic,
trpcUtils: ReturnType<typeof trpc.useUtils>,
): MenuItem[] => {
return [
Expand Down
Loading