You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@eventmesh.apache.org by xi...@apache.org on 2022/07/27 04:59:01 UTC

[incubator-eventmesh] branch dashboard-dev updated: Apache EventMesh Dashboard - Client Management (#907)

This is an automated email from the ASF dual-hosted git repository.

xiaoyang pushed a commit to branch dashboard-dev
in repository https://gitbox.apache.org/repos/asf/incubator-eventmesh.git


The following commit(s) were added to refs/heads/dashboard-dev by this push:
     new 2c4bcf7d Apache EventMesh Dashboard - Client Management (#907)
2c4bcf7d is described below

commit 2c4bcf7dfc89dadcc169d278b849a58a46408ea6
Author: Xiaoyang Liu <si...@gmail.com>
AuthorDate: Tue Jul 26 21:58:56 2022 -0700

    Apache EventMesh Dashboard - Client Management (#907)
---
 eventmesh-dashboard/.eslintrc.js                   |  53 +++++
 eventmesh-dashboard/.gitignore                     | 125 ++++++++++
 eventmesh-dashboard/README.md                      |  34 +++
 eventmesh-dashboard/components/Sidebar.tsx         | 188 +++++++++++++++
 .../components/client/ClientTable.tsx              | 245 +++++++++++++++++++
 eventmesh-dashboard/components/index/Endpoint.tsx  | 104 ++++++++
 eventmesh-dashboard/next-env.d.ts                  |  24 ++
 eventmesh-dashboard/next.config.js                 |  25 ++
 eventmesh-dashboard/package.json                   |  39 +++
 eventmesh-dashboard/pages/_app.tsx                 |  46 ++++
 eventmesh-dashboard/pages/_document.tsx            |  38 +++
 eventmesh-dashboard/pages/client.tsx               |  33 +++
 eventmesh-dashboard/pages/index.tsx                |  33 +++
 eventmesh-dashboard/public/favicon.ico             | Bin 0 -> 25931 bytes
 eventmesh-dashboard/tsconfig.json                  |  20 ++
 .../admin/controller/ClientManageController.java   |  15 +-
 .../runtime/admin/handler/ClientHandler.java       | 262 +++++++++++++++++++++
 .../runtime/admin/request/DeleteClientRequest.java |  44 ++++
 .../eventmesh/runtime/admin/response/Error.java    |  33 +++
 .../runtime/admin/response/GetClientResponse.java  |  63 +++++
 .../runtime/admin/utils/HttpExchangeUtils.java     |  43 ++++
 .../eventmesh/runtime/admin/utils/JsonUtils.java   |  71 ++++++
 .../eventmesh/runtime/boot/EventMeshServer.java    |   4 +
 .../eventmesh/runtime/boot/EventMeshTCPServer.java |  14 --
 .../protocol/grpc/consumer/ConsumerManager.java    |   4 +
 .../consumergroup/ConsumerGroupClient.java         |  20 +-
 .../protocol/http/consumer/ConsumerManager.java    |   4 +
 27 files changed, 1559 insertions(+), 25 deletions(-)

diff --git a/eventmesh-dashboard/.eslintrc.js b/eventmesh-dashboard/.eslintrc.js
new file mode 100644
index 00000000..120b0073
--- /dev/null
+++ b/eventmesh-dashboard/.eslintrc.js
@@ -0,0 +1,53 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+module.exports = {
+  env: {
+    browser: true,
+    es2021: true,
+  },
+  extends: [
+    'plugin:react/recommended',
+    'airbnb',
+    'airbnb-typescript',
+  ],
+  parser: '@typescript-eslint/parser',
+  parserOptions: {
+    ecmaFeatures: {
+      jsx: true,
+    },
+    ecmaVersion: 13,
+    sourceType: 'module',
+    project: 'tsconfig.json',
+  },
+  plugins: [
+    'react',
+    '@typescript-eslint',
+  ],
+  rules: {
+    'react/react-in-jsx-scope': 'off',
+    'react/function-component-definition': [
+      'error',
+      {
+        namedComponents: 'arrow-function',
+      },
+    ],
+    'no-param-reassign': 'off'
+  },
+};
diff --git a/eventmesh-dashboard/.gitignore b/eventmesh-dashboard/.gitignore
new file mode 100644
index 00000000..119a1e32
--- /dev/null
+++ b/eventmesh-dashboard/.gitignore
@@ -0,0 +1,125 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# TypeScript v1 declaration files
+typings/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+
+# Next.js build output
+.next
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and *not* Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# next.js
+/.next/
+/out/
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# local env files
+.env.local
+.env.development.local
+.env.test.local
+.env.production.local
+
+# vercel
+.vercel
diff --git a/eventmesh-dashboard/README.md b/eventmesh-dashboard/README.md
new file mode 100644
index 00000000..c87e0421
--- /dev/null
+++ b/eventmesh-dashboard/README.md
@@ -0,0 +1,34 @@
+This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
+
+## Getting Started
+
+First, run the development server:
+
+```bash
+npm run dev
+# or
+yarn dev
+```
+
+Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
+
+You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
+
+[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
+
+The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
+
+## Learn More
+
+To learn more about Next.js, take a look at the following resources:
+
+- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
+- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
+
+You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
+
+## Deploy on Vercel
+
+The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
+
+Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
diff --git a/eventmesh-dashboard/components/Sidebar.tsx b/eventmesh-dashboard/components/Sidebar.tsx
new file mode 100644
index 00000000..321207ce
--- /dev/null
+++ b/eventmesh-dashboard/components/Sidebar.tsx
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* eslint-disable react/jsx-props-no-spreading */
+import React, { ReactNode } from 'react';
+import {
+  IconButton,
+  Box,
+  CloseButton,
+  Flex,
+  Icon,
+  useColorModeValue,
+  Link,
+  Drawer,
+  DrawerContent,
+  Text,
+  useDisclosure,
+  BoxProps,
+  FlexProps,
+} from '@chakra-ui/react';
+import {
+  FiList,
+  FiGrid,
+  FiServer,
+  FiDatabase,
+  FiMenu,
+} from 'react-icons/fi';
+import { IconType } from 'react-icons';
+
+interface LinkItemProps {
+  name: string;
+  icon: IconType;
+  href: string;
+}
+
+const LinkItems: Array<LinkItemProps> = [
+  { name: 'Overview', icon: FiList, href: '/' },
+  { name: 'Client', icon: FiServer, href: '/client' },
+  { name: 'Topic', icon: FiGrid, href: '/topic' },
+  { name: 'Event', icon: FiDatabase, href: '/event' },
+];
+
+interface NavItemProps extends FlexProps {
+  icon: IconType;
+  href: string;
+  children: string | number;
+}
+
+const NavItem = ({
+  icon, href, children, ...rest
+}: NavItemProps) => (
+  <Link
+    href={href}
+    style={{ textDecoration: 'none' }}
+    _focus={{ boxShadow: 'none' }}
+  >
+    <Flex
+      align="center"
+      p="4"
+      mx="4"
+      borderRadius="lg"
+      role="group"
+      cursor="pointer"
+      _hover={{
+        bg: 'blue.500',
+        color: 'white',
+      }}
+      {...rest}
+    >
+      {icon && (
+      <Icon
+        mr="4"
+        fontSize="16"
+        _groupHover={{
+          color: 'white',
+        }}
+        as={icon}
+      />
+      )}
+      {children}
+    </Flex>
+  </Link>
+);
+
+interface SidebarProps extends BoxProps {
+  onClose: () => void;
+}
+
+const SidebarContent = ({ onClose, ...rest }: SidebarProps) => (
+  <Box
+    bg={useColorModeValue('white', 'gray.900')}
+    borderRight="1px"
+    borderRightColor={useColorModeValue('gray.200', 'gray.700')}
+    w={{ base: 'full', md: 60 }}
+    pos="fixed"
+    h="full"
+    {...rest}
+  >
+    <Flex h="20" alignItems="center" mx="8" justifyContent="space-between">
+      <Text fontSize="2xl" fontWeight="bold">
+        EventMesh
+      </Text>
+      <CloseButton display={{ base: 'flex', md: 'none' }} onClick={onClose} />
+    </Flex>
+    {LinkItems.map((link) => (
+      <NavItem key={link.name} href={link.href} icon={link.icon}>
+        {link.name}
+      </NavItem>
+    ))}
+  </Box>
+);
+
+interface MobileProps extends FlexProps {
+  onOpen: () => void;
+}
+
+const MobileNav = ({ onOpen }: MobileProps) => (
+  <Flex
+    ml={{ base: 0, md: 60 }}
+    px={{ base: 4, md: 24 }}
+    height="20"
+    alignItems="center"
+    bg={useColorModeValue('white', 'gray.900')}
+    borderBottomWidth="1px"
+    borderBottomColor={useColorModeValue('gray.200', 'gray.700')}
+    justifyContent="flex-start"
+    display={{ base: 'flex', md: 'none' }}
+  >
+    <IconButton
+      variant="outline"
+      onClick={onOpen}
+      aria-label="open menu"
+      icon={<FiMenu />}
+    />
+
+    <Text fontSize="2xl" ml="8" fontWeight="bold">
+      EventMesh
+    </Text>
+  </Flex>
+);
+
+const Sidebar = ({ children }: { children: ReactNode }) => {
+  const { isOpen, onOpen, onClose } = useDisclosure();
+  return (
+    <Box minH="100vh" bg={useColorModeValue('gray.100', 'gray.900')}>
+      <SidebarContent
+        onClose={() => onClose}
+        display={{ base: 'none', md: 'block' }}
+      />
+      <Drawer
+        autoFocus={false}
+        isOpen={isOpen}
+        placement="left"
+        onClose={onClose}
+        returnFocusOnClose={false}
+        onOverlayClick={onClose}
+        size="full"
+      >
+        <DrawerContent>
+          <SidebarContent onClose={onClose} />
+        </DrawerContent>
+      </Drawer>
+
+      <MobileNav onOpen={onOpen} />
+      <Box ml={{ base: 0, md: 60 }} p="4">
+        {children}
+      </Box>
+    </Box>
+  );
+};
+
+export default Sidebar;
diff --git a/eventmesh-dashboard/components/client/ClientTable.tsx b/eventmesh-dashboard/components/client/ClientTable.tsx
new file mode 100644
index 00000000..9a82fbf3
--- /dev/null
+++ b/eventmesh-dashboard/components/client/ClientTable.tsx
@@ -0,0 +1,245 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  HStack,
+  Select,
+  Input,
+  Table,
+  Thead,
+  Tbody,
+  Tr,
+  Th,
+  Td,
+  TableContainer,
+  useToast,
+  Box,
+  Button,
+} from '@chakra-ui/react';
+import axios from 'axios';
+import { useEffect, useState } from 'react';
+
+interface Client {
+  env: string,
+  subsystem: string,
+  url: string,
+  pid: number,
+  host: string,
+  port: number,
+  version: string,
+  idc: string,
+  group: string,
+  purpose: string,
+  protocol: string,
+}
+
+interface ClientProps {
+  host: string,
+  port: number,
+  group: string,
+  protocol: string,
+  url: string,
+}
+
+interface RemoveClientRequest {
+  host: string,
+  port: number,
+  protocol: string,
+  url: string,
+}
+
+const ClientRow = ({
+  host, port, group, protocol, url,
+}: ClientProps) => {
+  const toast = useToast();
+  const [loading, setLoading] = useState(false);
+  const onRemoveClick = async () => {
+    try {
+      setLoading(true);
+      await axios.delete<RemoveClientRequest>('/client', {
+        data: {
+          host,
+          port,
+          protocol,
+          url,
+        },
+      });
+      setLoading(false);
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        toast({
+          title: 'Failed to remove the client',
+          description: error.message,
+          status: 'error',
+          duration: 3000,
+          isClosable: true,
+        });
+      }
+    }
+  };
+
+  return (
+    <Tr>
+      {protocol === 'TCP' && <Td>{`${host}:${port}`}</Td>}
+      {(protocol === 'HTTP' || protocol === 'gRPC') && <Td>{url}</Td>}
+      <Td>{group}</Td>
+      <Td>{protocol}</Td>
+      <Td>
+        <HStack>
+          <Button
+            isDisabled
+            colorScheme="red"
+            isLoading={loading}
+            onClick={onRemoveClick}
+          >
+            Remove
+          </Button>
+        </HStack>
+      </Td>
+    </Tr>
+  );
+};
+
+const ClientTable = () => {
+  const [searchInput, setSearchInput] = useState<string>('');
+  const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+    setSearchInput(event.currentTarget.value);
+  };
+
+  const [protocolFilter, setProtocolFilter] = useState<string>('');
+  const handleProtocolSelectChange = (event: React.FormEvent<HTMLSelectElement>) => {
+    setProtocolFilter(event.currentTarget.value);
+  };
+
+  const [groupSet, setGroupSet] = useState<Set<string>>(new Set());
+  const [groupFilter, setGroupFilter] = useState<string>('');
+  const handleGroupSelectChange = (event: React.FormEvent<HTMLSelectElement>) => {
+    setGroupFilter(event.currentTarget.value);
+  };
+
+  const [clientList, setClientList] = useState<Client[]>([]);
+  const toast = useToast();
+  useEffect(() => {
+    const fetch = async () => {
+      try {
+        const { data } = await axios.get<Client[]>('/client');
+        setClientList(data);
+
+        const nextGroupSet = new Set<string>();
+        data.forEach(({ group }) => {
+          nextGroupSet.add(group);
+        });
+        setGroupSet(nextGroupSet);
+      } catch (error) {
+        if (axios.isAxiosError(error)) {
+          toast({
+            title: 'Failed to fetch the list of clients',
+            description: error.message,
+            status: 'error',
+            duration: 3000,
+            isClosable: true,
+          });
+          setClientList([]);
+        }
+      }
+    };
+
+    fetch();
+  }, []);
+
+  return (
+    <Box
+      maxW="full"
+      bg="white"
+      borderWidth="1px"
+      borderRadius="md"
+      overflow="hidden"
+      p="4"
+    >
+      <HStack
+        spacing="2"
+      >
+        <Input
+          w="200%"
+          placeholder="Search"
+          value={searchInput}
+          onChange={handleSearchInputChange}
+        />
+        <Select
+          placeholder="Select Protocol"
+          onChange={handleProtocolSelectChange}
+        >
+          <option value="TCP">TCP</option>
+          <option value="HTTP">HTTP</option>
+          <option value="gRPC">gRPC</option>
+        </Select>
+        <Select
+          placeholder="Select Group"
+          onChange={handleGroupSelectChange}
+        >
+          {Array.from(groupSet).map((group) => (
+            <option value={group} key={group}>{group}</option>
+          ))}
+        </Select>
+      </HStack>
+
+      <TableContainer>
+        <Table variant="simple">
+          <Thead>
+            <Tr>
+              <Th>Host or url</Th>
+              <Th>Group</Th>
+              <Th>Protocol</Th>
+              <Th>Action</Th>
+            </Tr>
+          </Thead>
+          <Tbody>
+            {clientList && clientList.filter(({
+              host, port, group, protocol,
+            }) => {
+              const address = `${host}:${port}`;
+              if (searchInput && !address.includes(searchInput)) {
+                return false;
+              }
+              if (groupFilter && groupFilter !== group) {
+                return false;
+              }
+              if (protocolFilter && protocolFilter !== protocol) {
+                return false;
+              }
+              return true;
+            }).map(({
+              host, port, group, protocol, url,
+            }) => (
+              <ClientRow
+                host={host}
+                port={port}
+                group={group}
+                protocol={protocol}
+                url={url}
+              />
+            ))}
+          </Tbody>
+        </Table>
+      </TableContainer>
+    </Box>
+  );
+};
+
+export default ClientTable;
diff --git a/eventmesh-dashboard/components/index/Endpoint.tsx b/eventmesh-dashboard/components/index/Endpoint.tsx
new file mode 100644
index 00000000..6aa45a17
--- /dev/null
+++ b/eventmesh-dashboard/components/index/Endpoint.tsx
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import {
+  HStack,
+  Input,
+  VStack,
+  Button,
+  Text,
+  useToast,
+} from '@chakra-ui/react';
+import axios from 'axios';
+import React, { useEffect, useState } from 'react';
+
+const Endpoint = () => {
+  const toast = useToast();
+  const [endpointInput, setEndpointInput] = useState('http://localhost:10106');
+  const [loading, setLoading] = useState(false);
+
+  useEffect(() => {
+    const endpoint = localStorage.getItem('endpoint');
+    if (endpoint === null) {
+      return;
+    }
+    setEndpointInput(endpoint);
+    axios.defaults.baseURL = endpoint;
+  }, []);
+
+  const handleEndpointInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+    setEndpointInput(event.currentTarget.value);
+  };
+
+  const handleSaveButtonClick = async () => {
+    try {
+      setLoading(true);
+      await axios.get(`${endpointInput}/client`);
+      axios.defaults.baseURL = endpointInput;
+      localStorage.setItem('endpoint', endpointInput);
+    } catch (error) {
+      if (axios.isAxiosError(error)) {
+        toast({
+          title: `Failed to connect to ${endpointInput}`,
+          description: error.message,
+          status: 'error',
+          duration: 3000,
+          isClosable: true,
+        });
+      }
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <VStack
+      maxW="full"
+      bg="white"
+      borderWidth="1px"
+      borderRadius="md"
+      overflow="hidden"
+      p="4"
+    >
+      <Text
+        w="full"
+      >
+        EventMesh Admin Endpoint
+      </Text>
+      <HStack
+        w="full"
+      >
+        <Input
+          placeholder="Apache EventMesh Backend Endpoint"
+          value={endpointInput}
+          onChange={handleEndpointInputChange}
+        />
+        <Button
+          colorScheme="blue"
+          isLoading={loading}
+          onClick={handleSaveButtonClick}
+        >
+          Save
+        </Button>
+      </HStack>
+    </VStack>
+  );
+};
+
+export default Endpoint;
diff --git a/eventmesh-dashboard/next-env.d.ts b/eventmesh-dashboard/next-env.d.ts
new file mode 100644
index 00000000..f13f10eb
--- /dev/null
+++ b/eventmesh-dashboard/next-env.d.ts
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/// <reference types="next" />
+/// <reference types="next/image-types/global" />
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/basic-features/typescript for more information.
diff --git a/eventmesh-dashboard/next.config.js b/eventmesh-dashboard/next.config.js
new file mode 100644
index 00000000..b0f480c7
--- /dev/null
+++ b/eventmesh-dashboard/next.config.js
@@ -0,0 +1,25 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/** @type {import('next').NextConfig} */
+const nextConfig = {
+  reactStrictMode: true,
+}
+
+module.exports = nextConfig
diff --git a/eventmesh-dashboard/package.json b/eventmesh-dashboard/package.json
new file mode 100644
index 00000000..1934a208
--- /dev/null
+++ b/eventmesh-dashboard/package.json
@@ -0,0 +1,39 @@
+{
+  "name": "eventmesh-dashboard",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev",
+    "build": "next build",
+    "start": "next start",
+    "lint": "eslint . --cache --fix --ext .ts,.tsx"
+  },
+  "dependencies": {
+    "@chakra-ui/react": "^2.1.2",
+    "@emotion/react": "^11.9.0",
+    "@emotion/styled": "^11.8.1",
+    "@fontsource/inter": "^4.5.10",
+    "axios": "^0.27.2",
+    "framer-motion": "^6.3.6",
+    "next": "12.1.6",
+    "react": "18.1.0",
+    "react-dom": "18.1.0",
+    "react-icons": "^4.4.0",
+    "swr": "^1.3.0"
+  },
+  "devDependencies": {
+    "@types/node": "17.0.38",
+    "@types/react": "18.0.10",
+    "@types/react-dom": "18.0.5",
+    "@typescript-eslint/eslint-plugin": "^5.4.0",
+    "@typescript-eslint/parser": "^5.4.0",
+    "eslint": "8.16.0",
+    "eslint-config-airbnb": "^19.0.1",
+    "eslint-config-airbnb-typescript": "^17.0.0",
+    "eslint-plugin-import": "^2.25.3",
+    "eslint-plugin-jsx-a11y": "^6.5.1",
+    "eslint-plugin-react": "^7.27.1",
+    "eslint-plugin-react-hooks": "^4.3.0",
+    "typescript": "4.7.2"
+  }
+}
diff --git a/eventmesh-dashboard/pages/_app.tsx b/eventmesh-dashboard/pages/_app.tsx
new file mode 100644
index 00000000..d76c7e3d
--- /dev/null
+++ b/eventmesh-dashboard/pages/_app.tsx
@@ -0,0 +1,46 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/* eslint-disable react/jsx-props-no-spreading */
+import '@fontsource/inter';
+import { ChakraProvider, extendTheme } from '@chakra-ui/react';
+import axios from 'axios';
+import type { AppProps } from 'next/app';
+import Sidebar from '../components/Sidebar';
+
+axios.defaults.baseURL = 'http://localhost:10106';
+
+const theme = extendTheme({
+  initialColorMode: 'light',
+  useSystemColorMode: true,
+  fonts: {
+    heading: 'Inter, sans-serif',
+    body: 'Inter, sans-serif',
+  },
+});
+
+const Application = ({ Component, pageProps }: AppProps) => (
+  <ChakraProvider theme={theme}>
+    <Sidebar>
+      <Component {...pageProps} />
+    </Sidebar>
+  </ChakraProvider>
+);
+
+export default Application;
diff --git a/eventmesh-dashboard/pages/_document.tsx b/eventmesh-dashboard/pages/_document.tsx
new file mode 100644
index 00000000..dd32fc0c
--- /dev/null
+++ b/eventmesh-dashboard/pages/_document.tsx
@@ -0,0 +1,38 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { ColorModeScript } from '@chakra-ui/react';
+import NextDocument, {
+  Html, Head, Main, NextScript,
+} from 'next/document';
+
+export default class Document extends NextDocument {
+  render() {
+    return (
+      <Html lang="en">
+        <Head />
+        <body>
+          <ColorModeScript />
+          <Main />
+          <NextScript />
+        </body>
+      </Html>
+    );
+  }
+}
diff --git a/eventmesh-dashboard/pages/client.tsx b/eventmesh-dashboard/pages/client.tsx
new file mode 100644
index 00000000..bf7aba10
--- /dev/null
+++ b/eventmesh-dashboard/pages/client.tsx
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import Head from 'next/head';
+import type { NextPage } from 'next';
+import ClientTable from '../components/client/ClientTable';
+
+const Client: NextPage = () => (
+  <>
+    <Head>
+      <title>Client | Apache EventMesh Dashboard</title>
+    </Head>
+    <ClientTable />
+  </>
+);
+
+export default Client;
diff --git a/eventmesh-dashboard/pages/index.tsx b/eventmesh-dashboard/pages/index.tsx
new file mode 100644
index 00000000..52961dcc
--- /dev/null
+++ b/eventmesh-dashboard/pages/index.tsx
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import Head from 'next/head';
+import type { NextPage } from 'next';
+import Endpoint from '../components/index/Endpoint';
+
+const Index: NextPage = () => (
+  <>
+    <Head>
+      <title>Apache EventMesh Dashboard</title>
+    </Head>
+    <Endpoint />
+  </>
+);
+
+export default Index;
diff --git a/eventmesh-dashboard/public/favicon.ico b/eventmesh-dashboard/public/favicon.ico
new file mode 100644
index 00000000..718d6fea
Binary files /dev/null and b/eventmesh-dashboard/public/favicon.ico differ
diff --git a/eventmesh-dashboard/tsconfig.json b/eventmesh-dashboard/tsconfig.json
new file mode 100644
index 00000000..99710e85
--- /dev/null
+++ b/eventmesh-dashboard/tsconfig.json
@@ -0,0 +1,20 @@
+{
+  "compilerOptions": {
+    "target": "es5",
+    "lib": ["dom", "dom.iterable", "esnext"],
+    "allowJs": true,
+    "skipLibCheck": true,
+    "strict": true,
+    "forceConsistentCasingInFileNames": true,
+    "noEmit": true,
+    "esModuleInterop": true,
+    "module": "esnext",
+    "moduleResolution": "node",
+    "resolveJsonModule": true,
+    "isolatedModules": true,
+    "jsx": "preserve",
+    "incremental": true
+  },
+  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
+  "exclude": ["node_modules"]
+}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/controller/ClientManageController.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/controller/ClientManageController.java
index 1c951276..ecb6cc4f 100644
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/controller/ClientManageController.java
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/controller/ClientManageController.java
@@ -18,6 +18,7 @@
 package org.apache.eventmesh.runtime.admin.controller;
 
 import org.apache.eventmesh.admin.rocketmq.controller.AdminController;
