Skip to content

Commit

Permalink
Merge pull request #610 from sinamics/member-count
Browse files Browse the repository at this point in the history
Added tot/act for members in the Network table.
  • Loading branch information
sinamics authored Dec 17, 2024
2 parents 21c2b5e + b2c25fd commit 13abc19
Show file tree
Hide file tree
Showing 15 changed files with 174 additions and 17 deletions.
12 changes: 9 additions & 3 deletions src/components/networkPage/networkTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import NetworkOptionsModal from "./networkOptionsModal";
import { CopyToClipboard } from "react-copy-to-clipboard";
import toast from "react-hot-toast";
import CopyIcon from "~/icons/copy";
import { NetworkTableMemberCount } from "./networkTableMemberCount";

const LOCAL_STORAGE_KEY = "networkTableSorting";

Expand Down Expand Up @@ -61,6 +62,9 @@ export const NetworkTable = ({ tableData = [] }) => {
members: network_members[];
networkMembers: network_members[];
action: string;
memberCounts: {
display: string;
};
};
const columnHelper = createColumnHelper<ColumnsType>();
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
Expand Down Expand Up @@ -101,10 +105,12 @@ export const NetworkTable = ({ tableData = [] }) => {
},
}),
columnHelper.accessor("members", {
header: () => <span>{t("commonTable.header.members")}</span>,
header: () => <span>{t("commonTable.header.memberActTot")}</span>,
cell: ({ row: { original } }) => {
if (!Array.isArray(original.networkMembers)) return <span>0</span>;
return <span>{original.networkMembers.length}</span>;
if (!Array.isArray(original.networkMembers)) {
return <NetworkTableMemberCount count="0" />;
}
return <NetworkTableMemberCount count={original.memberCounts.display} />;
},
}),
columnHelper.accessor("action", {
Expand Down
7 changes: 7 additions & 0 deletions src/components/networkPage/networkTableMemberCount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const NetworkTableMemberCount = ({ count }) => {
return (
<div className="tabular-nums inline-flex justify-end min-w-[3.5rem]">
<span>{count}</span>
</div>
);
};
13 changes: 7 additions & 6 deletions src/components/organization/networkTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import TableFooter from "~/components/shared/tableFooter";
import { CopyToClipboard } from "react-copy-to-clipboard";
import toast from "react-hot-toast";
import CopyIcon from "~/icons/copy";
import { MemberCounts } from "~/types/local/member";
import { NetworkTableMemberCount } from "../networkPage/networkTableMemberCount";

// import { makeNetworkData } from "../../utils/fakeData";
const TruncateText = ({ text }: { text: string }) => {
Expand All @@ -41,6 +43,7 @@ const LOCAL_STORAGE_KEY = "centralNetworkTableSorting";

interface OrgMemberEntity extends CentralMemberEntity {
networkMembers: string[];
memberCounts: MemberCounts;
}

export const OrganizationNetworkTable = ({ tableData = [] }) => {
Expand All @@ -64,14 +67,12 @@ export const OrganizationNetworkTable = ({ tableData = [] }) => {
header: () => <span>{t("commonTable.header.name")}</span>,
}),
columnHelper.accessor("description", {
size: 300,
size: 280,
cell: (info) => <TruncateText text={info.getValue()} />,
header: () => <span>{t("commonTable.header.description")}</span>,
}),
columnHelper.accessor("nwid", {
// cell: (info) => info.getValue(),
header: () => <span>{t("commonTable.header.networkId")}</span>,
// footer: (info) => info.column.id,
cell: ({ row: { original } }) => {
return (
<div onClick={(e) => e.stopPropagation()}>
Expand All @@ -93,11 +94,11 @@ export const OrganizationNetworkTable = ({ tableData = [] }) => {
);
},
}),
columnHelper.accessor("networkMembers", {
header: () => <span>{t("commonTable.header.members")}</span>,
columnHelper.accessor("memberCounts", {
header: () => <span>{t("commonTable.header.memberActTot")}</span>,
cell: (info) => {
const data = info.getValue();
return data.length;
return <NetworkTableMemberCount count={data.display} />;
},
}),
],
Expand Down
1 change: 1 addition & 0 deletions src/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"description": "Description",
"networkId": "Network ID",
"members": "Members",
"memberActTot": "Nodes Act/Tot",
"actions": "Actions",
"role": "Role",
"email": "Email",
Expand Down
1 change: 1 addition & 0 deletions src/locales/es/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"description": "Descripción",
"networkId": "ID de Red",
"members": "Miembros",
"memberActTot": "Nodos Act/Tot",
"actions": "Acciones",
"role": "Rol",
"email": "Correo electrónico",
Expand Down
1 change: 1 addition & 0 deletions src/locales/fr/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"description": "Description",
"networkId": "Identifiant du réseau",
"members": "Membres",
"memberActTot": "Nœuds Act/Tot",
"actions": "Actions",
"role": "Rôle",
"email": "Email",
Expand Down
1 change: 1 addition & 0 deletions src/locales/no/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"description": "Beskrivelse",
"networkId": "Nettverks-ID",
"members": "Medlemmer",
"memberActTot": "Noder Akt/Tot",
"actions": "Handlinger",
"role": "Rolle",
"email": "E-post",
Expand Down
3 changes: 2 additions & 1 deletion src/locales/pl/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"description": "Opis sieci",
"networkId": "ID Sieci",
"members": "Ilość członków",
"memberActTot": "Węzły Akt/Całk",
"actions": "Akcje",
"role": "Rola",
"email": "Email",
Expand Down Expand Up @@ -939,4 +940,4 @@
}
}
}
}
}
1 change: 1 addition & 0 deletions src/locales/ru/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"description": "Описание",
"networkId": "ID сети",
"members": "Участники",
"memberActTot": "Узлы Акт/Всего",
"actions": "Действия",
"role": "Роль",
"email": "Email",
Expand Down
1 change: 1 addition & 0 deletions src/locales/zh-tw/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"description": "描述",
"networkId": "網路ID",
"members": "成員",
"memberActTot": "節點 活躍/總數",
"actions": "操作",
"role": "角色",
"email": "電子郵件",
Expand Down
1 change: 1 addition & 0 deletions src/locales/zh/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"description": "描述",
"networkId": "网络ID",
"members": "成员",
"memberActTot": "节点 活跃/总数",
"actions": "操作",
"role": "角色",
"email": "电子邮件",
Expand Down
20 changes: 20 additions & 0 deletions src/server/api/__tests__/network/getUserNetworks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { appRouter } from "../../root";
import { type Session } from "next-auth";
import { PrismaClient } from "@prisma/client";
import { type PartialDeep } from "type-fest";
import { MemberCounts } from "~/types/local/member";

