From 1579b16280af6ad32679202df21a32d4d899cd63 Mon Sep 17 00:00:00 2001 From: Riccardo Forina Date: Tue, 3 Oct 2023 17:05:11 +0200 Subject: [PATCH] Remove charts and visual details that will not be available for mvp (#57) --- ui/api/kafka.ts | 42 +++ ui/api/resources.ts | 40 ++- ui/api/types.ts | 37 ++- ui/app/[locale]/kafka/ClustersTable.tsx | 88 ++++++ ui/app/[locale]/kafka/[...anyKafka]/route.tsx | 16 -- .../(root)/@activeBreadcrumb/brokers/page.tsx | 3 + .../consumer-groups/page.tsx | 3 + .../schema-registry/page.tsx | 3 + .../(root)/@activeBreadcrumb/topics/page.tsx | 3 + .../(root)/KafkaSelectorBreadcrumbItem.tsx | 6 +- .../[kafkaId]/(root)/brokers/BrokersTable.tsx | 65 +++++ .../kafka/[kafkaId]/(root)/brokers/page.tsx | 20 +- .../[kafkaId]/(root)/consumer-groups/page.tsx | 3 + .../kafka/[kafkaId]/(root)/layout.tsx | 33 ++- .../[kafkaId]/(root)/schema-registry/page.tsx | 3 + .../[locale]/kafka/[kafkaId]/kafka.params.ts | 3 + ui/app/[locale]/kafka/[kafkaId]/page.tsx | 13 +- .../topics/{[topic] => [topicId]}/layout.tsx | 50 ++-- .../{[topic] => [topicId]}/messages/page.tsx | 18 +- .../kafka/[kafkaId]/topics/[topicId]/page.tsx | 6 + .../[topicId]/partitions/TopicDashboard.tsx | 167 +++++++++++ .../topics/[topicId]/partitions/page.tsx | 16 ++ .../{[topic] => [topicId]}/template.tsx | 0 .../[topic]/overview/TopicDashboard.tsx | 266 ------------------ .../topics/[topic]/overview/page.tsx | 18 -- .../kafka/[kafkaId]/topics/[topic]/page.tsx | 9 - .../[kafkaId]/topics/kafkaTopic.params.ts | 1 + ui/app/[locale]/kafka/page.tsx | 22 +- .../[locale]/resources/create/kafka/page.tsx | 4 +- .../resources/create/kafka/validate/Step3.tsx | 4 +- .../resources/create/kafka/validate/page.tsx | 4 +- ui/components/Bytes.tsx | 6 + ui/components/Number.tsx | 4 +- ui/components/clusterCard.tsx | 4 +- 34 files changed, 553 insertions(+), 427 deletions(-) create mode 100644 ui/api/kafka.ts create mode 100644 ui/app/[locale]/kafka/ClustersTable.tsx delete mode 100644 ui/app/[locale]/kafka/[...anyKafka]/route.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/brokers/page.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/consumer-groups/page.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/schema-registry/page.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/topics/page.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/(root)/brokers/BrokersTable.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/(root)/consumer-groups/page.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/(root)/schema-registry/page.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/kafka.params.ts rename ui/app/[locale]/kafka/[kafkaId]/topics/{[topic] => [topicId]}/layout.tsx (65%) rename ui/app/[locale]/kafka/[kafkaId]/topics/{[topic] => [topicId]}/messages/page.tsx (79%) create mode 100644 ui/app/[locale]/kafka/[kafkaId]/topics/[topicId]/page.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/topics/[topicId]/partitions/TopicDashboard.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/topics/[topicId]/partitions/page.tsx rename ui/app/[locale]/kafka/[kafkaId]/topics/{[topic] => [topicId]}/template.tsx (100%) delete mode 100644 ui/app/[locale]/kafka/[kafkaId]/topics/[topic]/overview/TopicDashboard.tsx delete mode 100644 ui/app/[locale]/kafka/[kafkaId]/topics/[topic]/overview/page.tsx delete mode 100644 ui/app/[locale]/kafka/[kafkaId]/topics/[topic]/page.tsx create mode 100644 ui/app/[locale]/kafka/[kafkaId]/topics/kafkaTopic.params.ts create mode 100644 ui/components/Bytes.tsx diff --git a/ui/api/kafka.ts b/ui/api/kafka.ts new file mode 100644 index 000000000..adafe73ea --- /dev/null +++ b/ui/api/kafka.ts @@ -0,0 +1,42 @@ +import { + ClusterDetail, + ClusterList, + ClusterResponse, + ClustersResponse, +} from "@/api/types"; + +export async function getKafkaClusters(): Promise { + const url = `${process.env.BACKEND_URL}/api/kafkas?fields%5Bkafkas%5D=name,bootstrapServers,authType`; + try { + const res = await fetch(url, { + headers: { + Accept: "application/json", + }, + cache: "no-store", + }); + const rawData = await res.json(); + return ClustersResponse.parse(rawData).data; + } catch (err) { + console.error(err); + throw err; + } +} + +export async function getKafkaCluster( + clusterId: string, +): Promise { + const url = `${process.env.BACKEND_URL}/api/kafkas/${clusterId}/?fields%5Bkafkas%5D=name,namespace,creationTimestamp,nodes,controller,authorizedOperations,bootstrapServers,authType`; + try { + const res = await fetch(url, { + headers: { + Accept: "application/json", + }, + cache: "no-store", + }); + const rawData = await res.json(); + return ClusterResponse.parse(rawData).data; + } catch (err) { + console.error(err); + throw err; + } +} diff --git a/ui/api/resources.ts b/ui/api/resources.ts index f8e00b475..1d2221269 100644 --- a/ui/api/resources.ts +++ b/ui/api/resources.ts @@ -1,12 +1,12 @@ "use server"; +import { getKafkaClusters } from "@/api/kafka"; import { - Cluster, + ClusterList, KafkaResource, RegistryResource, Resource, ResourceTypeKafka, ResourceTypeRegistry, - Response, } from "@/api/types"; import { getSession, setSession } from "@/utils/session"; @@ -22,7 +22,21 @@ export async function getResources(scope: unknown): Promise { const { resources } = await getSession("resources"); switch (scope) { case "kafka": - return resources.filter((b) => b.type === "kafka"); + return [ + ...(await getKafkaClusters()).map((c) => ({ + id: c.id, + type: "kafka", + attributes: { + name: c.attributes.name, + cluster: c, + source: "auto", + bootstrapServer: c.attributes.bootstrapServers, + principal: "automatic", + mechanism: "automatic", + }, + })), + ...resources.filter((b) => b.type === "kafka"), + ]; case "registry": return resources.filter((b) => b.type === "registry"); } @@ -69,7 +83,7 @@ export async function createKafkaResource({ bootstrapServer: string; principal: string; name: string; - cluster: Cluster | undefined; + cluster: ClusterList | undefined; }) { const session = await getSession("resources"); const resources = (session?.resources || []) as Resource[]; @@ -82,6 +96,7 @@ export async function createKafkaResource({ name, bootstrapServer, principal, + source: "user", }, }; const newAuthProfiles = [...resources, newProfile]; @@ -91,20 +106,3 @@ export async function createKafkaResource({ }); return newProfile; } - -export async function getClusters(): Promise { - const url = `${process.env.BACKEND_URL}/api/kafkas?fields%5Bkafkas%5D=name,bootstrapServers,authType`; - try { - const res = await fetch(url, { - headers: { - Accept: "application/json", - }, - cache: "no-store", - }); - const rawData = await res.json(); - return Response.parse(rawData).data; - } catch (err) { - console.error(err); - throw err; - } -} diff --git a/ui/api/types.ts b/ui/api/types.ts index 73a4e80d4..33ddd9517 100644 --- a/ui/api/types.ts +++ b/ui/api/types.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -const ClusterSchema = z.object({ +const ClusterListSchema = z.object({ id: z.string(), type: z.string(), attributes: z.object({ @@ -8,10 +8,36 @@ const ClusterSchema = z.object({ bootstrapServers: z.string(), }), }); -export const Response = z.object({ - data: z.array(ClusterSchema), +export const ClustersResponse = z.object({ + data: z.array(ClusterListSchema), }); -export type Cluster = z.infer; +export type ClusterList = z.infer; + +const NodeSchema = z.object({ + id: z.number(), + host: z.string(), + port: z.number(), + rack: z.string().optional(), +}); +export type KafkaNode = z.infer; +const ClusterDetailSchema = z.object({ + id: z.string(), + type: z.string(), + attributes: z.object({ + name: z.string(), + namespace: z.string(), + creationTimestamp: z.string(), + nodes: z.array(NodeSchema), + controller: NodeSchema, + authorizedOperations: z.array(z.string()), + bootstrapServers: z.string(), + authType: z.string(), + }), +}); +export const ClusterResponse = z.object({ + data: ClusterDetailSchema, +}); +export type ClusterDetail = z.infer; export const ResourceTypeRegistry = "registry" as const; export const ResourceTypeKafka = "kafka" as const; @@ -22,8 +48,9 @@ export const KafkaResourceSchema = z.object({ name: z.string(), bootstrapServer: z.string(), principal: z.string(), - cluster: ClusterSchema.optional(), + cluster: ClusterListSchema.optional(), mechanism: z.string(), + source: z.union([z.literal("user"), z.literal("auto")]), }), }); export type KafkaResource = z.infer; diff --git a/ui/app/[locale]/kafka/ClustersTable.tsx b/ui/app/[locale]/kafka/ClustersTable.tsx new file mode 100644 index 000000000..a61ed09b1 --- /dev/null +++ b/ui/app/[locale]/kafka/ClustersTable.tsx @@ -0,0 +1,88 @@ +"use client"; + +import { KafkaResource } from "@/api/types"; +import { ResponsiveTable } from "@/components/table"; +import { ClipboardCopy, Label, LabelGroup } from "@patternfly/react-core"; +import { + DataSourceIcon, + SecurityIcon, + UserIcon, +} from "@patternfly/react-icons"; +import Link from "next/link"; + +const columns = ["name", "source", "bootstrapUrl", "authentication"] as const; + +export function ClustersTable({ + clusters = [], +}: { + clusters: KafkaResource[] | undefined; +}) { + return ( + { + switch (column) { + case "name": + return Name; + case "bootstrapUrl": + return Bootstrap url; + case "source": + return Source; + case "authentication": + return Authentication; + } + }} + renderCell={({ column, row, Td }) => { + switch (column) { + case "name": + return ( + + {row.attributes.name} + + ); + case "bootstrapUrl": + return ( + + + {row.attributes.bootstrapServer} + + + ); + case "source": + return ( + + + + ); + case "authentication": + return ( + + {row.attributes.source === "auto" ? ( + + ) : ( + + + + + )} + + ); + } + }} + /> + ); +} diff --git a/ui/app/[locale]/kafka/[...anyKafka]/route.tsx b/ui/app/[locale]/kafka/[...anyKafka]/route.tsx deleted file mode 100644 index 55094c3f4..000000000 --- a/ui/app/[locale]/kafka/[...anyKafka]/route.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { getResource } from "@/api/resources"; -import { setSession } from "@/utils/session"; -import { NextRequest, NextResponse } from "next/server"; - -export async function GET( - _request: NextRequest, - route: { params: { kafkaId: string } }, -) { - const cluster = await getResource(route.params.kafkaId, "kafka"); - if (cluster?.attributes.cluster) { - await setSession("kafka", { - lastUsed: cluster, - }); - } - return NextResponse.next(); -} diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/brokers/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/brokers/page.tsx new file mode 100644 index 000000000..67301f07b --- /dev/null +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/brokers/page.tsx @@ -0,0 +1,3 @@ +export default function BrokersActiveBreadcrumb() { + return "Brokers"; +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/consumer-groups/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/consumer-groups/page.tsx new file mode 100644 index 000000000..fe4d998c7 --- /dev/null +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/consumer-groups/page.tsx @@ -0,0 +1,3 @@ +export default function ConsumerGroupsActiveBreadcrumb() { + return "Consumer groups"; +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/schema-registry/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/schema-registry/page.tsx new file mode 100644 index 000000000..cbd547dc8 --- /dev/null +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/schema-registry/page.tsx @@ -0,0 +1,3 @@ +export default function SchemaRegistryActiveBreadcrumb() { + return "Schema registry"; +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/topics/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/topics/page.tsx new file mode 100644 index 000000000..116f375ff --- /dev/null +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/@activeBreadcrumb/topics/page.tsx @@ -0,0 +1,3 @@ +export default function TopicsActiveBreadcrumb() { + return "Topics"; +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/KafkaSelectorBreadcrumbItem.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/KafkaSelectorBreadcrumbItem.tsx index 12e5e8675..65f130377 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/(root)/KafkaSelectorBreadcrumbItem.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/KafkaSelectorBreadcrumbItem.tsx @@ -1,5 +1,5 @@ "use client"; -import { KafkaResource } from "@/api/types"; +import { ClusterDetail, KafkaResource } from "@/api/types"; import { Divider, Dropdown, @@ -18,7 +18,7 @@ export const KafkaSelectorBreadcrumbItem = ({ clusters, isActive = false, }: { - selected: KafkaResource; + selected: ClusterDetail; clusters: KafkaResource[]; isActive?: boolean; }) => { @@ -33,7 +33,7 @@ export const KafkaSelectorBreadcrumbItem = ({ const resourceToDropdownItem = (b: KafkaResource) => ( { setIsOpen(false); diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/brokers/BrokersTable.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/brokers/BrokersTable.tsx new file mode 100644 index 000000000..07380333c --- /dev/null +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/brokers/BrokersTable.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { KafkaNode } from "@/api/types"; +import { ResponsiveTable } from "@/components/table"; +import { ClipboardCopy, Label, LabelGroup } from "@patternfly/react-core"; +import { ServerIcon } from "@patternfly/react-icons"; + +const columns = ["id", "host", "rack"] as const; + +export function BrokersTable({ + brokers, + controller, +}: { + brokers: KafkaNode[]; + controller: KafkaNode; +}) { + return ( + { + switch (column) { + case "id": + return Id; + case "host": + return Host; + case "rack": + return Rack; + } + }} + renderCell={({ column, row, Td }) => { + switch (column) { + case "id": + return ( + + + + {row.id === controller.id && ( + + )} + + + ); + case "host": + return ( + + + {row.host}:{row.port} + + + ); + case "rack": + return {row.rack || "-"}; + } + }} + /> + ); +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/brokers/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/brokers/page.tsx index 3fa8ac97c..ba6213845 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/(root)/brokers/page.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/brokers/page.tsx @@ -1,12 +1,16 @@ -import { PageSection, Title } from "@/libs/patternfly/react-core"; +import { getKafkaCluster } from "@/api/kafka"; +import { BrokersTable } from "@/app/[locale]/kafka/[kafkaId]/(root)/brokers/BrokersTable"; +import { KafkaParams } from "@/app/[locale]/kafka/[kafkaId]/kafka.params"; +import { PageSection } from "@/libs/patternfly/react-core"; -export default function BrokersPage() { +export default async function BrokersPage({ params }: { params: KafkaParams }) { + const cluster = await getKafkaCluster(params.kafkaId); return ( - <> - - Brokers - - lorem - + + + ); } diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/consumer-groups/page.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/consumer-groups/page.tsx new file mode 100644 index 000000000..8f6bdde6f --- /dev/null +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/consumer-groups/page.tsx @@ -0,0 +1,3 @@ +export default function ConsumerGroupsPage() { + return
TBD
; +} diff --git a/ui/app/[locale]/kafka/[kafkaId]/(root)/layout.tsx b/ui/app/[locale]/kafka/[kafkaId]/(root)/layout.tsx index 5a7509e55..e9bed9189 100644 --- a/ui/app/[locale]/kafka/[kafkaId]/(root)/layout.tsx +++ b/ui/app/[locale]/kafka/[kafkaId]/(root)/layout.tsx @@ -1,4 +1,6 @@ -import { getResource, getResources } from "@/api/resources"; +import { getKafkaCluster } from "@/api/kafka"; +import { getResources } from "@/api/resources"; +import { KafkaParams } from "@/app/[locale]/kafka/[kafkaId]/kafka.params"; import { BreadcrumbLink } from "@/components/BreadcrumbLink"; import { Loading } from "@/components/Loading"; import { NavItemLink } from "@/components/NavItemLink"; @@ -12,20 +14,19 @@ import { PageGroup, PageNavigation, PageSection, - Text, - TextContent, Title, } from "@/libs/patternfly/react-core"; import { notFound } from "next/navigation"; -import { PropsWithChildren, Suspense } from "react"; +import { PropsWithChildren, ReactNode, Suspense } from "react"; import { KafkaSelectorBreadcrumbItem } from "./KafkaSelectorBreadcrumbItem"; export default async function KafkaLayout({ children, + activeBreadcrumb, params: { kafkaId }, -}: PropsWithChildren<{ params: { kafkaId: string } }>) { - const cluster = await getResource(kafkaId, "kafka"); - if (!cluster || !cluster.attributes.cluster) { +}: PropsWithChildren<{ params: KafkaParams; activeBreadcrumb: ReactNode }>) { + const cluster = await getKafkaCluster(kafkaId); + if (!cluster) { notFound(); } const clusters = await getResources("kafka"); @@ -36,15 +37,18 @@ export default async function KafkaLayout({ Kafka - + + {activeBreadcrumb && ( + {activeBreadcrumb} + )} {cluster.attributes.name} - - {`${cluster.attributes.principal}@${cluster.attributes.bootstrapServer}`} -