+import org.apache.eventmesh.runtime.admin.handler.ClientHandler;
 import org.apache.eventmesh.runtime.admin.handler.QueryRecommendEventMeshHandler;
 import org.apache.eventmesh.runtime.admin.handler.RedirectClientByIpPortHandler;
 import org.apache.eventmesh.runtime.admin.handler.RedirectClientByPathHandler;
@@ -28,6 +29,8 @@ import org.apache.eventmesh.runtime.admin.handler.RejectClientBySubSystemHandler
 import org.apache.eventmesh.runtime.admin.handler.ShowClientBySystemHandler;
 import org.apache.eventmesh.runtime.admin.handler.ShowClientHandler;
 import org.apache.eventmesh.runtime.admin.handler.ShowListenClientByTopicHandler;
+import org.apache.eventmesh.runtime.boot.EventMeshGrpcServer;
+import org.apache.eventmesh.runtime.boot.EventMeshHTTPServer;
 import org.apache.eventmesh.runtime.boot.EventMeshTCPServer;
 
 import java.io.IOException;
@@ -43,11 +46,19 @@ public class ClientManageController {
     private static final Logger logger = LoggerFactory.getLogger(ClientManageController.class);
 
     private EventMeshTCPServer eventMeshTCPServer;
+    private final EventMeshHTTPServer eventMeshHTTPServer;
+    private final EventMeshGrpcServer eventMeshGrpcServer;
 
     private AdminController adminController;
 
-    public ClientManageController(EventMeshTCPServer eventMeshTCPServer) {
+    public ClientManageController(
+            EventMeshTCPServer eventMeshTCPServer,
+            EventMeshHTTPServer eventMeshHTTPServer,
+            EventMeshGrpcServer eventMeshGrpcServer
+    ) {
         this.eventMeshTCPServer = eventMeshTCPServer;
+        this.eventMeshHTTPServer = eventMeshHTTPServer;
+        this.eventMeshGrpcServer = eventMeshGrpcServer;
     }
 
     public void start() throws IOException {
@@ -64,6 +75,8 @@ public class ClientManageController {
         server.createContext("/clientManage/showListenClientByTopic", new ShowListenClientByTopicHandler(eventMeshTCPServer));
         server.createContext("/eventMesh/recommend", new QueryRecommendEventMeshHandler(eventMeshTCPServer));
 
+        server.createContext("/client", new ClientHandler(eventMeshTCPServer, eventMeshHTTPServer, eventMeshGrpcServer));
+
         adminController = new AdminController();
         adminController.run(server);
 
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/handler/ClientHandler.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/handler/ClientHandler.java
new file mode 100644
index 00000000..a317c961
--- /dev/null
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/handler/ClientHandler.java
@@ -0,0 +1,262 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.eventmesh.runtime.admin.handler;
+
+import org.apache.eventmesh.common.protocol.tcp.UserAgent;
+import org.apache.eventmesh.runtime.admin.request.DeleteClientRequest;
+import org.apache.eventmesh.runtime.admin.response.Error;
+import org.apache.eventmesh.runtime.admin.response.GetClientResponse;
+import org.apache.eventmesh.runtime.admin.utils.HttpExchangeUtils;
+import org.apache.eventmesh.runtime.admin.utils.JsonUtils;
+import org.apache.eventmesh.runtime.boot.EventMeshGrpcServer;
+import org.apache.eventmesh.runtime.boot.EventMeshHTTPServer;
+import org.apache.eventmesh.runtime.boot.EventMeshTCPServer;
+import org.apache.eventmesh.runtime.core.protocol.grpc.consumer.ConsumerManager;
+import org.apache.eventmesh.runtime.core.protocol.grpc.consumer.consumergroup.ConsumerGroupClient;
+import org.apache.eventmesh.runtime.core.protocol.http.processor.inf.Client;
+import org.apache.eventmesh.runtime.core.protocol.tcp.client.EventMeshTcp2Client;
+import org.apache.eventmesh.runtime.core.protocol.tcp.client.group.ClientSessionGroupMapping;
+import org.apache.eventmesh.runtime.core.protocol.tcp.client.session.Session;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.sun.net.httpserver.HttpExchange;
+import com.sun.net.httpserver.HttpHandler;
+
+/**
+ * The client handler
+ */
+public class ClientHandler implements HttpHandler {
+    private static final Logger logger = LoggerFactory.getLogger(ShowClientHandler.class);
+
+    private final EventMeshTCPServer eventMeshTCPServer;
+    private final EventMeshHTTPServer eventMeshHTTPServer;
+    private final EventMeshGrpcServer eventMeshGrpcServer;
+
+    public ClientHandler(
+            EventMeshTCPServer eventMeshTCPServer,
+            EventMeshHTTPServer eventMeshHTTPServer,
+            EventMeshGrpcServer eventMeshGrpcServer
+    ) {
+        this.eventMeshTCPServer = eventMeshTCPServer;
+        this.eventMeshHTTPServer = eventMeshHTTPServer;
+        this.eventMeshGrpcServer = eventMeshGrpcServer;
+    }
+
+    /**
+     * OPTION /client
+     */
+    void preflight(HttpExchange httpExchange) throws IOException {
+        httpExchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
+        httpExchange.getResponseHeaders().add("Access-Control-Allow-Method", "*");
+        httpExchange.getResponseHeaders().add("Access-Control-Max-Age", "86400");
+        httpExchange.sendResponseHeaders(200, 0);
+        OutputStream out = httpExchange.getResponseBody();
+        out.close();
+    }
+
+    /**
+     * DELETE /client
+     */
+    void delete(HttpExchange httpExchange) throws IOException {
+        OutputStream out = httpExchange.getResponseBody();
+        try {
+            String request = HttpExchangeUtils.streamToString(httpExchange.getRequestBody());
+            DeleteClientRequest deleteClientRequest = JsonUtils.toObject(request, DeleteClientRequest.class);
+            String host = deleteClientRequest.host;
+            int port = deleteClientRequest.port;
+            String protocol = deleteClientRequest.protocol;
+            String url = deleteClientRequest.url;
+
+            if (Objects.equals(protocol, "TCP")) {
+                ClientSessionGroupMapping clientSessionGroupMapping = eventMeshTCPServer.getClientSessionGroupMapping();
+                ConcurrentHashMap<InetSocketAddress, Session> sessionMap = clientSessionGroupMapping.getSessionMap();
+                if (!sessionMap.isEmpty()) {
+                    for (Map.Entry<InetSocketAddress, Session> entry : sessionMap.entrySet()) {
+                        if (entry.getKey().getHostString().equals(host) && entry.getKey().getPort() == port) {
+                            EventMeshTcp2Client.serverGoodby2Client(
+                                    eventMeshTCPServer,
+                                    entry.getValue(),
+                                    clientSessionGroupMapping
+                            );
+                        }
+                    }
+                }
+            }
+
+            if (Objects.equals(protocol, "HTTP")) {
+                for (List<Client> clientList : eventMeshHTTPServer.localClientInfoMapping.values()) {
+                    clientList.removeIf(client -> Objects.equals(client.url, url));
+                }
+            }
+
+            if (Objects.equals(protocol, "gRPC")) {
+                ConsumerManager consumerManager = eventMeshGrpcServer.getConsumerManager();
+                Map<String, List<ConsumerGroupClient>> clientTable = consumerManager.getClientTable();
+                for (List<ConsumerGroupClient> clientList : clientTable.values()) {
+                    for (ConsumerGroupClient client : clientList) {
+                        if (Objects.equals(client.getUrl(), url)) {
+                            consumerManager.deregisterClient(client);
+                        }
+                    }
+                }
+            }
+
+            httpExchange.getResponseHeaders().add("Content-Type", "application/json");
+            httpExchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
+            httpExchange.sendResponseHeaders(200, 0);
+        } catch (Exception e) {
+            Error error = new Error(e.toString());
+            String result = JsonUtils.toJson(error);
+            httpExchange.sendResponseHeaders(500, result.getBytes().length);
+            out.write(result.getBytes());
+        } finally {
+            if (out != null) {
+                try {
+                    out.close();
+                } catch (IOException e) {
+                    logger.warn("out close failed...", e);
+                }
+            }
+        }
+    }
+
+    /**
+     * GET /client
+     * Return a response that contains the list of clients
+     */
+    void list(HttpExchange httpExchange) throws IOException {
+        OutputStream out = httpExchange.getResponseBody();
+        httpExchange.getResponseHeaders().add("Content-Type", "application/json");
+        httpExchange.getResponseHeaders().add("Access-Control-Allow-Origin", "*");
+
+        try {
+            // Get the list of TCP clients
+            ClientSessionGroupMapping clientSessionGroupMapping = eventMeshTCPServer.getClientSessionGroupMapping();
+            Map<InetSocketAddress, Session> sessionMap = clientSessionGroupMapping.getSessionMap();
+            List<GetClientResponse> getClientResponseList = new ArrayList<>();
+            for (Session session : sessionMap.values()) {
+                UserAgent userAgent = session.getClient();
+                GetClientResponse getClientResponse = new GetClientResponse(
+                        Optional.ofNullable(userAgent.getEnv()).orElse(""),
+                        Optional.ofNullable(userAgent.getSubsystem()).orElse(""),
+                        Optional.ofNullable(userAgent.getPath()).orElse(""),
+                        String.valueOf(userAgent.getPid()),
+                        Optional.ofNullable(userAgent.getHost()).orElse(""),
+                        userAgent.getPort(),
+                        Optional.ofNullable(userAgent.getVersion()).orElse(""),
+                        Optional.ofNullable(userAgent.getIdc()).orElse(""),
+                        Optional.ofNullable(userAgent.getGroup()).orElse(""),
+                        Optional.ofNullable(userAgent.getPurpose()).orElse(""),
+                        "TCP"
+                );
+                getClientResponseList.add(getClientResponse);
+            }
+
+            // Get the list of HTTP clients
+            for (List<Client> clientList : eventMeshHTTPServer.localClientInfoMapping.values()) {
+                for (Client client : clientList) {
+                    GetClientResponse getClientResponse = new GetClientResponse(
+                            Optional.ofNullable(client.env).orElse(""),
+                            Optional.ofNullable(client.sys).orElse(""),
+                            Optional.ofNullable(client.url).orElse(""),
+                            "0",
+                            Optional.ofNullable(client.hostname).orElse(""),
+                            0,
+                            Optional.ofNullable(client.apiVersion).orElse(""),
+                            Optional.ofNullable(client.idc).orElse(""),
+                            Optional.ofNullable(client.consumerGroup).orElse(""),
+                            "",
+                            "HTTP"
+                    );
+                    getClientResponseList.add(getClientResponse);
+                }
+            }
+
+            // Get the list of gRPC clients
+            ConsumerManager consumerManager = eventMeshGrpcServer.getConsumerManager();
+            Map<String, List<ConsumerGroupClient>> clientTable = consumerManager.getClientTable();
+            for (List<ConsumerGroupClient> clientList : clientTable.values()) {
+                for (ConsumerGroupClient client : clientList) {
+                    GetClientResponse getClientResponse = new GetClientResponse(
+                            Optional.ofNullable(client.env).orElse(""),
+                            Optional.ofNullable(client.sys).orElse(""),
+                            Optional.ofNullable(client.url).orElse(""),
+                            "0",
+                            Optional.ofNullable(client.hostname).orElse(""),
+                            0,
+                            Optional.ofNullable(client.apiVersion).orElse(""),
+                            Optional.ofNullable(client.idc).orElse(""),
+                            Optional.ofNullable(client.consumerGroup).orElse(""),
+                            "",
+                            "gRPC"
+                    );
+                    getClientResponseList.add(getClientResponse);
+                }
+            }
+
+            getClientResponseList.sort((lhs, rhs) -> {
+                if (lhs.host.equals(rhs.host)) {
+                    return lhs.host.compareTo(rhs.host);
+                }
+                return Integer.compare(rhs.port, lhs.port);
+            });
+
+            String result = JsonUtils.toJson(getClientResponseList);
+            httpExchange.sendResponseHeaders(200, result.getBytes().length);
+            out.write(result.getBytes());
+        } catch (Exception e) {
+            Error error = new Error(e.toString());
+            String result = JsonUtils.toJson(error);
+            httpExchange.sendResponseHeaders(500, result.getBytes().length);
+            out.write(result.getBytes());
+        } finally {
+            if (out != null) {
+                try {
+                    out.close();
+                } catch (IOException e) {
+                    logger.warn("out close failed...", e);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void handle(HttpExchange httpExchange) throws IOException {
+        if (httpExchange.getRequestMethod().equals("OPTION")) {
+            preflight(httpExchange);
+        }
+        if (httpExchange.getRequestMethod().equals("GET")) {
+            list(httpExchange);
+        }
+        if (httpExchange.getRequestMethod().equals("DELETE")) {
+            delete(httpExchange);
+        }
+    }
+}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/request/DeleteClientRequest.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/request/DeleteClientRequest.java
new file mode 100644
index 00000000..d7ec05bc
--- /dev/null
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/request/DeleteClientRequest.java
@@ -0,0 +1,44 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.eventmesh.runtime.admin.request;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DeleteClientRequest {
+    public String host;
+    public int port;
+    public String protocol;
+    public String url;
+
+    @JsonCreator
+    public DeleteClientRequest(
+            @JsonProperty("host") String host,
+            @JsonProperty("port") int port,
+            @JsonProperty("protocol") String protocol,
+            @JsonProperty("url") String url
+    ) {
+        super();
+        this.host = host;
+        this.port = port;
+        this.protocol = protocol;
+        this.url = url;
+    }
+}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/response/Error.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/response/Error.java
new file mode 100644
index 00000000..f559ef08
--- /dev/null
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/response/Error.java
@@ -0,0 +1,33 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.eventmesh.runtime.admin.response;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class Error {
+    public String message;
+
+    @JsonCreator
+    public Error(
+            @JsonProperty("message") String message
+    ) {
+        super();
+        this.message = message;
+    }
+}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/response/GetClientResponse.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/response/GetClientResponse.java
new file mode 100644
index 00000000..381451a7
--- /dev/null
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/response/GetClientResponse.java
@@ -0,0 +1,63 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.eventmesh.runtime.admin.response;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class GetClientResponse {
+    public String env;
+    public String subsystem;
+    public String url;
+    public String pid;
+    public String host;
+    public int port;
+    public String version;
+    public String idc;
+    public String group;
+    public String purpose;
+    public String protocol;
+
+    @JsonCreator
+    public GetClientResponse(
+            @JsonProperty("env") String env,
+            @JsonProperty("subsystem") String subsystem,
+            @JsonProperty("url") String url,
+            @JsonProperty("pid") String pid,
+            @JsonProperty("host") String host,
+            @JsonProperty("port") int port,
+            @JsonProperty("version") String version,
+            @JsonProperty("idc") String idc,
+            @JsonProperty("group") String group,
+            @JsonProperty("purpose") String purpose,
+            @JsonProperty("protocol") String protocol
+    ) {
+        super();
+        this.env = env;
+        this.subsystem = subsystem;
+        this.url = url;
+        this.pid = pid;
+        this.host = host;
+        this.port = port;
+        this.idc = idc;
+        this.group = group;
+        this.purpose = purpose;
+        this.version = version;
+        this.protocol = protocol;
+    }
+}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/utils/HttpExchangeUtils.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/utils/HttpExchangeUtils.java
new file mode 100644
index 00000000..a378d030
--- /dev/null
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/utils/HttpExchangeUtils.java
@@ -0,0 +1,43 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.eventmesh.runtime.admin.utils;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+public class HttpExchangeUtils {
+    public static String streamToString(InputStream stream) throws IOException {
+        InputStreamReader isr = new InputStreamReader(stream, StandardCharsets.UTF_8);
+        BufferedReader bufferedReader = new BufferedReader(isr);
+
+        int b;
+        StringBuilder buffer = new StringBuilder();
+        while ((b = bufferedReader.read()) != -1) {
+            buffer.append((char) b);
+        }
+
+        bufferedReader.close();
+        isr.close();
+        return buffer.toString();
+    }
+}
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/utils/JsonUtils.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/utils/JsonUtils.java
new file mode 100644
index 00000000..4a6fb75d
--- /dev/null
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/admin/utils/JsonUtils.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.eventmesh.runtime.admin.utils;
+
+import java.io.IOException;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+
+public class JsonUtils {
+    private static final ObjectMapper objectMapper;
+
+    static {
+        objectMapper = new ObjectMapper();
+        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
+        objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+        objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
+    }
+
+    public static <T> byte[] serialize(String topic, Class<T> data) throws JsonProcessingException {
+        if (data == null) {
+            return null;
+        }
+        return objectMapper.writeValueAsBytes(data);
+    }
+
+    public static String toJson(Object obj) throws JsonProcessingException {
+        if (obj == null) {
+            return null;
+        }
+        return objectMapper.writeValueAsString(obj);
+    }
+
+    public static <T> T toObject(String json, Class<T> clazz) throws JsonProcessingException {
+        return objectMapper.readValue(json, clazz);
+    }
+
+    public static <T> T deserialize(Class<T> clazz, byte[] bytes) throws IOException {
+        if (bytes == null || bytes.length == 0) {
+            return null;
+        }
+
+        return objectMapper.readValue(bytes, clazz);
+    }
+
+    public static <T> T deserialize(Class<T> clazz, String json) throws IOException {
+        if (json == null || json.length() == 0) {
+            return null;
+        }
+
+        return objectMapper.readValue(json, clazz);
+    }
+}
\ No newline at end of file
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshServer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshServer.java
index a36a48ab..7d114b04 100644
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshServer.java
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshServer.java
@@ -19,6 +19,7 @@ package org.apache.eventmesh.runtime.boot;
 
 import org.apache.eventmesh.common.utils.ConfigurationContextUtil;
 import org.apache.eventmesh.runtime.acl.Acl;
+import org.apache.eventmesh.runtime.admin.controller.ClientManageController;
 import org.apache.eventmesh.runtime.common.ServiceState;
 import org.apache.eventmesh.runtime.configuration.EventMeshGrpcConfiguration;
 import org.apache.eventmesh.runtime.configuration.EventMeshHTTPConfiguration;
@@ -117,6 +118,9 @@ public class EventMeshServer {
             }
         }
 
+        ClientManageController clientManageController = new ClientManageController(eventMeshTCPServer, eventMeshHTTPServer, eventMeshGrpcServer);
+        clientManageController.start();
+
         String eventStore = System
             .getProperty(EventMeshConstants.EVENT_STORE_PROPERTIES, System.getenv(EventMeshConstants.EVENT_STORE_ENV));
         logger.info("eventStore : {}", eventStore);
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshTCPServer.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshTCPServer.java
index cfff7040..c992ed28 100644
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshTCPServer.java
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/boot/EventMeshTCPServer.java
@@ -71,8 +71,6 @@ public class EventMeshTCPServer extends AbstractRemotingServer {
 
     private EventMeshTcpMonitor eventMeshTcpMonitor;
 
-    private ClientManageController clientManageController;
-
     private EventMeshServer eventMeshServer;
 
     private EventMeshTCPConfiguration eventMeshTCPConfiguration;
@@ -93,14 +91,6 @@ public class EventMeshTCPServer extends AbstractRemotingServer {
         this.clientSessionGroupMapping = clientSessionGroupMapping;
     }
 
-    public ClientManageController getClientManageController() {
-        return clientManageController;
-    }
-
-    public void setClientManageController(ClientManageController clientManageController) {
-        this.clientManageController = clientManageController;
-    }
-
     public ScheduledExecutorService getScheduler() {
         return scheduler;
     }
@@ -208,8 +198,6 @@ public class EventMeshTCPServer extends AbstractRemotingServer {
 
         globalTrafficShapingHandler = newGTSHandler();
 
-        clientManageController = new ClientManageController(this);
-
         clientSessionGroupMapping = new ClientSessionGroupMapping(this);
         clientSessionGroupMapping.init();
 
@@ -243,8 +231,6 @@ public class EventMeshTCPServer extends AbstractRemotingServer {
 
         eventMeshTcpMonitor.start();
 
-        clientManageController.start();
-
         if (eventMeshTCPConfiguration.eventMeshServerRegistryEnable) {
             this.register();
             eventMeshRebalanceService.start();
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/grpc/consumer/ConsumerManager.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/grpc/consumer/ConsumerManager.java
index 1f881153..5f9f6f26 100644
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/grpc/consumer/ConsumerManager.java
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/grpc/consumer/ConsumerManager.java
@@ -61,6 +61,10 @@ public class ConsumerManager {
         this.eventMeshGrpcServer = eventMeshGrpcServer;
     }
 
+    public Map<String, List<ConsumerGroupClient>> getClientTable() {
+        return clientTable;
+    }
+
     public void init() throws Exception {
         logger.info("Grpc ConsumerManager initialized......");
     }
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/grpc/consumer/consumergroup/ConsumerGroupClient.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/grpc/consumer/consumergroup/ConsumerGroupClient.java
index 679ed517..1d17e416 100644
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/grpc/consumer/consumergroup/ConsumerGroupClient.java
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/grpc/consumer/consumergroup/ConsumerGroupClient.java
@@ -32,31 +32,31 @@ import lombok.Getter;
 @Getter
 public class ConsumerGroupClient {
 
-    private final String env;
+    public final String env;
 
-    private final String idc;
+    public final String idc;
 
-    private final String consumerGroup;
+    public final String consumerGroup;
 
-    private final String topic;
+    public final String topic;
 
     private final GrpcType grpcType;
 
-    private String url;
+    public String url;
 
     private EventEmitter<SimpleMessage> eventEmitter;
 
     private final SubscriptionMode subscriptionMode;
 
-    private final String sys;
+    public final String sys;
 
-    private final String ip;
+    public final String ip;
 
-    private final String pid;
+    public final String pid;
 
-    private final String hostname;
+    public final String hostname;
 
-    private final String apiVersion;
+    public final String apiVersion;
 
     private Date lastUpTime;
 
diff --git a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/http/consumer/ConsumerManager.java b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/http/consumer/ConsumerManager.java
index 3e22d307..1f2cf758 100644
--- a/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/http/consumer/ConsumerManager.java
+++ b/eventmesh-runtime/src/main/java/org/apache/eventmesh/runtime/core/protocol/http/consumer/ConsumerManager.java
@@ -340,4 +340,8 @@ public class ConsumerManager {
             logger.error("onChange event:{} err", event, ex);
         }
     }
+
+    public ConcurrentHashMap<String, ConsumerGroupManager> getClientTable() {
+        return consumerTable;
+    }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@eventmesh.apache.org
For additional commands, e-mail: commits-help@eventmesh.apache.org