const mockSession: PartialDeep<Session> = {
expires: new Date().toISOString(),
Expand All @@ -14,6 +15,19 @@ const mockSession: PartialDeep<Session> = {
},
};

// Mock the ZeroTier controller
jest.mock("~/utils/ztApi", () => ({
member_details: jest.fn().mockImplementation((_ctx, nwid, memberId) => {
// Mock both members as authorized for this test
return Promise.resolve({
authorized: true,
// Add other required properties that your implementation might need
id: memberId,
nwid: nwid,
});
}),
}));

test("getUserNetworks", async () => {
const prismaMock = new PrismaClient();

Expand All @@ -22,6 +36,7 @@ test("getUserNetworks", async () => {
name: string;
authorId: number;
networkMembers: NetworkMember[];
memberCounts: MemberCounts;
}

interface NetworkMember {
Expand All @@ -33,6 +48,11 @@ test("getUserNetworks", async () => {
nwid: "1",
name: "test",
authorId: 10,
memberCounts: {
display: "2 (2)",
authorized: 2,
total: 2,
},
networkMembers: [
{
id: "4ef7287f63",
Expand Down
44 changes: 41 additions & 3 deletions src/server/api/routers/networkRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { type TagsByName, type NetworkEntity, RoutesEntity } from "~/types/local
import { MemberEntity, type CapabilitiesByName } from "~/types/local/member";
import { type CentralNetwork } from "~/types/central/network";
import { checkUserOrganizationRole } from "~/utils/role";
import { Prisma, Role } from "@prisma/client";
import { network, network_members, Prisma, Role } from "@prisma/client";
import { HookType, NetworkConfigChanged, NetworkDeleted } from "~/types/webhooks";
import { sendWebhook } from "~/utils/webhook";
import { fetchZombieMembers, syncMemberPeersAndStatus } from "../services/memberService";
Expand Down Expand Up @@ -52,19 +52,57 @@ export const networkRouter = createTRPCRouter({
return await ztController.get_controller_networks(ctx, input.central);
}

const networks = await ctx.prisma.network.findMany({
// Define the interface for member counts
interface MemberCounts {
authorized: number;
total: number;
display: string;
}

interface NetworkWithMemberCount extends network {
memberCounts: MemberCounts;
networkMembers: network_members[];
}

const rawNetworks = (await ctx.prisma.network.findMany({
where: {
authorId: ctx.session.user.id,
},
include: {
networkMembers: {
select: {
id: true,
deleted: true,
},
},
},
});
})) as unknown as Omit<NetworkWithMemberCount, "memberCounts">[];

// Initialize networks with memberCounts property
const networks: NetworkWithMemberCount[] = rawNetworks.map((network) => ({
...network,
memberCounts: {
authorized: 0,
total: 0,
display: "0 (0)",
},
}));

// Get authorized member and total member counts for each network.
for (const network of networks) {
for (const member of network.networkMembers) {
const memberDetails = await ztController.member_details(
ctx,
network.nwid,
member.id,
);
if (memberDetails.authorized) {
network.memberCounts.authorized += 1;
}
network.memberCounts.total += 1;
network.memberCounts.display = `${network.memberCounts.authorized} (${network.memberCounts.total})`;
}
}
return networks;
}),

Expand Down
79 changes: 75 additions & 4 deletions src/server/api/routers/organizationRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,16 @@ import {
generateInstanceSecret,
} from "~/utils/encryption";
import { sendMailWithTemplate } from "~/utils/mail";
import { Role } from "@prisma/client";
import {
Invitation,
network,
network_members,
Organization,
Role,
User,
UserOrganizationRole,
Webhook,
} from "@prisma/client";
import { checkUserOrganizationRole } from "~/utils/role";
import { HookType, NetworkCreated, OrgMemberRemoved } from "~/types/webhooks";
import { throwError } from "~/server/helpers/errorHandler";
Expand All @@ -25,6 +34,8 @@ import { nameGeneratorConfig } from "../services/networkService";
import rateLimit from "~/utils/rateLimit";
import { RoutesEntity } from "~/types/local/network";
import { MailTemplateKey } from "~/utils/enums";
import { TRPCError } from "@trpc/server";
import { MemberCounts } from "~/types/local/member";

// Create a Zod schema for the HookType enum
const HookTypeEnum = z.enum(Object.values(HookType) as [HookType, ...HookType[]]);
Expand Down Expand Up @@ -311,8 +322,22 @@ export const organizationRouter = createTRPCRouter({
organizationId: input.organizationId,
minimumRequiredRole: Role.READ_ONLY,
});
// get all organizations related to the user
return await ctx.prisma.organization.findUnique({

// Define the interface for member counts

interface OrganizationWithCounts extends Organization {
userRoles: UserOrganizationRole[];
users: User[];
webhooks: Webhook[];
invitations: Invitation[];
memberCounts: MemberCounts;
networks: (network & {
networkMembers: network_members[];
memberCounts: MemberCounts;
})[];
}

const rawOrganization = (await ctx.prisma.organization.findUnique({
where: {
id: input.organizationId,
},
Expand All @@ -327,7 +352,53 @@ export const organizationRouter = createTRPCRouter({
},
},
},
});
})) as unknown as Omit<OrganizationWithCounts, "memberCounts">;

if (!rawOrganization) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Organization not found",
});
}

// Initialize organization with member counts
const organization: OrganizationWithCounts = {
...rawOrganization,
memberCounts: {
authorized: 0,
total: 0,
display: "0 (0)",
},
networks: rawOrganization.networks.map((network) => ({
...network,
memberCounts: {
authorized: 0,
total: 0,
display: "0 (0)",
},
})),
};

// Get authorized member and total member counts for each network
for (const network of organization.networks) {
for (const member of network.networkMembers) {
const memberDetails = await ztController.member_details(
ctx,
network.nwid,
member.id,
);
if (memberDetails.authorized) {
network.memberCounts.authorized += 1;
organization.memberCounts.authorized += 1;
}
network.memberCounts.total += 1;
organization.memberCounts.total += 1;
}
network.memberCounts.display = `${network.memberCounts.authorized} (${network.memberCounts.total})`;
}

organization.memberCounts.display = `${organization.memberCounts.authorized} (${organization.memberCounts.total})`;
return organization;
}),
createOrgNetwork: protectedProcedure
.input(
Expand Down
6 changes: 6 additions & 0 deletions src/types/local/member.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,9 @@ export interface Notation {
orderIndex?: number;
visibility?: string;
}

export interface MemberCounts {
authorized: number;
total: number;
display: string;
}

0 comments on commit 13abc19

Please sign in to comment.