transfer dashboard from parent
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..c87e042
--- /dev/null
+++ b/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/components/client/GrpcClientTable.tsx b/components/client/GrpcClientTable.tsx
new file mode 100644
index 0000000..1ffdfea
--- /dev/null
+++ b/components/client/GrpcClientTable.tsx
@@ -0,0 +1,217 @@
+/*
+ * 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 { useContext, useEffect, useState } from 'react';
+import { AppContext } from '../../context/context';
+
+interface GrpcClient {
+ env: string,
+ subsystem: string,
+ url: string,
+ pid: number,
+ host: string,
+ port: number,
+ version: string,
+ idc: string,
+ group: string,
+ purpose: string,
+ protocol: string,
+}
+
+interface GrpcClientProps {
+ url: string,
+ group: string,
+}
+
+interface RemoveGrpcClientRequest {
+ url: string,
+}
+
+const GrpcClientRow = ({
+ url, group,
+}: GrpcClientProps) => {
+ const { state } = useContext(AppContext);
+
+ const toast = useToast();
+ const [loading, setLoading] = useState(false);
+ const onRemoveClick = async () => {
+ try {
+ setLoading(true);
+ await axios.delete<RemoveGrpcClientRequest>(`${state.endpoint}/client/grpc`, {
+ data: {
+ url,
+ },
+ });
+ setLoading(false);
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to remove the gRPC Client',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ }
+ }
+ };
+
+ return (
+ <Tr>
+ <Td>{url}</Td>
+ <Td>{group}</Td>
+ <Td>
+ <HStack>
+ <Button
+ colorScheme="red"
+ isLoading={loading}
+ onClick={onRemoveClick}
+ >
+ Remove
+ </Button>
+ </HStack>
+ </Td>
+ </Tr>
+ );
+};
+
+const GrpcClientTable = () => {
+ const { state } = useContext(AppContext);
+
+ const [searchInput, setSearchInput] = useState<string>('');
+ const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setSearchInput(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 [GrpcClientList, setGrpcClientList] = useState<GrpcClient[]>([]);
+ const toast = useToast();
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ const { data } = await axios.get<GrpcClient[]>(`${state.endpoint}/client/grpc`);
+ setGrpcClientList(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 gRPC Clients',
+ description: 'Unable to connect to the EventMesh daemon',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ setGrpcClientList([]);
+ }
+ }
+ };
+
+ 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 Group"
+ onChange={handleGroupSelectChange}
+ >
+ {Array.from(groupSet).map((group) => (
+ <option value={group} key={group}>{group}</option>
+ ))}
+ </Select>
+ </HStack>
+
+ <TableContainer>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>URL</Th>
+ <Th>Group</Th>
+ <Th>Action</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {GrpcClientList && GrpcClientList.filter(({
+ url, group,
+ }) => {
+ if (searchInput && !url.includes(searchInput)) {
+ return false;
+ }
+ if (groupFilter && groupFilter !== group) {
+ return false;
+ }
+ return true;
+ }).map(({
+ url, group,
+ }) => (
+ <GrpcClientRow
+ url={url}
+ group={group}
+ />
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ );
+};
+
+export default GrpcClientTable;
diff --git a/components/client/HTTPClientTable.tsx b/components/client/HTTPClientTable.tsx
new file mode 100644
index 0000000..832ea91
--- /dev/null
+++ b/components/client/HTTPClientTable.tsx
@@ -0,0 +1,217 @@
+/*
+ * 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 { useContext, useEffect, useState } from 'react';
+import { AppContext } from '../../context/context';
+
+interface HTTPClient {
+ env: string,
+ subsystem: string,
+ url: string,
+ pid: number,
+ host: string,
+ port: number,
+ version: string,
+ idc: string,
+ group: string,
+ purpose: string,
+ protocol: string,
+}
+
+interface HTTPClientProps {
+ url: string,
+ group: string,
+}
+
+interface RemoveHTTPClientRequest {
+ url: string,
+}
+
+const HTTPClientRow = ({
+ url, group,
+}: HTTPClientProps) => {
+ const { state } = useContext(AppContext);
+
+ const toast = useToast();
+ const [loading, setLoading] = useState(false);
+ const onRemoveClick = async () => {
+ try {
+ setLoading(true);
+ await axios.delete<RemoveHTTPClientRequest>(`${state.endpoint}/client/http`, {
+ data: {
+ url,
+ },
+ });
+ setLoading(false);
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to remove the HTTP Client',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ }
+ }
+ };
+
+ return (
+ <Tr>
+ <Td>{url}</Td>
+ <Td>{group}</Td>
+ <Td>
+ <HStack>
+ <Button
+ colorScheme="red"
+ isLoading={loading}
+ onClick={onRemoveClick}
+ >
+ Remove
+ </Button>
+ </HStack>
+ </Td>
+ </Tr>
+ );
+};
+
+const HTTPClientTable = () => {
+ const { state } = useContext(AppContext);
+
+ const [searchInput, setSearchInput] = useState<string>('');
+ const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setSearchInput(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 [HTTPClientList, setHTTPClientList] = useState<HTTPClient[]>([]);
+ const toast = useToast();
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ const { data } = await axios.get<HTTPClient[]>(`${state.endpoint}/client/http`);
+ setHTTPClientList(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 HTTP Clients',
+ description: 'Unable to connect to the EventMesh daemon',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ setHTTPClientList([]);
+ }
+ }
+ };
+
+ 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 Group"
+ onChange={handleGroupSelectChange}
+ >
+ {Array.from(groupSet).map((group) => (
+ <option value={group} key={group}>{group}</option>
+ ))}
+ </Select>
+ </HStack>
+
+ <TableContainer>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>URL</Th>
+ <Th>Group</Th>
+ <Th>Action</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {HTTPClientList && HTTPClientList.filter(({
+ url, group,
+ }) => {
+ if (searchInput && !url.includes(searchInput)) {
+ return false;
+ }
+ if (groupFilter && groupFilter !== group) {
+ return false;
+ }
+ return true;
+ }).map(({
+ url, group,
+ }) => (
+ <HTTPClientRow
+ url={url}
+ group={group}
+ />
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ );
+};
+
+export default HTTPClientTable;
diff --git a/components/client/TCPClientTable.tsx b/components/client/TCPClientTable.tsx
new file mode 100644
index 0000000..dc24589
--- /dev/null
+++ b/components/client/TCPClientTable.tsx
@@ -0,0 +1,223 @@
+/*
+ * 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 { useContext, useEffect, useState } from 'react';
+import { AppContext } from '../../context/context';
+
+interface TCPClient {
+ env: string,
+ subsystem: string,
+ url: string,
+ pid: number,
+ host: string,
+ port: number,
+ version: string,
+ idc: string,
+ group: string,
+ purpose: string,
+ protocol: string,
+}
+
+interface TCPClientProps {
+ host: string,
+ port: number,
+ group: string,
+}
+
+interface RemoveTCPClientRequest {
+ host: string,
+ port: number,
+}
+
+const TCPClientRow = ({
+ host, port, group,
+}: TCPClientProps) => {
+ const { state } = useContext(AppContext);
+
+ const toast = useToast();
+ const [loading, setLoading] = useState(false);
+ const onRemoveClick = async () => {
+ try {
+ setLoading(true);
+ await axios.delete<RemoveTCPClientRequest>(`${state.endpoint}/client/tcp`, {
+ data: {
+ host,
+ port,
+ },
+ });
+ setLoading(false);
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to remove the TCP Client',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ }
+ }
+ };
+
+ return (
+ <Tr>
+ <Td>{`${host}:${port}`}</Td>
+ <Td>{group}</Td>
+ <Td>
+ <HStack>
+ <Button
+ colorScheme="red"
+ isLoading={loading}
+ onClick={onRemoveClick}
+ >
+ Remove
+ </Button>
+ </HStack>
+ </Td>
+ </Tr>
+ );
+};
+
+const TCPClientTable = () => {
+ const { state } = useContext(AppContext);
+
+ const [searchInput, setSearchInput] = useState<string>('');
+ const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setSearchInput(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 [TCPClientList, setTCPClientList] = useState<TCPClient[]>([]);
+ const toast = useToast();
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ const { data } = await axios.get<TCPClient[]>(`${state.endpoint}/client/tcp`);
+ setTCPClientList(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 TCP Clients',
+ description: 'Unable to connect to the EventMesh daemon',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ setTCPClientList([]);
+ }
+ }
+ };
+
+ 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 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</Th>
+ <Th>Host</Th>
+ <Th>Group</Th>
+ <Th>Action</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {TCPClientList && TCPClientList.filter(({
+ host, port, group,
+ }) => {
+ const address = `${host}:${port}`;
+ if (searchInput && !address.includes(searchInput)) {
+ return false;
+ }
+ if (groupFilter && groupFilter !== group) {
+ return false;
+ }
+ return true;
+ }).map(({
+ host, port, group,
+ }) => (
+ <TCPClientRow
+ host={host}
+ port={port}
+ group={group}
+ />
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ );
+};
+
+export default TCPClientTable;
diff --git a/components/event/EventTable.tsx b/components/event/EventTable.tsx
new file mode 100644
index 0000000..b0d0a13
--- /dev/null
+++ b/components/event/EventTable.tsx
@@ -0,0 +1,371 @@
+/*
+ * 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,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ TableContainer,
+ useToast,
+ Box,
+ Button,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ useDisclosure,
+ Select,
+ VStack,
+ Textarea,
+} from '@chakra-ui/react';
+import axios from 'axios';
+import { useContext, useEffect, useState } from 'react';
+import { CloudEvent } from 'cloudevents';
+import { AppContext } from '../../context/context';
+
+interface Topic {
+ name: string,
+ messageCount: number,
+}
+
+interface EventProps {
+ event: CloudEvent<string>,
+}
+
+interface CreateEventRequest {
+ event: CloudEvent<string>,
+}
+
+const CreateEventModal = () => {
+ const { state } = useContext(AppContext);
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+
+ const [id, setId] = useState('');
+ const handleIdChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setId(event.currentTarget.value);
+ };
+
+ const [source, setSource] = useState('');
+ const handleSourceChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setSource(event.currentTarget.value);
+ };
+
+ const [subject, setSubject] = useState('');
+ const handleSubjectChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setSubject(event.currentTarget.value);
+ };
+
+ const [type, setType] = useState('');
+ const handleTypeChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setType(event.currentTarget.value);
+ };
+
+ const [data, setData] = useState('');
+ const handleDataChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setData(event.currentTarget.value);
+ };
+
+ const toast = useToast();
+ const [loading, setLoading] = useState(false);
+ const onCreateClick = async () => {
+ try {
+ setLoading(true);
+ await axios.post<CreateEventRequest>(`${state.endpoint}/event`, new CloudEvent({
+ source,
+ subject,
+ type,
+ data,
+ specversion: '1.0',
+ }));
+ onClose();
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to publish the event',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <>
+ <Button
+ w="25%"
+ colorScheme="blue"
+ onClick={onOpen}
+ >
+ Create Event
+ </Button>
+ <Modal isOpen={isOpen} onClose={onClose}>
+ <ModalOverlay />
+ <ModalContent>
+ <ModalHeader>Create Event</ModalHeader>
+ <ModalCloseButton />
+ <ModalBody>
+ <VStack>
+ <Input
+ placeholder="Event ID"
+ value={id}
+ onChange={handleIdChange}
+ />
+ <Input
+ placeholder="Event Source"
+ value={source}
+ onChange={handleSourceChange}
+ />
+ <Input
+ placeholder="Event Subject"
+ value={subject}
+ onChange={handleSubjectChange}
+ />
+ <Input
+ placeholder="Event Type"
+ value={type}
+ onChange={handleTypeChange}
+ />
+ <Input
+ placeholder="Event Data"
+ value={data}
+ onChange={handleDataChange}
+ />
+ </VStack>
+ </ModalBody>
+
+ <ModalFooter>
+ <Button
+ mr={2}
+ onClick={onClose}
+ >
+ Close
+ </Button>
+ <Button
+ colorScheme="blue"
+ onClick={onCreateClick}
+ isLoading={loading}
+ isDisabled={
+ id.length === 0 || subject.length === 0 || source.length === 0 || type.length === 0
+ }
+ >
+ Create
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ </>
+ );
+};
+
+const EventRow = ({
+ event,
+}: EventProps) => {
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const eventDataBase64 = event.data_base64 || '';
+ const eventData = Buffer.from(eventDataBase64, 'base64').toString('utf-8');
+
+ return (
+ <>
+ <Modal isOpen={isOpen} onClose={onClose}>
+ <ModalOverlay />
+ <ModalContent>
+ <ModalHeader>Event Data</ModalHeader>
+ <ModalCloseButton />
+ <ModalBody>
+ <Box>
+ <Textarea isDisabled value={eventData} />
+ </Box>
+ </ModalBody>
+
+ <ModalFooter>
+ <Button
+ mr={2}
+ onClick={onClose}
+ >
+ Close
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+
+ <Tr>
+ <Td>{event.id}</Td>
+ <Td>{event.subject}</Td>
+ <Td>{new Date(Number(event.reqc2eventmeshtimestamp)).toLocaleString()}</Td>
+ <Td>
+ <HStack>
+ <Button
+ colorScheme="blue"
+ onClick={onOpen}
+ >
+ View Data
+ </Button>
+ </HStack>
+
+ </Td>
+ </Tr>
+ </>
+ );
+};
+
+const EventTable = () => {
+ const { state } = useContext(AppContext);
+
+ const [searchInput, setSearchInput] = useState<string>('');
+ const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setSearchInput(event.currentTarget.value);
+ };
+
+ const [eventList, setEventList] = useState<CloudEvent<string>[]>([]);
+ const [topicList, setTopicList] = useState<Topic[]>([]);
+ const [topic, setTopic] = useState<Topic>({
+ name: '',
+ messageCount: 0,
+ });
+ const handleTopicChange = (event: React.FormEvent<HTMLSelectElement>) => {
+ setTopic({
+ name: event.currentTarget.value,
+ messageCount: 0,
+ });
+ };
+
+ const toast = useToast();
+
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ const { data } = await axios.get<Topic[]>(`${state.endpoint}/topic`);
+ setTopicList(data);
+ if (data.length !== 0) {
+ setTopic(data[0]);
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to fetch the list of events',
+ description: 'unable to connect to the EventMesh daemon',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ setEventList([]);
+ }
+ }
+ };
+
+ fetch();
+ }, []);
+
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ if (topic.name !== '') {
+ const eventResponse = await axios.get<string[]>(`${state.endpoint}/event`, {
+ params: {
+ topicName: topic.name,
+ offset: 0,
+ length: 15,
+ },
+ });
+ setEventList(eventResponse.data.map((rawEvent) => JSON.parse(rawEvent)));
+ }
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to fetch the list of events',
+ description: 'Unable to connect to the EventMesh daemon',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ setEventList([]);
+ }
+ }
+ };
+
+ fetch();
+ }, [topic]);
+
+ return (
+ <Box
+ maxW="full"
+ bg="white"
+ borderWidth="1px"
+ borderRadius="md"
+ overflow="hidden"
+ p="4"
+ >
+ <HStack
+ spacing="2"
+ >
+ <Input
+ w="100%"
+ placeholder="Search"
+ value={searchInput}
+ onChange={handleSearchInputChange}
+ />
+ <Select
+ w="100%"
+ onChange={handleTopicChange}
+ >
+ {topicList.map(({ name }) => (
+ <option value={name} key={name} selected={topic.name === name}>{name}</option>
+ ))}
+ </Select>
+ <CreateEventModal />
+ </HStack>
+
+ <TableContainer>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Event Id</Th>
+ <Th>Event Subject</Th>
+ <Th>Event Time</Th>
+ <Th>Action</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {eventList.filter(() => true).map((event) => (
+ <EventRow
+ key={event.id}
+ event={event}
+ />
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ );
+};
+
+export default EventTable;
diff --git a/components/eventCatalogs/Create.tsx b/components/eventCatalogs/Create.tsx
new file mode 100644
index 0000000..7d75f4b
--- /dev/null
+++ b/components/eventCatalogs/Create.tsx
@@ -0,0 +1,137 @@
+/*
+ * 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 React, { FC, useRef, useState } from 'react';
+
+import {
+ Drawer,
+ DrawerContent,
+ DrawerCloseButton,
+ DrawerOverlay,
+ DrawerHeader,
+ DrawerBody,
+ DrawerFooter,
+ Button,
+ Box,
+ useToast,
+ Spinner,
+} from '@chakra-ui/react';
+import Editor, { Monaco } from '@monaco-editor/react';
+import axios from 'axios';
+
+const ApiRoot = process.env.NEXT_PUBLIC_EVENTCATALOG_API_ROOT;
+
+const Create: FC<{ visible: boolean; onClose: () => void; onSucceed:()=>void
+}> = ({ visible = false, onClose, onSucceed }) => {
+ const toast = useToast();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const editorRef = useRef<Monaco | null>(null);
+ const defaultEditorValue = '# Your code goes here';
+
+ const handleEditorDidMount = (editor: any) => {
+ // here is the editor instance
+ // you can store it in `useRef` for further usage
+ editorRef.current = editor;
+ };
+
+ const onSubmit = () => {
+ setIsSubmitting(true);
+
+ try {
+ const value = editorRef.current.getValue();
+ if (value === defaultEditorValue) {
+ toast({
+ title: 'Invalid definition',
+ description: 'Please input your workflow definition properly',
+ status: 'warning',
+ position: 'top-right',
+ });
+ setIsSubmitting(false);
+ return;
+ }
+ axios
+ .post(
+ `${ApiRoot}/catalog`,
+ { event: { definition: value } },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ )
+ .then(() => {
+ toast({
+ title: 'Succeeded',
+ status: 'success',
+ position: 'top-right',
+ });
+ onSucceed();
+ setIsSubmitting(false);
+ })
+ .catch((error) => {
+ toast({
+ title: 'Failed',
+ description: error.response.data,
+ status: 'error',
+ position: 'top-right',
+ });
+ setIsSubmitting(false);
+ });
+ } catch (error) {
+ setIsSubmitting(false);
+ }
+ };
+
+ return (
+ <Drawer
+ isOpen={visible}
+ size="xl"
+ placement="right"
+ onClose={() => onClose()}
+ >
+ <DrawerOverlay />
+ <DrawerContent>
+ <DrawerCloseButton />
+ <DrawerHeader>Create Catalog</DrawerHeader>
+ <DrawerBody>
+ <Box height="full">
+ <Editor
+ height="100%"
+ defaultLanguage="yaml"
+ defaultValue="# Your code goes here"
+ onMount={handleEditorDidMount}
+ theme="vs-dark"
+ />
+ </Box>
+ </DrawerBody>
+ <DrawerFooter justifyContent="flex-start">
+ <Button colorScheme="blue" mr={3} onClick={onSubmit}>
+ {isSubmitting ? <Spinner colorScheme="white" size="sm" /> : 'Submit'}
+ </Button>
+ <Button variant="ghost" colorScheme="blue" onClick={onClose}>
+ Cancel
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+
+ </Drawer>
+ );
+};
+
+export default Create;
diff --git a/components/eventCatalogs/Details.tsx b/components/eventCatalogs/Details.tsx
new file mode 100644
index 0000000..b4d33ee
--- /dev/null
+++ b/components/eventCatalogs/Details.tsx
@@ -0,0 +1,121 @@
+/*
+ * 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 React, { FC, useRef } from 'react';
+
+import {
+ Drawer,
+ DrawerContent,
+ DrawerCloseButton,
+ DrawerOverlay,
+ DrawerHeader,
+ DrawerBody,
+ FormLabel,
+ Box,
+ Flex,
+ Text,
+ Stack,
+ Badge,
+} from '@chakra-ui/react';
+import moment from 'moment';
+import Editor, { Monaco } from '@monaco-editor/react';
+import { EventCatalogType } from './types';
+// import { WorkflowStatusMap } from './constant';
+
+const Details: FC<{
+ visible: boolean;
+ data?: EventCatalogType | undefined;
+ onClose: () => void;
+}> = ({ visible = false, data, onClose = () => {} }) => {
+ const editorRef = useRef(null);
+ const handleEditorDidMount = (editor: Monaco) => {
+ editorRef.current = editor;
+ editor.setValue(data?.definition ?? '');
+ };
+
+ return (
+ <Drawer
+ isOpen={visible}
+ size="xl"
+ placement="right"
+ onClose={() => onClose()}
+ >
+ <DrawerOverlay />
+ <DrawerContent>
+ <DrawerCloseButton />
+ <DrawerHeader>
+ {data?.title}
+ <Badge ml={2}>
+ Version
+ {' '}
+ {data?.version}
+ </Badge>
+ </DrawerHeader>
+ <DrawerBody>
+ <Flex flexDirection="column" h="full">
+ <Stack direction="row">
+ <Flex width="240px" flexDirection="column">
+ <Box mb="1">
+ <FormLabel opacity={0.5}>Catalog ID</FormLabel>
+ <Text>{data?.id}</Text>
+ </Box>
+ <Box mt="1" mb="3">
+ <FormLabel opacity={0.5}>Title</FormLabel>
+ <Text>{data?.title}</Text>
+ </Box>
+ <Box mt="1" mb="3">
+ <FormLabel opacity={0.5}>File Name</FormLabel>
+ <Text>{data?.file_name}</Text>
+ </Box>
+ </Flex>
+ <Flex flexDirection="column">
+ <Box mb="1">
+ <FormLabel opacity={0.5}>Created At</FormLabel>
+ <Text>
+ {moment(data?.create_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Text>
+ </Box>
+ <Box mt="1" mb="3">
+ <FormLabel opacity={0.5}>Updated At</FormLabel>
+ <Text>
+ {moment(data?.update_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Text>
+ </Box>
+ </Flex>
+ </Stack>
+ <Box flex={1}>
+ <Editor
+ height="100%"
+ defaultLanguage="yaml"
+ defaultValue="# Your code goes here"
+ onMount={handleEditorDidMount}
+ theme="vs-dark"
+ options={{ readOnly: true }}
+ />
+ </Box>
+ </Flex>
+ </DrawerBody>
+ </DrawerContent>
+ </Drawer>
+ );
+};
+Details.defaultProps = {
+ data: undefined,
+};
+export default Details;
diff --git a/components/eventCatalogs/constant.ts b/components/eventCatalogs/constant.ts
new file mode 100644
index 0000000..df9cfab
--- /dev/null
+++ b/components/eventCatalogs/constant.ts
@@ -0,0 +1,27 @@
+/*
+ * 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 { CatalogStatusEnum } from './types';
+
+export const WorkflowStatusMap = new Map([
+ [CatalogStatusEnum.Normal, 'Normal'],
+ [CatalogStatusEnum.Deleted, 'Deleted'],
+]);
+
+export default WorkflowStatusMap;
diff --git a/components/eventCatalogs/types.ts b/components/eventCatalogs/types.ts
new file mode 100644
index 0000000..8ab7004
--- /dev/null
+++ b/components/eventCatalogs/types.ts
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+export type SchemaTypes = {
+ schemaId: string;
+ lastVersion: string;
+ description: string;
+};
+
+export type EventCatalogType = {
+ create_time: string,
+ definition: string,
+ file_name: string,
+ id: 0,
+ status: 0,
+ title: string,
+ update_time: string,
+ version: string
+};
+
+export enum CatalogStatusEnum {
+ 'Normal' = 1,
+ 'Deleted' = -1,
+}
diff --git a/components/index/Configuration.tsx b/components/index/Configuration.tsx
new file mode 100755
index 0000000..8258db2
--- /dev/null
+++ b/components/index/Configuration.tsx
@@ -0,0 +1,249 @@
+/*
+ * 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 {
+ Text,
+ Table,
+ TableContainer,
+ Tbody,
+ Th,
+ Thead,
+ Tr,
+ Td,
+ Box,
+} from '@chakra-ui/react';
+import axios from 'axios';
+import React, { useContext, useEffect, useState } from 'react';
+import { AppContext } from '../../context/context';
+
+interface EventMeshConfiguration {
+ sysID: string;
+ namesrvAddr: string;
+ eventMeshEnv: string;
+ eventMeshIDC: string;
+ eventMeshCluster: string;
+ eventMeshServerIp: string;
+ eventMeshName: string;
+ eventMeshWebhookOrigin: string;
+ eventMeshServerSecurityEnable: boolean;
+ eventMeshServerRegistryEnable: boolean;
+
+ eventMeshTcpServerPort: number;
+ eventMeshTcpServerEnabled: boolean;
+
+ eventMeshHttpServerPort: number;
+ eventMeshHttpServerUseTls: boolean;
+
+ eventMeshGrpcServerPort: number;
+ eventMeshGrpcServerUseTls: boolean;
+}
+
+const Configuration = () => {
+ const { state } = useContext(AppContext);
+ const [configuration, setConfiguration] = useState<
+ Partial<EventMeshConfiguration>
+ >({});
+
+ useEffect(() => {
+ const controller = new AbortController();
+ const fetch = async () => {
+ try {
+ const { data } = await axios.get<EventMeshConfiguration>(
+ `${state.endpoint}/configuration`,
+ {
+ signal: controller.signal,
+ },
+ );
+ setConfiguration(data);
+ } catch (error) {
+ setConfiguration({});
+ }
+ };
+
+ fetch();
+
+ return () => {
+ controller.abort();
+ };
+ }, [state.endpoint]);
+
+ type ConfigurationRecord = Record<
+ string,
+ string | number | boolean | undefined
+ >;
+ const commonConfiguration: ConfigurationRecord = {
+ 'System ID': configuration.sysID,
+ 'NameServer Address': configuration.namesrvAddr,
+ 'EventMesh Environment': configuration.eventMeshEnv,
+ 'EventMesh IDC': configuration.eventMeshIDC,
+ 'EventMesh Cluster': configuration.eventMeshCluster,
+ 'EventMesh Server IP': configuration.eventMeshServerIp,
+ 'EventMEsh Name': configuration.eventMeshName,
+ 'EventMesh Webhook Origin': configuration.eventMeshWebhookOrigin,
+ 'EventMesh Server Security Enable':
+ configuration.eventMeshServerSecurityEnable,
+ 'EventMesh Server Registry Enable':
+ configuration.eventMeshServerRegistryEnable,
+ };
+
+ const tcpConfiguration: ConfigurationRecord = {
+ 'TCP Server Port': configuration.eventMeshTcpServerPort,
+ 'TCP Server Enabled': configuration.eventMeshTcpServerEnabled,
+ };
+
+ const httpConfiguration: ConfigurationRecord = {
+ 'HTTP Server Port': configuration.eventMeshHttpServerPort,
+ 'HTTP Server TLS Enabled': configuration.eventMeshHttpServerUseTls,
+ };
+
+ const grpcConfiguration: ConfigurationRecord = {
+ 'gRPC Server Port': configuration.eventMeshGrpcServerPort,
+ 'gRPC Server TLS Enabled': configuration.eventMeshGrpcServerUseTls,
+ };
+
+ const convertConfigurationToTable = (
+ configurationRecord: Record<string, string | number | boolean | undefined>,
+ ) => Object.entries(configurationRecord).map(([key, value]) => {
+ if (value === undefined) {
+ return (
+ <Tr>
+ <Td>{key}</Td>
+ <Td>Undefined</Td>
+ </Tr>
+ );
+ }
+
+ return (
+ <Tr>
+ <Td>{key}</Td>
+ <Td>{value.toString()}</Td>
+ </Tr>
+ );
+ });
+
+ if (Object.keys(configuration).length === 0) {
+ return (
+ <Box
+ maxW="full"
+ bg="white"
+ borderWidth="2px"
+ borderRadius="md"
+ borderColor="rgb(211,85,25)"
+ overflow="hidden"
+ p="4"
+ mt="4"
+ opacity="0.8"
+ >
+ <Text
+ fontSize="l"
+ fontWeight="semibold"
+ color="rgb(211,85,25)"
+ textAlign={['left', 'center']}
+ >
+ EventMesh Daemon Not Connected
+ </Text>
+ </Box>
+ );
+ }
+
+ return (
+ <>
+ <Box
+ maxW="full"
+ bg="white"
+ borderWidth="1px"
+ borderRadius="md"
+ overflow="hidden"
+ p="4"
+ mt="4"
+ >
+ <Text w="full">EventMesh Configuration</Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Configuration Field</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>{convertConfigurationToTable(commonConfiguration)}</Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ <Box
+ maxW="full"
+ bg="white"
+ borderWidth="1px"
+ borderRadius="md"
+ overflow="hidden"
+ p="4"
+ mt="4"
+ >
+ <Text w="full">TCP Configuration</Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Configuration Field</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>{convertConfigurationToTable(tcpConfiguration)}</Tbody>
+ </Table>
+ </TableContainer>
+
+ <Text w="full" mt="4">
+ HTTP Configuration
+ </Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Configuration Field</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>{convertConfigurationToTable(httpConfiguration)}</Tbody>
+ </Table>
+ </TableContainer>
+
+ <Text w="full" mt="4">
+ gRPC Configuration
+ </Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Configuration Field</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>{convertConfigurationToTable(grpcConfiguration)}</Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ </>
+ );
+};
+
+export default Configuration;
diff --git a/components/index/Endpoint.tsx b/components/index/Endpoint.tsx
new file mode 100755
index 0000000..9e550fa
--- /dev/null
+++ b/components/index/Endpoint.tsx
@@ -0,0 +1,105 @@
+/*
+ * 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, { useContext, useEffect, useState } from 'react';
+import { AppContext } from '../../context/context';
+
+const Endpoint = () => {
+ const { state, dispatch } = useContext(AppContext);
+ const toast = useToast();
+ const [endpointInput, setEndpointInput] = useState('http://localhost:10106');
+ const [loading, setLoading] = useState(false);
+
+ useEffect(() => {
+ setEndpointInput(state.endpoint);
+ }, [state.endpoint]);
+
+ const handleEndpointInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setEndpointInput(event.currentTarget.value);
+ };
+
+ const handleSaveButtonClick = async () => {
+ try {
+ setLoading(true);
+ await axios.get(`${endpointInput}/client`);
+ dispatch({
+ type: 'SetEndPointAction',
+ payload: {
+ 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/components/metrics/MetricsTable.tsx b/components/metrics/MetricsTable.tsx
new file mode 100755
index 0000000..9b80d49
--- /dev/null
+++ b/components/metrics/MetricsTable.tsx
@@ -0,0 +1,313 @@
+/*
+ * 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 {
+ Text,
+ Table,
+ TableContainer,
+ Tbody,
+ Th,
+ Thead,
+ Tr,
+ Td,
+ Box,
+} from '@chakra-ui/react';
+import axios from 'axios';
+import React, { useContext, useEffect, useState } from 'react';
+import { AppContext } from '../../context/context';
+
+interface EventMeshMetrics {
+ maxHTTPTPS: number,
+ avgHTTPTPS: number,
+ maxHTTPCost: number,
+ avgHTTPCost: number,
+ avgHTTPBodyDecodeCost: number,
+ httpDiscard: number,
+ maxBatchSendMsgTPS: number,
+ avgBatchSendMsgTPS: number,
+ sendBatchMsgNumSum: number,
+ sendBatchMsgFailNumSum: number,
+ sendBatchMsgFailRate: number,
+ sendBatchMsgDiscardNumSum: number,
+ maxSendMsgTPS: number,
+ avgSendMsgTPS: number,
+ sendMsgNumSum: number,
+ sendMsgFailNumSum: number,
+ sendMsgFailRate: number,
+ replyMsgNumSum: number,
+ replyMsgFailNumSum: number,
+ maxPushMsgTPS: number,
+ avgPushMsgTPS: number,
+ pushHTTPMsgNumSum: number,
+ pushHTTPMsgFailNumSum: number,
+ pushHTTPMsgFailRate: number,
+ maxHTTPPushLatency : number,
+ avgHTTPPushLatency : number,
+ batchMsgQueueSize : number,
+ sendMsgQueueSize : number,
+ pushMsgQueueSize : number,
+ retryHTTPQueueSize : number,
+ avgBatchSendMsgCost : number,
+ avgSendMsgCost : number,
+ avgReplyMsgCost : number,
+
+ // TCP Metrics
+ retryTCPQueueSize: number,
+ client2eventMeshTCPTPS : number,
+ eventMesh2mqTCPTPS : number,
+ mq2eventMeshTCPTPS : number,
+ eventMesh2clientTCPTPS : number,
+ allTCPTPS : number,
+ allTCPConnections : number,
+ subTopicTCPNum : number
+}
+
+const MetricsTable = () => {
+ const { state } = useContext(AppContext);
+ const [metrics, setMetrics] = useState<Partial<EventMeshMetrics>>({});
+
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ const { data } = await axios.get<EventMeshMetrics>(`${state.endpoint}/metrics`);
+ setMetrics(data);
+ } catch (error) {
+ setMetrics({});
+ }
+ };
+
+ fetch();
+ }, []);
+
+ type MetricRecord = Record<string, number | undefined>;
+ const httpMetrics: MetricRecord = {
+ 'Max HTTP TPS': metrics.maxHTTPTPS,
+ 'Avg HTTP TPS': metrics.avgHTTPTPS,
+ 'Max HTTP Cost': metrics.maxHTTPCost,
+ 'Avg HTTP Cost': metrics.avgHTTPCost,
+ 'Avg HTTP Body Decode Cost': metrics.avgHTTPBodyDecodeCost,
+ 'HTTP Discard': metrics.httpDiscard,
+ };
+ const batchMetrics: MetricRecord = {
+ 'Max Batch Send Msg TPS': metrics.maxBatchSendMsgTPS,
+ 'Avg Batch Send Msg TPS': metrics.avgBatchSendMsgTPS,
+ 'Send Batch Msg Num Sum': metrics.sendBatchMsgNumSum,
+ 'Send Batch Msg Fail Num Sum': metrics.sendBatchMsgFailNumSum,
+ 'Send Batch Msg Fail Rate': metrics.sendBatchMsgFailRate,
+ 'Send Batch Msg Discard Num Sum': metrics.sendBatchMsgDiscardNumSum,
+ };
+ const sendMetrics: MetricRecord = {
+ 'Max Send Msg TPS': metrics.maxSendMsgTPS,
+ 'Avg Send Msg TPS': metrics.avgSendMsgTPS,
+ 'Send Msg Num Sum': metrics.sendMsgNumSum,
+ 'Send Msg Fail Num Sum': metrics.sendMsgFailNumSum,
+ 'Send Msg Fail Rate': metrics.sendMsgFailRate,
+ 'Reply Msg Num Sum': metrics.replyMsgNumSum,
+ 'Reply Msg Fail Num Sum': metrics.replyMsgFailNumSum,
+ };
+ const pushMetrics: MetricRecord = {
+ 'Max Push Msg TPS': metrics.maxPushMsgTPS,
+ 'Avg Push Msg TPS': metrics.avgPushMsgTPS,
+ 'Push HTTP Msg Num Sum': metrics.pushHTTPMsgNumSum,
+ 'Push HTTP Msg Fail Num Sum': metrics.pushHTTPMsgFailNumSum,
+ 'Push HTTP Msg Fail Rate': metrics.pushHTTPMsgFailRate,
+ 'Max HTTP Push Latency': metrics.maxHTTPPushLatency,
+ 'Avg HTTP Push Latency': metrics.avgHTTPPushLatency,
+ };
+ const tcpMetrics: MetricRecord = {
+ 'Retry TCP Queue Size': metrics.retryTCPQueueSize,
+ 'Client2eventMesh TCP TPS': metrics.client2eventMeshTCPTPS,
+ 'EventMesh2mq TCP TPS': metrics.eventMesh2mqTCPTPS,
+ 'MQ2eventMesh TCP TPS': metrics.mq2eventMeshTCPTPS,
+ 'EventMesh2client TCP TPS': metrics.eventMesh2clientTCPTPS,
+ 'All TCP TPS': metrics.allTCPTPS,
+ 'All TCP Connections': metrics.allTCPConnections,
+ 'Sub Topic TCP Num': metrics.subTopicTCPNum,
+ };
+
+ const convertConfigurationToTable = (
+ metricRecord: Record<string, string | number | boolean | undefined>,
+ ) => Object.entries(metricRecord).map(([key, value]) => {
+ if (value === undefined) {
+ return (
+ <Tr>
+ <Td>{key}</Td>
+ <Td>Undefined</Td>
+ </Tr>
+ );
+ }
+
+ return (
+ <Tr>
+ <Td>{key}</Td>
+ <Td>{value.toString()}</Td>
+ </Tr>
+ );
+ });
+
+ if (Object.keys(metrics).length === 0) {
+ return (
+ <Box
+ maxW="full"
+ bg="white"
+ borderWidth="2px"
+ borderRadius="md"
+ borderColor="rgb(211,85,25)"
+ overflow="hidden"
+ p="4"
+ mt="4"
+ opacity="0.8"
+ >
+ <Text fontSize="l" fontWeight="semibold" color="rgb(211,85,25)" textAlign={['left', 'center']}>
+ EventMesh Daemon Not Connected
+ </Text>
+ </Box>
+ );
+ }
+ return (
+ <>
+ <Box
+ maxW="full"
+ bg="white"
+ borderWidth="1px"
+ borderRadius="md"
+ overflow="hidden"
+ p="4"
+ mt="4"
+ >
+ <Text
+ w="full"
+ >
+ EventMesh HTTP Metrics
+ </Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Metric</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {convertConfigurationToTable(httpMetrics)}
+ </Tbody>
+ </Table>
+ </TableContainer>
+
+ <Text
+ w="full"
+ mt="4"
+ >
+ HTTP Batch Metrics
+ </Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Metric</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {convertConfigurationToTable(batchMetrics)}
+ </Tbody>
+ </Table>
+ </TableContainer>
+
+ <Text
+ w="full"
+ mt="4"
+ >
+ HTTP Send Metrics
+ </Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Metric</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {convertConfigurationToTable(sendMetrics)}
+ </Tbody>
+ </Table>
+ </TableContainer>
+
+ <Text
+ w="full"
+ mt="4"
+ >
+ HTTP Push Metrics
+ </Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Metric</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {convertConfigurationToTable(pushMetrics)}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ <Box
+ maxW="full"
+ bg="white"
+ borderWidth="1px"
+ borderRadius="md"
+ overflow="hidden"
+ p="4"
+ mt="4"
+ >
+
+ <Text
+ w="full"
+ mt="4"
+ >
+ TCP Metrics
+ </Text>
+
+ <TableContainer mt="4">
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Metric</Th>
+ <Th>Value</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {convertConfigurationToTable(tcpMetrics)}
+ </Tbody>
+ </Table>
+ </TableContainer>
+
+ </Box>
+ </>
+ );
+};
+
+export default MetricsTable;
diff --git a/components/navigation/MenuItem.tsx b/components/navigation/MenuItem.tsx
new file mode 100644
index 0000000..31f119b
--- /dev/null
+++ b/components/navigation/MenuItem.tsx
@@ -0,0 +1,115 @@
+/*
+ * 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 React, { FC, ReactNode } from 'react';
+import {
+ Flex, FlexProps, Link, Text,
+} from '@chakra-ui/react';
+
+interface MenuItemProps extends FlexProps {
+ selected: boolean;
+ active: boolean;
+ href: string;
+ children: string | number;
+ setActiveName: (name: string) => void;
+}
+
+export const MenuItem = ({
+ selected,
+ active,
+ href,
+ children,
+ setActiveName,
+}: MenuItemProps) => (
+ <Link
+ position="relative"
+ href={href}
+ h="8"
+ mt={1.5}
+ mb={1.5}
+ ml={3}
+ mr={3}
+ boxSizing="border-box"
+ style={{ textDecoration: 'none' }}
+ _focus={{ boxShadow: 'none' }}
+ onMouseOver={() => setActiveName(children.toString())}
+ onMouseOut={() => setActiveName('')}
+ >
+ <Flex
+ position="absolute"
+ zIndex={2}
+ w="full"
+ h="full"
+ p="6"
+ borderRadius="lg"
+ align="center"
+ role="group"
+ cursor="pointer"
+ fontSize="md"
+ transition="color 0.15s, background-color 0.15s"
+ color={selected || active ? '#2a62ad' : 'current'}
+ fontWeight={selected ? 'bolder' : 'none'}
+ bgColor={selected || active ? '#dce5fe' : 'none'}
+ wordBreak="break-word"
+ overflowWrap="normal"
+ >
+ {children}
+ </Flex>
+ {/* <Box
+ position="absolute"
+ zIndex={1}
+ p={selected || active ? '4' : 0}
+ borderRadius="lg"
+ role="group"
+ cursor="pointer"
+ transition="width 0.3s, opacity 0.4s"
+ bgGradient="linear(28deg,#2a4cad, #2a6bad, #28c9ff)"
+ h="100%"
+ w={selected || active ? '100%' : '0'}
+ opacity={selected || active ? 1 : 0}
+ /> */}
+ </Link>
+);
+
+export const MenuGroupItem: FC<{ name: string; children: ReactNode }> = (
+ props,
+) => {
+ const { name, children } = props;
+ return (
+ <Flex
+ flexDirection="column"
+ w="full"
+ justifyContent="center"
+ alignContent="center"
+ >
+ {name && (
+ <Text
+ mt={5}
+ mb="2"
+ pl="6"
+ fontSize="xs"
+ color="#a2b5d8"
+ fontWeight="bold"
+ >
+ {name.toUpperCase()}
+ </Text>
+ )}
+ {children}
+ </Flex>
+ );
+};
diff --git a/components/navigation/Menus.tsx b/components/navigation/Menus.tsx
new file mode 100644
index 0000000..72be935
--- /dev/null
+++ b/components/navigation/Menus.tsx
@@ -0,0 +1,179 @@
+/*
+ * 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 React, { FC, ReactNode, useState } from 'react';
+import {
+ Box,
+ BoxProps,
+ Button,
+ Flex,
+ useColorModeValue,
+ Image,
+} from '@chakra-ui/react';
+import { IconType } from 'react-icons';
+import { ArrowBackIcon } from '@chakra-ui/icons';
+
+import { useRouter } from 'next/router';
+
+import {
+ FiList, FiGrid, FiServer, FiDatabase, FiMenu,
+} from 'react-icons/fi';
+import LogoImg from '../../static/images/logo.png';
+import { MenuItem, MenuGroupItem } from './MenuItem';
+
+const Menus: Array<{
+ group?: string;
+ name: string;
+ icon: IconType;
+ href: string;
+ subPath?:string[]
+}> = [
+ { name: 'Overview', icon: FiList, href: '/' },
+ { name: 'Metrics', icon: FiMenu, href: '/metrics' },
+ { name: 'Registry', icon: FiDatabase, href: '/registry' },
+ { name: 'Topic', icon: FiGrid, href: '/topic' },
+ { name: 'Event', icon: FiDatabase, href: '/event' },
+ {
+ group: 'Workflow',
+ name: 'Workflows',
+ icon: FiServer,
+ href: '/workflows',
+ subPath: ['/workflows/create'],
+ },
+ {
+ group: 'Workflow',
+ name: 'Event Catalogs',
+ icon: FiServer,
+ href: '/eventCatalogs',
+ },
+ {
+ group: 'Clients',
+ name: 'TCP',
+ icon: FiServer,
+ href: '/tcp',
+ },
+ {
+ group: 'Clients',
+ name: 'HTTP',
+ icon: FiServer,
+ href: '/http',
+ },
+ {
+ group: 'Clients',
+ name: 'gRPC',
+ icon: FiServer,
+ href: '/grpc',
+ },
+];
+
+interface MenuProps extends BoxProps {
+ onClose: () => void;
+}
+interface IGroupItem {
+ name?: string;
+ children: ReactNode[];
+}
+
+const NavMenu: FC<MenuProps> = ({ display = {}, onClose }) => {
+ const router = useRouter();
+ const [curMenu, setCurMenu] = useState('');
+ const curRoute = router.pathname;
+
+ const MenuByGroup = Menus.reduce<{
+ [groupName: string]: IGroupItem;
+ }>(
+ (groupItems, item) => {
+ const {
+ group, name, href, subPath,
+ } = item;
+ const menuItem = (
+ <MenuItem
+ key={`menu_item_${name}`}
+ selected={curRoute === href || (subPath?.includes(curRoute) ?? false)}
+ active={curMenu === group}
+ href={href}
+ setActiveName={(selectedName:string) => setCurMenu(selectedName)}
+ >
+ {name}
+ </MenuItem>
+ );
+
+ if (!group) {
+ groupItems.topMenu.children.push(menuItem);
+ return groupItems;
+ }
+
+ if (!groupItems[group]) {
+ groupItems[group] = { name: group, children: [] };
+ }
+ groupItems[group].children.push(menuItem);
+
+ return groupItems;
+ },
+ { topMenu: { children: [] } },
+ );
+
+ return (
+ <Box
+ display={display}
+ pos="fixed"
+ w={{ base: 'full', md: 60 }}
+ borderRight="1px"
+ borderRightColor={useColorModeValue('gray.200', 'gray.700')}
+ h="full"
+ bg={useColorModeValue('white', 'gray.900')}
+ boxShadow="base"
+ >
+ <Flex
+ mt={{ base: 5, md: 10 }}
+ mb={{ base: 5, md: 10 }}
+ alignItems={{ base: 'space-between', md: 'center' }}
+ justifyContent={{ base: 'space-between', md: 'center' }}
+ w={{ base: 'full' }}
+ >
+ <Image
+ display={{ base: 'none', md: 'block' }}
+ w={100}
+ src={LogoImg.src}
+ alt="Dan Abramov"
+ />
+ <Button
+ display={{ base: 'block', md: 'none' }}
+ w={{ base: 'full' }}
+ size="lg"
+ textAlign="left"
+ onClick={onClose}
+ >
+ <ArrowBackIcon mr={2} />
+ Back
+ </Button>
+ </Flex>
+
+ <Flex flexDirection="column" alignItems="center">
+ {Object.entries(MenuByGroup).map((groupItem) => (
+ <MenuGroupItem key={`group_item_${groupItem[1].name}`} name={groupItem[1].name ?? ''}>
+ {groupItem[1].children}
+ </MenuGroupItem>
+ ))}
+ </Flex>
+ </Box>
+ );
+};
+
+export default NavMenu;
diff --git a/components/navigation/MenusMobile.tsx b/components/navigation/MenusMobile.tsx
new file mode 100644
index 0000000..8885f14
--- /dev/null
+++ b/components/navigation/MenusMobile.tsx
@@ -0,0 +1,58 @@
+/*
+ * 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 {
+ IconButton,
+ Flex,
+ useColorModeValue,
+ Text,
+ FlexProps,
+} from '@chakra-ui/react';
+import { FiMenu } from 'react-icons/fi';
+
+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>
+);
+
+export default MobileNav;
diff --git a/components/navigation/Sidebar.tsx b/components/navigation/Sidebar.tsx
new file mode 100644
index 0000000..e8e349b
--- /dev/null
+++ b/components/navigation/Sidebar.tsx
@@ -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.
+ */
+
+/* eslint-disable react/jsx-props-no-spreading */
+import React, { FC, ReactNode } from 'react';
+import {
+ Box,
+ useColorModeValue,
+ Drawer,
+ DrawerContent,
+ useDisclosure,
+} from '@chakra-ui/react';
+import Menus from './Menus';
+import MenusMobile from './MenusMobile';
+
+const Sidebar: FC<{ children: ReactNode }> = ({ children }) => {
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ return (
+ <>
+ <Box minH="100vh" bg={useColorModeValue('gray.100', 'gray.900')}>
+ <Menus
+ display={{ base: 'none', md: 'block' }}
+ onClose={() => onClose}
+ />
+ <MenusMobile onOpen={onOpen} />
+ <Box ml={{ base: 0, md: 60 }} p="4">
+ {children}
+ </Box>
+ </Box>
+ <Drawer
+ autoFocus={false}
+ isOpen={isOpen}
+ placement="left"
+ onClose={onClose}
+ returnFocusOnClose={false}
+ onOverlayClick={onClose}
+ size="full"
+ >
+ <DrawerContent>
+ <Menus onClose={onClose} />
+ </DrawerContent>
+ </Drawer>
+ </>
+ );
+};
+
+export default Sidebar;
diff --git a/components/registry/RegistryTable.tsx b/components/registry/RegistryTable.tsx
new file mode 100644
index 0000000..719fde6
--- /dev/null
+++ b/components/registry/RegistryTable.tsx
@@ -0,0 +1,170 @@
+/*
+ * 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,
+} from '@chakra-ui/react';
+import axios from 'axios';
+import { useContext, useEffect, useState } from 'react';
+import { AppContext } from '../../context/context';
+
+interface EventMeshInstance {
+ eventMeshClusterName: string,
+ eventMeshName: string,
+ endpoint: string,
+ lastUpdateTimestamp: number,
+ metadata: string
+}
+
+const EventMeshInstanceRow = ({
+ eventMeshClusterName, eventMeshName, endpoint, lastUpdateTimestamp, metadata,
+}: EventMeshInstance) => (
+ <Tr>
+ <Td>{eventMeshClusterName}</Td>
+ <Td>{eventMeshName}</Td>
+ <Td>{endpoint}</Td>
+ <Td>{lastUpdateTimestamp}</Td>
+ <Td>{metadata}</Td>
+ </Tr>
+);
+
+const RegistryTable = () => {
+ const { state } = useContext(AppContext);
+
+ const [searchInput, setSearchInput] = useState<string>('');
+ const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setSearchInput(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 [EventMeshInstanceList, setEventMeshInstanceList] = useState<EventMeshInstance[]>([]);
+ const toast = useToast();
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ const { data } = await axios.get<EventMeshInstance[]>(`${state.endpoint}/registry`);
+ setEventMeshInstanceList(data);
+
+ const nextGroupSet = new Set<string>();
+ data.forEach(({ eventMeshClusterName }) => {
+ nextGroupSet.add(eventMeshClusterName);
+ });
+ setGroupSet(nextGroupSet);
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to fetch registry list',
+ description: 'unable to connect to the EventMesh daemon',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ setEventMeshInstanceList([]);
+ }
+ }
+ };
+
+ 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 Group"
+ onChange={handleGroupSelectChange}
+ >
+ {Array.from(groupSet).map((group) => (
+ <option value={group} key={group}>{group}</option>
+ ))}
+ </Select>
+ </HStack>
+
+ <TableContainer>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Event Mesh Cluster Name</Th>
+ <Th>Event Mesh Name</Th>
+ <Th>Endpoint</Th>
+ <Th>Last Update Timestamp</Th>
+ <Th>Metadata</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {EventMeshInstanceList && EventMeshInstanceList.filter(({
+ eventMeshClusterName, eventMeshName,
+ }) => {
+ if (searchInput && !eventMeshName.includes(searchInput)) {
+ return false;
+ }
+ if (groupFilter && groupFilter !== eventMeshClusterName) {
+ return false;
+ }
+ return true;
+ }).map(({
+ eventMeshClusterName, eventMeshName, endpoint, lastUpdateTimestamp, metadata,
+ }) => (
+ <EventMeshInstanceRow
+ eventMeshClusterName={eventMeshClusterName}
+ eventMeshName={eventMeshName}
+ endpoint={endpoint}
+ lastUpdateTimestamp={lastUpdateTimestamp}
+ metadata={metadata}
+ />
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ );
+};
+
+export default RegistryTable;
diff --git a/components/topic/TopicTable.tsx b/components/topic/TopicTable.tsx
new file mode 100644
index 0000000..f3a1369
--- /dev/null
+++ b/components/topic/TopicTable.tsx
@@ -0,0 +1,275 @@
+/*
+ * 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,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ TableContainer,
+ useToast,
+ Box,
+ Button,
+ Modal,
+ ModalBody,
+ ModalCloseButton,
+ ModalContent,
+ ModalFooter,
+ ModalHeader,
+ ModalOverlay,
+ useDisclosure,
+} from '@chakra-ui/react';
+import axios from 'axios';
+import { useContext, useEffect, useState } from 'react';
+import { AppContext } from '../../context/context';
+
+interface Topic {
+ name: string,
+ messageCount: number,
+}
+
+interface TopicProps {
+ name: string,
+ messageCount: number,
+}
+
+interface CreateTopicRequest {
+ name: string,
+}
+
+interface RemoveTopicRequest {
+ name: string,
+}
+
+const CreateTopicModal = () => {
+ const { state } = useContext(AppContext);
+
+ const { isOpen, onOpen, onClose } = useDisclosure();
+ const [topicName, setTopicName] = useState('');
+ const handleTopicNameChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setTopicName(event.currentTarget.value);
+ };
+
+ const toast = useToast();
+ const [loading, setLoading] = useState(false);
+ const onCreateClick = async () => {
+ try {
+ setLoading(true);
+ await axios.post<CreateTopicRequest>(`${state.endpoint}/topic`, {
+ name: topicName,
+ });
+ onClose();
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to create the topic',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+ <>
+ <Button
+ colorScheme="blue"
+ onClick={onOpen}
+ >
+ Create Topic
+ </Button>
+ <Modal isOpen={isOpen} onClose={onClose}>
+ <ModalOverlay />
+ <ModalContent>
+ <ModalHeader>Create Topic</ModalHeader>
+ <ModalCloseButton />
+ <ModalBody>
+ <Input
+ placeholder="Topic Name"
+ value={topicName}
+ onChange={handleTopicNameChange}
+ />
+ </ModalBody>
+
+ <ModalFooter>
+ <Button
+ mr={2}
+ onClick={onClose}
+ >
+ Close
+ </Button>
+ <Button
+ colorScheme="blue"
+ onClick={onCreateClick}
+ isLoading={loading}
+ isDisabled={topicName.length === 0}
+ >
+ Create
+ </Button>
+ </ModalFooter>
+ </ModalContent>
+ </Modal>
+ </>
+ );
+};
+
+const TopicRow = ({
+ name,
+ messageCount,
+}: TopicProps) => {
+ const { state } = useContext(AppContext);
+
+ const toast = useToast();
+ const [loading, setLoading] = useState(false);
+ const onRemoveClick = async () => {
+ try {
+ setLoading(true);
+ await axios.delete<RemoveTopicRequest>(`${state.endpoint}/topic`, {
+ data: {
+ name,
+ },
+ });
+ setLoading(false);
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to remove the topic',
+ description: error.message,
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ }
+ }
+ };
+
+ return (
+ <Tr>
+ <Td>{name}</Td>
+ <Td>{messageCount}</Td>
+ <Td>
+ <HStack>
+ <Button
+ colorScheme="red"
+ isLoading={loading}
+ onClick={onRemoveClick}
+ >
+ Remove
+ </Button>
+ </HStack>
+ </Td>
+ </Tr>
+ );
+};
+
+const TopicTable = () => {
+ const { state } = useContext(AppContext);
+
+ const [searchInput, setSearchInput] = useState<string>('');
+ const handleSearchInputChange = (event: React.FormEvent<HTMLInputElement>) => {
+ setSearchInput(event.currentTarget.value);
+ };
+
+ const [topicList, setTopicList] = useState<Topic[]>([]);
+ const toast = useToast();
+ useEffect(() => {
+ const fetch = async () => {
+ try {
+ const { data } = await axios.get<Topic[]>(`${state.endpoint}/topic`);
+ setTopicList(data);
+ } catch (error) {
+ if (axios.isAxiosError(error)) {
+ toast({
+ title: 'Failed to fetch the list of topics',
+ description: 'Unable to connect to the EventMesh daemon',
+ status: 'error',
+ duration: 3000,
+ isClosable: true,
+ });
+ setTopicList([]);
+ }
+ }
+ };
+
+ fetch();
+ }, []);
+
+ return (
+ <Box
+ maxW="full"
+ bg="white"
+ borderWidth="1px"
+ borderRadius="md"
+ overflow="hidden"
+ p="4"
+ >
+ <HStack
+ spacing="2"
+ >
+ <Input
+ w="100%"
+ placeholder="Search"
+ value={searchInput}
+ onChange={handleSearchInputChange}
+ />
+ <CreateTopicModal />
+ </HStack>
+
+ <TableContainer>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Topic Name</Th>
+ <Th>Message Count</Th>
+ <Th>Action</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {topicList.filter(({
+ name,
+ }) => {
+ if (searchInput && !name.includes(searchInput)) {
+ return false;
+ }
+ return true;
+ }).map(({
+ name,
+ messageCount,
+ }) => (
+ <TopicRow
+ name={name}
+ messageCount={messageCount}
+ />
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ </Box>
+ );
+};
+
+export default TopicTable;
diff --git a/components/workflow/Create.tsx b/components/workflow/Create.tsx
new file mode 100644
index 0000000..d15d5a4
--- /dev/null
+++ b/components/workflow/Create.tsx
@@ -0,0 +1,136 @@
+/*
+ * 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 React, { FC, useRef, useState } from 'react';
+
+import {
+ Drawer,
+ DrawerContent,
+ DrawerOverlay,
+ DrawerHeader,
+ DrawerBody,
+ DrawerFooter,
+ DrawerCloseButton,
+ Button,
+ useToast,
+ Spinner,
+} from '@chakra-ui/react';
+import axios from 'axios';
+
+import Editor, { Monaco } from '@monaco-editor/react';
+
+const ApiRoot = process.env.NEXT_PUBLIC_WORKFLOW_API_ROOT;
+
+const Create: FC<{
+ visible: boolean;
+ onClose: () => void;
+ onSucceed: () => void;
+}> = ({ visible = false, onClose = () => {}, onSucceed = () => {} }) => {
+ const toast = useToast();
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const editorRef = useRef<Monaco | null>(null);
+ const defaultEditorValue = '# Your code goes here';
+
+ const onSubmit = () => {
+ setIsSubmitting(true);
+
+ try {
+ const value = editorRef.current.getValue();
+ if (value === defaultEditorValue) {
+ toast({
+ title: 'Invalid definition',
+ description: 'Please input your workflow definition properly',
+ status: 'warning',
+ position: 'top-right',
+ });
+ setIsSubmitting(false);
+ return;
+ }
+ axios
+ .post(
+ `${ApiRoot}/workflow`,
+ { workflow: { definition: value } },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ )
+ .then(() => {
+ toast({
+ title: 'Succeeded',
+ status: 'success',
+ position: 'top-right',
+ });
+ onSucceed();
+ setIsSubmitting(false);
+ })
+ .catch((error) => {
+ toast({
+ title: 'Failed',
+ description: error.response.data,
+ status: 'error',
+ position: 'top-right',
+ });
+ setIsSubmitting(false);
+ });
+ } catch (error) {
+ setIsSubmitting(false);
+ }
+ };
+
+ const handleEditorDidMount = (editor: Monaco) => {
+ editorRef.current = editor;
+ };
+
+ return (
+ <Drawer
+ isOpen={visible}
+ size="xl"
+ placement="right"
+ closeOnEsc={false}
+ onClose={() => onClose()}
+ >
+ <DrawerOverlay />
+ <DrawerContent>
+ <DrawerCloseButton />
+ <DrawerHeader>Create New Workflow</DrawerHeader>
+ <DrawerBody>
+ <Editor
+ height="1000px"
+ defaultLanguage="yaml"
+ defaultValue={defaultEditorValue}
+ onMount={handleEditorDidMount}
+ theme="vs-dark"
+ />
+ </DrawerBody>
+ <DrawerFooter justifyContent="flex-start">
+ <Button colorScheme="blue" mr={3} onClick={onSubmit}>
+ {isSubmitting ? <Spinner colorScheme="white" size="sm" /> : 'Submit'}
+ </Button>
+ <Button variant="ghost" colorScheme="blue" onClick={onClose}>
+ Cancel
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ );
+};
+
+export default React.memo(Create);
diff --git a/components/workflow/Details/Details.tsx b/components/workflow/Details/Details.tsx
new file mode 100644
index 0000000..601cee9
--- /dev/null
+++ b/components/workflow/Details/Details.tsx
@@ -0,0 +1,285 @@
+/*
+ * 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 React, {
+ FC, useEffect, useRef, useState,
+} from 'react';
+
+import {
+ Drawer,
+ DrawerContent,
+ DrawerOverlay,
+ DrawerHeader,
+ DrawerBody,
+ DrawerFooter,
+ DrawerCloseButton,
+ Box,
+ FormLabel,
+ Flex,
+ Text,
+ Button,
+ Stack,
+ Tabs,
+ TabList,
+ TabPanels,
+ Tab,
+ TabPanel,
+ useToast,
+ AlertDialog,
+ AlertDialogOverlay,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogBody,
+ AlertDialogFooter,
+ Alert,
+ Badge,
+} from '@chakra-ui/react';
+
+import { WarningIcon, InfoIcon } from '@chakra-ui/icons';
+import moment from 'moment';
+
+import Editor, { Monaco } from '@monaco-editor/react';
+import axios from 'axios';
+import { WorkflowType, WorkflowStatusEnum } from '../types';
+// import { WorkflowStatusMap } from '../constant';
+import Intances from './Instances';
+
+const ApiRoot = process.env.NEXT_PUBLIC_WORKFLOW_API_ROOT;
+
+const Details: FC<{
+ visible: boolean;
+ data?: WorkflowType | null;
+ onClose: () => void;
+ onSaved: () => void;
+}> = ({
+ visible = false, data, onClose = () => {}, onSaved = () => {},
+}) => {
+ const toast = useToast();
+ const editorRef = useRef<Monaco | null>(null);
+ const [isShowConfirm, setIsShowComfirm] = useState(false);
+ const cancelRef = useRef(null);
+ const handleEditorDidMount = (editor: Monaco) => {
+ editorRef.current = editor;
+ editor.setValue(data?.definition ?? '');
+ };
+
+ const onSubmit = () => {
+ const value = editorRef.current.getValue();
+
+ axios
+ .post(
+ `${ApiRoot}/workflow`,
+ {
+ workflow: {
+ workflow_id: data?.workflow_id,
+ definition: value,
+ },
+ },
+ {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ },
+ )
+ .then(() => {
+ toast({
+ title: 'Workflow saved',
+ status: 'success',
+ position: 'top-right',
+ });
+ setIsShowComfirm(false);
+ onSaved();
+ onClose();
+ })
+ .catch((error) => {
+ setIsShowComfirm(false);
+ toast({
+ title: 'Failed to save',
+ description: error.response.data,
+ status: 'error',
+ position: 'top-right',
+ });
+ });
+ };
+
+ const onConfirm = () => {
+ const value = editorRef.current.getValue();
+ if (data?.definition === value) {
+ onClose();
+ return;
+ }
+ setIsShowComfirm(true);
+ };
+
+ useEffect(() => {
+ if (editorRef.current) {
+ editorRef.current.setValue(data?.definition ?? '');
+ }
+ }, [data, editorRef]);
+
+ return (
+ <>
+ <AlertDialog
+ leastDestructiveRef={cancelRef}
+ isOpen={isShowConfirm}
+ onClose={onClose}
+ >
+ <AlertDialogOverlay>
+ <AlertDialogContent>
+ <AlertDialogHeader
+ fontSize="lg"
+ fontWeight="bold"
+ alignItems="center"
+ >
+ <WarningIcon boxSize="6" mr={2} color="orange" />
+ <Text fontSize="xl" as="b">
+ Confirm
+ </Text>
+ </AlertDialogHeader>
+ <AlertDialogBody>
+ Are you sure to save the changes to
+ {' '}
+ <Text as="b">{data?.workflow_id}</Text>
+ ?
+ </AlertDialogBody>
+ <AlertDialogFooter>
+ <Button ref={cancelRef} onClick={() => setIsShowComfirm(false)}>
+ No
+ </Button>
+ <Button colorScheme="blue" onClick={onSubmit} ml={3}>
+ Save
+ </Button>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialogOverlay>
+ </AlertDialog>
+
+ <Drawer
+ isOpen={visible}
+ size="xl"
+ placement="right"
+ onClose={() => onClose()}
+ >
+ <DrawerOverlay />
+ <DrawerContent>
+ <DrawerCloseButton />
+ <DrawerHeader>
+ {data?.workflow_id}
+ {/* <Badge
+ ml={2}
+ colorScheme={
+ data?.status === WorkflowStatusEnum.Normal ? 'blue' : 'red'
+ }
+ >
+ {WorkflowStatusMap.get(data?.status ?? 0) ?? '-'}
+ </Badge> */}
+ <Badge ml={2}>
+ Version
+ {' '}
+ {data?.version}
+ </Badge>
+ </DrawerHeader>
+ <DrawerBody>
+ <Stack mb="15px" direction="row" spacing={10}>
+ <Flex flexDirection="column" h="full">
+ <Box mb={2}>
+ <FormLabel opacity={0.5}>Workflow Name</FormLabel>
+ <Text>{data?.workflow_name}</Text>
+ </Box>
+ <Box mb={2}>
+ <FormLabel opacity={0.5}>Created At</FormLabel>
+ <Text>
+ {moment(data?.update_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Text>
+ </Box>
+ <Box>
+ <FormLabel opacity={0.5}>Updated At</FormLabel>
+ <Text>
+ {moment(data?.update_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Text>
+ </Box>
+ </Flex>
+
+ <Flex flexDirection="column" h="full">
+ <Box mb={2}>
+ <FormLabel opacity={0.5}>Total Instance</FormLabel>
+ <Text>{data?.total_instances}</Text>
+ </Box>
+ <Box mb={2}>
+ <FormLabel opacity={0.5}>Running</FormLabel>
+ <Text>{data?.total_running_instances}</Text>
+ </Box>
+ <Box>
+ <FormLabel opacity={0.5}>Failed</FormLabel>
+ <Text>{data?.total_failed_instances}</Text>
+ </Box>
+ </Flex>
+ </Stack>
+
+ <Tabs>
+ <TabList>
+ <Tab>Definition</Tab>
+ <Tab>Instances</Tab>
+ </TabList>
+ <TabPanels>
+ <TabPanel>
+ <Alert status="info" mb={2}>
+ <InfoIcon color="#3182ce" mr={2} />
+ <Text fontSize="sm" color="#3182ce">
+ You can edit the workflow directly and click "OK" to save
+ it
+ </Text>
+ </Alert>
+ <Editor
+ height="1000px"
+ defaultLanguage="yaml"
+ defaultValue="# Your code goes here"
+ onMount={handleEditorDidMount}
+ theme="vs-dark"
+ />
+ </TabPanel>
+ <TabPanel>
+ <Intances workflowId={data?.workflow_id ?? ''} />
+ </TabPanel>
+ </TabPanels>
+ </Tabs>
+ </DrawerBody>
+ <DrawerFooter justifyContent="flex-start">
+ <Button colorScheme="blue" mr={3} onClick={onConfirm}>
+ OK
+ </Button>
+ <Button
+ colorScheme="blue"
+ mr={3}
+ variant="ghost"
+ onClick={onClose}
+ >
+ Cancel
+ </Button>
+ </DrawerFooter>
+ </DrawerContent>
+ </Drawer>
+ </>
+ );
+};
+
+Details.defaultProps = {
+ data: null,
+};
+export default Details;
diff --git a/components/workflow/Details/Instances.tsx b/components/workflow/Details/Instances.tsx
new file mode 100644
index 0000000..3c46ebd
--- /dev/null
+++ b/components/workflow/Details/Instances.tsx
@@ -0,0 +1,164 @@
+/*
+ * 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 React, {
+ FC, useCallback, useEffect, useState,
+} from 'react';
+import axios from 'axios';
+import {
+ Flex,
+ Text,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ TableContainer,
+ TableCaption,
+ Tag,
+ Spinner,
+} from '@chakra-ui/react';
+import moment from 'moment';
+import { WorkflowInstanceType } from '../types';
+import {
+ WorkflowIntanceStatusMap,
+ WorkflowIntanceStatusColorMap,
+} from '../constant';
+
+const ApiRoot = process.env.NEXT_PUBLIC_WORKFLOW_API_ROOT;
+
+const Instances: FC<{ workflowId: string }> = ({ workflowId }) => {
+ const [isLoading, setIsLoading] = useState(true);
+ const [instances, setInstances] = useState<WorkflowInstanceType[]>([]);
+ const [total, setTotal] = useState(0);
+ const [pageIndex, setPageIndex] = useState(1);
+ const pageSize = 10;
+
+ const getWorkflows = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const reqParams: {
+ page: number;
+ size: number;
+ workflow_id?: string;
+ } = {
+ page: pageIndex,
+ size: pageSize,
+ workflow_id: workflowId,
+ };
+
+ const { data } = await axios.get<{
+ total: number;
+ workflow_instances: WorkflowInstanceType[];
+ }>(`${ApiRoot}/workflow/instances`, {
+ params: reqParams,
+ });
+ setInstances([...instances, ...(data?.workflow_instances ?? [])]);
+ setTotal(data.total);
+ setIsLoading(false);
+ } catch (error) {
+ setIsLoading(false);
+ }
+ }, [workflowId, pageIndex, pageSize]);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ getWorkflows();
+ return () => {
+ controller.abort();
+ };
+ }, [workflowId, pageIndex, pageSize]);
+
+ return (
+ <TableContainer>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Instance ID</Th>
+ <Th>Status</Th>
+ <Th>Updated at</Th>
+ <Th>Created At</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {instances.map((workflow) => (
+ <Tr key={workflow.workflow_instance_id}>
+ <Td>{workflow.workflow_instance_id}</Td>
+ <Td>
+ <Tag
+ size="sm"
+ colorScheme={WorkflowIntanceStatusColorMap.get(
+ workflow.workflow_status,
+ )}
+ variant="outline"
+ >
+ {WorkflowIntanceStatusMap.get(workflow.workflow_status)}
+ </Tag>
+ </Td>
+ <Td>
+ {moment(workflow.update_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Td>
+ <Td>
+ {moment(workflow.create_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Td>
+ </Tr>
+ ))}
+ </Tbody>
+
+ {instances.length === 0 && (
+ <TableCaption>
+ <Text variant="xs" color="#909090">
+ empty
+ </Text>
+ </TableCaption>
+ )}
+
+ {instances.length > 0 && (
+ <TableCaption>
+ <Flex alignItems="center">
+ <Text fontSize="sx">
+ {`${instances.length} of ${total} intance${
+ total > 1 ? 's' : ''
+ } in list `}
+ </Text>
+ {isLoading ? (
+ <Spinner ml={2} size="sm" colorScheme="blue" />
+ ) : (
+ instances.length < total && (
+ <Text
+ color={instances.length < total ? '#3182ce' : ''}
+ ml="2"
+ cursor="pointer"
+ as="u"
+ onClick={() => setPageIndex(pageIndex + 1)}
+ >
+ Load More
+ </Text>
+ )
+ )}
+ </Flex>
+ </TableCaption>
+ )}
+ </Table>
+ </TableContainer>
+ );
+};
+
+export default Instances;
diff --git a/components/workflow/Details/index.ts b/components/workflow/Details/index.ts
new file mode 100644
index 0000000..11a5b7d
--- /dev/null
+++ b/components/workflow/Details/index.ts
@@ -0,0 +1,22 @@
+/*
+ * 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 Details from './Details';
+
+export default Details;
diff --git a/components/workflow/constant.ts b/components/workflow/constant.ts
new file mode 100644
index 0000000..f39a0c0
--- /dev/null
+++ b/components/workflow/constant.ts
@@ -0,0 +1,41 @@
+/*
+ * 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 { WorkflowStatusEnum, WorkflowInstanceStatusEnum } from './types';
+
+export const WorkflowStatusMap = new Map([
+ [WorkflowStatusEnum.Normal, 'Normal'],
+ [WorkflowStatusEnum.Deleted, 'Deleted'],
+]);
+
+export const WorkflowIntanceStatusMap = new Map([
+ [WorkflowInstanceStatusEnum.Sleep, 'Sleeping'],
+ [WorkflowInstanceStatusEnum.Wait, 'Waiting'],
+ [WorkflowInstanceStatusEnum.Process, 'Processing'],
+ [WorkflowInstanceStatusEnum.Succeed, 'Succeeded'],
+ [WorkflowInstanceStatusEnum.Fail, 'Failed'],
+]);
+
+export const WorkflowIntanceStatusColorMap = new Map([
+ [WorkflowInstanceStatusEnum.Sleep, 'gray'],
+ [WorkflowInstanceStatusEnum.Wait, 'orange'],
+ [WorkflowInstanceStatusEnum.Process, 'blue'],
+ [WorkflowInstanceStatusEnum.Succeed, 'green'],
+ [WorkflowInstanceStatusEnum.Fail, 'red'],
+]);
diff --git a/components/workflow/types.ts b/components/workflow/types.ts
new file mode 100644
index 0000000..45fe58a
--- /dev/null
+++ b/components/workflow/types.ts
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+export type WorkflowType = {
+ create_time: string;
+ definition: string;
+ id: number;
+ status: number;
+ total_failed_instances: number;
+ total_instances: number;
+ total_running_instances: number;
+ update_time: string;
+ version: string;
+ workflow_id: string;
+ workflow_name: string;
+};
+
+export type WorkflowInstanceType = {
+ create_time: string,
+ id: number,
+ update_time: string,
+ workflow_id : string,
+ workflow_instance_id : string,
+ workflow_status : number
+};
+
+export enum WorkflowStatusEnum {
+ 'Normal' = 1,
+ 'Deleted' = -1,
+}
+
+export enum WorkflowInstanceStatusEnum {
+ Sleep = 1,
+ Wait = 2,
+ Process = 3,
+ Succeed = 4,
+ Fail = 5,
+}
diff --git a/context/context.tsx b/context/context.tsx
new file mode 100644
index 0000000..19b074f
--- /dev/null
+++ b/context/context.tsx
@@ -0,0 +1,87 @@
+/*
+ * 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 {
+ useMemo,
+ useEffect,
+ useState,
+ createContext,
+ Dispatch,
+} from 'react';
+import { useImmerReducer } from 'use-immer';
+import { State, Action } from './type';
+import reducer from './reducer';
+
+const initialState: State = {
+ endpoint: 'http://localhost:10106',
+};
+
+const AppContext = createContext<{
+ state: State;
+ dispatch: Dispatch<Action>;
+}>({
+ state: initialState,
+ dispatch: () => null,
+});
+
+interface AppProviderProps {
+ children: React.ReactNode;
+}
+
+const AppProvider = ({ children }: AppProviderProps) => {
+ const [state, dispatch] = useImmerReducer(
+ reducer,
+ initialState,
+ );
+ const [isLoading, setIsLoading] = useState(true);
+
+ useEffect(() => {
+ const localState = localStorage.getItem('state');
+ if (localState === null) {
+ return;
+ }
+ const parsedState: State = JSON.parse(localState);
+ dispatch({
+ type: 'SetState',
+ payload: {
+ endpoint: parsedState.endpoint,
+ },
+ });
+ }, []);
+
+ useEffect(() => {
+ if (!isLoading) localStorage.setItem('state', JSON.stringify(state));
+ setIsLoading(false);
+ }, [state]);
+
+ const context = useMemo(() => ({
+ state,
+ dispatch,
+ }), [state]);
+
+ return (
+ <AppContext.Provider
+ value={context}
+ >
+ {children}
+ </AppContext.Provider>
+ );
+};
+
+export { AppContext, AppProvider };
diff --git a/context/reducer.ts b/context/reducer.ts
new file mode 100644
index 0000000..6e4eed7
--- /dev/null
+++ b/context/reducer.ts
@@ -0,0 +1,40 @@
+/*
+ * 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 { Action, State } from './type';
+
+const reducer = (
+ state: State,
+ action: Action,
+) => {
+ switch (action.type) {
+ case 'SetState':
+ state.endpoint = action.payload.endpoint;
+ break;
+
+ case 'SetEndPointAction':
+ state.endpoint = action.payload.endpoint;
+ break;
+
+ default:
+ break;
+ }
+};
+
+export default reducer;
diff --git a/context/type.ts b/context/type.ts
new file mode 100644
index 0000000..5deb2ba
--- /dev/null
+++ b/context/type.ts
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+
+export interface State {
+ endpoint: string;
+}
+
+interface SetState {
+ type: 'SetState';
+ payload: {
+ endpoint: string;
+ };
+}
+
+interface SetEndPointAction {
+ type: 'SetEndPointAction';
+ payload: {
+ endpoint: string;
+ };
+}
+
+export type Action =
+ | SetState
+ | SetEndPointAction;
diff --git a/next.config.js b/next.config.js
new file mode 100644
index 0000000..b0f480c
--- /dev/null
+++ b/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/package.json b/package.json
new file mode 100644
index 0000000..22d8dc8
--- /dev/null
+++ b/package.json
@@ -0,0 +1,45 @@
+{
+ "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/icons": "^2.0.15",
+ "@chakra-ui/react": "^2.1.2",
+ "@emotion/react": "^11.9.0",
+ "@emotion/styled": "^11.8.1",
+ "@fontsource/inter": "^4.5.10",
+ "@monaco-editor/react": "^4.4.6",
+ "axios": "^0.27.2",
+ "cloudevents": "^6.0.2",
+ "framer-motion": "^6.3.6",
+ "immer": "^9.0.15",
+ "moment": "^2.29.4",
+ "next": "12.1.6",
+ "react": "18.1.0",
+ "react-dom": "18.1.0",
+ "react-icons": "^4.4.0",
+ "swr": "^1.3.0",
+ "use-immer": "^0.7.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/pages/_app.tsx b/pages/_app.tsx
new file mode 100644
index 0000000..6095173
--- /dev/null
+++ b/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 type { AppProps } from 'next/app';
+import Sidebar from '../components/navigation/Sidebar';
+import { AppProvider } from '../context/context';
+
+const theme = extendTheme({
+ initialColorMode: 'light',
+ useSystemColorMode: true,
+ fonts: {
+ heading: 'Inter, sans-serif',
+ body: 'Inter, sans-serif',
+ },
+});
+
+const Application = ({ Component, pageProps }: AppProps) => (
+ <ChakraProvider theme={theme}>
+ <AppProvider>
+ <Sidebar>
+ <Component {...pageProps} />
+ </Sidebar>
+ </AppProvider>
+ </ChakraProvider>
+);
+
+export default Application;
diff --git a/pages/_document.tsx b/pages/_document.tsx
new file mode 100644
index 0000000..dd32fc0
--- /dev/null
+++ b/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/pages/event.tsx b/pages/event.tsx
new file mode 100644
index 0000000..8214d74
--- /dev/null
+++ b/pages/event.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 EventTable from '../components/event/EventTable';
+
+const Event: NextPage = () => (
+ <>
+ <Head>
+ <title>Event | Apache EventMesh Dashboard</title>
+ </Head>
+ <EventTable />
+ </>
+);
+
+export default Event;
diff --git a/pages/eventCatalogs.tsx b/pages/eventCatalogs.tsx
new file mode 100644
index 0000000..f33d57e
--- /dev/null
+++ b/pages/eventCatalogs.tsx
@@ -0,0 +1,223 @@
+/*
+ * 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 React, { useState, useEffect, useCallback } from 'react';
+import Head from 'next/head';
+import type { NextPage } from 'next';
+
+import {
+ Divider,
+ Button,
+ Flex,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ TableContainer,
+ Box,
+ Spinner,
+ Text,
+} from '@chakra-ui/react';
+import { ChevronLeftIcon, ChevronRightIcon } from '@chakra-ui/icons';
+import axios from 'axios';
+import moment from 'moment';
+import Details from '../components/eventCatalogs/Details';
+import CreateCatalog from '../components/eventCatalogs/Create';
+import { EventCatalogType } from '../components/eventCatalogs/types';
+import { WorkflowStatusMap } from '../components/eventCatalogs/constant';
+
+const ApiRoot = process.env.NEXT_PUBLIC_EVENTCATALOG_API_ROOT;
+
+const EventCatalogs: NextPage = () => {
+ const [isShowCreate, setIsShowCreate] = useState(false);
+ const [curCatalog, setCurCatalog] = useState<EventCatalogType>();
+
+ const [catalogs, setCatalogs] = useState<EventCatalogType[]>([]);
+ const [total, setTotal] = useState(0);
+
+ const pageSize = 10;
+ const [isLoading, setIsLoading] = useState(true);
+ const [pageIndex, setPageIndex] = useState(1);
+
+ const [refreshFlag, setRefreshFlag] = useState<number>(+new Date());
+
+ const getEventCatalogs = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const { data } = await axios.get<{
+ total: number;
+ events: EventCatalogType[];
+ }>(`${ApiRoot}/catalog`, {
+ params: { page: pageIndex, size: pageSize },
+ });
+ setCatalogs(data.events);
+ setTotal(data.total);
+ setIsLoading(false);
+ } catch (error) {
+ setIsLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ getEventCatalogs();
+ return () => {
+ controller.abort();
+ };
+ }, [pageIndex, pageSize, refreshFlag]);
+
+ return (
+ <>
+ <Head>
+ <title>Event Catalogs | Apache EventMesh Dashboard</title>
+ </Head>
+ <Box
+ w="full"
+ h="full"
+ bg="white"
+ flexDirection="column"
+ borderWidth="1px"
+ borderRadius="md"
+ overflow="hidden"
+ p="6"
+ >
+ <Flex w="full" justifyContent="space-between" mt="2" mb="2">
+ <Button
+ size="md"
+ backgroundColor="#2a62ad"
+ color="white"
+ _hover={{ bg: '#dce5fe', color: '#2a62ad' }}
+ onClick={() => setIsShowCreate(true)}
+ >
+ Create Catalog
+ </Button>
+ <Button
+ size="md"
+ colorScheme="blue"
+ variant="ghost"
+ onClick={() => setRefreshFlag(+new Date())}
+ >
+ Refresh
+ </Button>
+ </Flex>
+ <Divider mt="15" mb="15" orientation="horizontal" />
+ <TableContainer>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ {/* <Th>Catalog ID</Th> */}
+ <Th>Title</Th>
+ <Th>File Name</Th>
+ <Th>Version</Th>
+ <Th>Status</Th>
+ <Th>Created At</Th>
+ <Th>Updated At</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {catalogs.map((catalog) => (
+ <Tr key={catalog.id}>
+ {/* <Td>
+ <Button
+ colorScheme="blue"
+ variant="ghost"
+ onClick={() => setCurCatalog(catalog)}
+ >
+ {catalog.id}
+ </Button>
+ </Td> */}
+ <Td>
+ <Button
+ colorScheme="blue"
+ variant="ghost"
+ onClick={() => setCurCatalog(catalog)}
+ >
+ {catalog.title}
+ </Button>
+ </Td>
+ <Td>{catalog.file_name}</Td>
+ <Td>{catalog.version}</Td>
+ <Td>{WorkflowStatusMap.get(catalog.status)}</Td>
+ <Td>
+ {moment(catalog.create_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Td>
+ <Td>
+ {moment(catalog.update_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Td>
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ <Flex mt={4} alignItems="center">
+ {isLoading ? (
+ <Spinner colorScheme="blue" size="sm" />
+ ) : (
+ <Text fontSize="sm" color="#909090">
+ {total}
+ {` catalog${total > 1 ? 's' : ''} in total, `}
+ {`page ${pageIndex} of ${Math.ceil(total / pageSize)}`}
+ </Text>
+ )}
+ <Flex flex={1} justifyContent="flex-end" align="center">
+ <Button
+ mr={2}
+ size="sm"
+ leftIcon={<ChevronLeftIcon />}
+ colorScheme="blue"
+ variant="outline"
+ disabled={pageIndex < 2}
+ onClick={() => setPageIndex(pageIndex - 1)}
+ >
+ Prev
+ </Button>
+ <Button
+ size="sm"
+ rightIcon={<ChevronRightIcon />}
+ colorScheme="blue"
+ variant="outline"
+ disabled={pageIndex >= Math.ceil(total / pageSize)}
+ onClick={() => setPageIndex(pageIndex + 1)}
+ >
+ Next
+ </Button>
+ </Flex>
+ </Flex>
+ </Box>
+ <Details
+ visible={Boolean(curCatalog)}
+ data={curCatalog}
+ onClose={() => setCurCatalog(undefined)}
+ />
+ <CreateCatalog
+ visible={isShowCreate}
+ onSucceed={() => {
+ setIsShowCreate(false);
+ setPageIndex(1);
+ setRefreshFlag(+new Date());
+ }}
+ onClose={() => setIsShowCreate(false)}
+ />
+ </>
+ );
+};
+
+export default EventCatalogs;
diff --git a/pages/grpc.tsx b/pages/grpc.tsx
new file mode 100644
index 0000000..8d68014
--- /dev/null
+++ b/pages/grpc.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 GrpcClientTable from '../components/client/GrpcClientTable';
+
+const GrpcClient: NextPage = () => (
+ <>
+ <Head>
+ <title>Grpc Client | Apache EventMesh Dashboard</title>
+ </Head>
+ <GrpcClientTable />
+ </>
+);
+
+export default GrpcClient;
diff --git a/pages/http.tsx b/pages/http.tsx
new file mode 100644
index 0000000..2d692d9
--- /dev/null
+++ b/pages/http.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 HTTPClientTable from '../components/client/HTTPClientTable';
+
+const HTTPClient: NextPage = () => (
+ <>
+ <Head>
+ <title>HTTP Client | Apache EventMesh Dashboard</title>
+ </Head>
+ <HTTPClientTable />
+ </>
+);
+
+export default HTTPClient;
diff --git a/pages/index.tsx b/pages/index.tsx
new file mode 100755
index 0000000..7077600
--- /dev/null
+++ b/pages/index.tsx
@@ -0,0 +1,35 @@
+/*
+ * 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';
+import Configuration from '../components/index/Configuration';
+
+const Index: NextPage = () => (
+ <>
+ <Head>
+ <title>Apache EventMesh Dashboard</title>
+ </Head>
+ <Endpoint />
+ <Configuration />
+ </>
+);
+
+export default Index;
diff --git a/pages/metrics.tsx b/pages/metrics.tsx
new file mode 100644
index 0000000..3793b64
--- /dev/null
+++ b/pages/metrics.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 MetricsTable from '../components/metrics/MetricsTable';
+
+const Metrics: NextPage = () => (
+ <>
+ <Head>
+ <title>Metrics | Apache EventMesh Dashboard</title>
+ </Head>
+ <MetricsTable />
+ </>
+);
+
+export default Metrics;
diff --git a/pages/registry.tsx b/pages/registry.tsx
new file mode 100644
index 0000000..b2dd367
--- /dev/null
+++ b/pages/registry.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 RegistryTable from '../components/registry/RegistryTable';
+
+const Registry: NextPage = () => (
+ <>
+ <Head>
+ <title>Registry | Apache EventMesh Dashboard</title>
+ </Head>
+ <RegistryTable />
+ </>
+);
+
+export default Registry;
diff --git a/pages/tcp.tsx b/pages/tcp.tsx
new file mode 100644
index 0000000..93a4028
--- /dev/null
+++ b/pages/tcp.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 TCPClientTable from '../components/client/TCPClientTable';
+
+const TCPClient: NextPage = () => (
+ <>
+ <Head>
+ <title>TCP Client | Apache EventMesh Dashboard</title>
+ </Head>
+ <TCPClientTable />
+ </>
+);
+
+export default TCPClient;
diff --git a/pages/topic.tsx b/pages/topic.tsx
new file mode 100644
index 0000000..9023151
--- /dev/null
+++ b/pages/topic.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 TopicTable from '../components/topic/TopicTable';
+
+const Topic: NextPage = () => (
+ <>
+ <Head>
+ <title>Topic | Apache EventMesh Dashboard</title>
+ </Head>
+ <TopicTable />
+ </>
+);
+
+export default Topic;
diff --git a/pages/workflows.tsx b/pages/workflows.tsx
new file mode 100644
index 0000000..29fd982
--- /dev/null
+++ b/pages/workflows.tsx
@@ -0,0 +1,375 @@
+/*
+ * 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 React, {
+ useCallback, useEffect, useState, useRef,
+} from 'react';
+import Head from 'next/head';
+import type { NextPage } from 'next';
+import moment from 'moment';
+import {
+ ChevronLeftIcon,
+ ChevronRightIcon,
+ WarningTwoIcon,
+} from '@chakra-ui/icons';
+
+import {
+ Divider,
+ Button,
+ Flex,
+ Input,
+ Stack,
+ // Select,
+ Table,
+ Thead,
+ Tbody,
+ Tr,
+ Th,
+ Td,
+ TableContainer,
+ Text,
+ AlertDialog,
+ AlertDialogOverlay,
+ AlertDialogContent,
+ AlertDialogHeader,
+ AlertDialogBody,
+ AlertDialogFooter,
+ useToast,
+ Box,
+ Spinner,
+} from '@chakra-ui/react';
+import axios from 'axios';
+import Details from '../components/workflow/Details';
+import Create from '../components/workflow/Create';
+import { WorkflowType } from '../components/workflow/types';
+// import { WorkflowStatusMap } from '../components/workflow/constant';
+
+const ApiRoot = process.env.NEXT_PUBLIC_WORKFLOW_API_ROOT;
+
+const Workflows: NextPage = () => {
+ const toast = useToast();
+
+ const [isLoading, setIsLoading] = useState(true);
+ const [workflows, setWorkflows] = useState<WorkflowType[]>([]);
+ const [total, setTotal] = useState(0);
+ const [keywordFilter, setKeywordFilter] = useState('');
+ const [statusFilter, setStatusFilter] = useState('any');
+ const [pageIndex, setPageIndex] = useState(1);
+ const pageSize = 10;
+ const [refreshFlag, setRefreshFlag] = useState<number>(+new Date());
+ const [isShowCreate, setIsShowCreate] = useState(false);
+ const [isShowDetails, setIsShowDetails] = useState(false);
+ const [isShowCancelConfirm, setIsShowCancelComfirm] = useState(false);
+ const cancelRef = useRef(null);
+
+ const [selectedWorkflow, setSelectedWorkflow] = useState<WorkflowType | null>(
+ null,
+ );
+
+ const onDelete = () => {
+ axios
+ .delete(`${ApiRoot}/workflow/${selectedWorkflow?.workflow_id}`)
+ .then(() => {
+ toast({
+ title: 'Workflow has been deleted',
+ description: (
+ <Box>
+ <Text>{`Workflow ID: ${selectedWorkflow?.workflow_id}`}</Text>
+ <Text>{`Workflow Name: ${selectedWorkflow?.workflow_name}`}</Text>
+ </Box>
+ ),
+ status: 'success',
+ position: 'top-right',
+ });
+ setIsShowCancelComfirm(false);
+ setSelectedWorkflow(null);
+ setRefreshFlag(+new Date());
+ })
+ .catch((error) => {
+ setIsShowCancelComfirm(false);
+ toast({
+ title: 'Failed to delete',
+ description: error.response.data,
+ status: 'error',
+ position: 'top-right',
+ });
+ });
+ };
+
+ const getWorkflows = useCallback(async () => {
+ setIsLoading(true);
+ try {
+ const reqParams: {
+ page: number;
+ size: number;
+ workflow_id?: string;
+ status?: string;
+ } = {
+ page: pageIndex,
+ size: pageSize,
+ };
+ // if (statusFilter) {
+ // reqParams.status = statusFilter;
+ // }
+ if (keywordFilter) {
+ reqParams.workflow_id = keywordFilter;
+ }
+ const { data } = await axios.get<{
+ total: number;
+ workflows: WorkflowType[];
+ }>(`${ApiRoot}/workflow`, {
+ params: reqParams,
+ });
+ setWorkflows(data.workflows);
+ setTotal(data.total);
+ setIsLoading(false);
+ } catch (error) {
+ setIsLoading(false);
+ }
+ }, [pageIndex, pageSize, keywordFilter, statusFilter, refreshFlag]);
+
+ useEffect(() => {
+ const controller = new AbortController();
+ getWorkflows();
+ return () => {
+ controller.abort();
+ };
+ }, [pageIndex, pageSize, keywordFilter, statusFilter, refreshFlag]);
+
+ return (
+ <>
+ <Head>
+ <title>Workflows | Apache EventMesh Dashboard</title>
+ </Head>
+ <Flex
+ w="full"
+ h="full"
+ bg="white"
+ flexDirection="column"
+ borderWidth="1px"
+ borderRadius="md"
+ overflow="hidden"
+ p="6"
+ >
+ <Flex w="full" justifyContent="space-between" mt="2" mb="2">
+ <Button
+ size="md"
+ backgroundColor="#2a62ad"
+ color="white"
+ _hover={{ bg: '#dce5fe', color: '#2a62ad' }}
+ onClick={() => setIsShowCreate(true)}
+ >
+ Create Workflow
+ </Button>
+ <Stack direction="row" spacing="2">
+ <Input
+ size="md"
+ placeholder="Workflow ID"
+ value={keywordFilter}
+ onChange={(evt) => setKeywordFilter(evt.target.value)}
+ />
+ {/* <Select
+ size="md"
+ placeholder="Status"
+ value={statusFilter}
+ onChange={(event) => setStatusFilter(event.target.value)}
+ >
+ <option value="1">Running</option>
+ <option value="-1">Deleted</option>
+ </Select> */}
+ <Box>
+ <Button
+ colorScheme="blue"
+ variant="ghost"
+ onClick={() => setRefreshFlag(+new Date())}
+ >
+ Refresh
+ </Button>
+ </Box>
+ </Stack>
+ </Flex>
+ <Divider mt="15" mb="15" orientation="horizontal" />
+ <TableContainer>
+ <Table variant="simple">
+ <Thead>
+ <Tr>
+ <Th>Workflow ID</Th>
+ <Th>Workflow Name</Th>
+ {/* <Th>Status</Th> */}
+ <Th isNumeric>Total Instance</Th>
+ <Th isNumeric>Running</Th>
+ <Th isNumeric>Failed</Th>
+ <Th>Updated at</Th>
+ <Th>Created At</Th>
+ <Th>Actions</Th>
+ </Tr>
+ </Thead>
+ <Tbody>
+ {workflows.map((workflow) => (
+ <Tr key={workflow.workflow_id}>
+ <Td>
+ <Button
+ size="sm"
+ colorScheme="blue"
+ variant="ghost"
+ onClick={() => {
+ setIsShowDetails(true);
+ setSelectedWorkflow(workflow);
+ }}
+ >
+ {workflow.workflow_id}
+ </Button>
+ </Td>
+ <Td>{workflow.workflow_name}</Td>
+
+ {/* <Td>{WorkflowStatusMap.get(workflow.status)}</Td> */}
+ <Td isNumeric>{workflow.total_instances}</Td>
+ <Td isNumeric>{workflow.total_running_instances}</Td>
+ <Td isNumeric>{workflow.total_failed_instances}</Td>
+ <Td>
+ {moment(workflow.update_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Td>
+ <Td>
+ {moment(workflow.create_time).format('YYYY-MM-DD HH:mm:ss')}
+ </Td>
+ <Td>
+ <Button
+ size="sm"
+ colorScheme="blue"
+ variant="ghost"
+ onClick={() => {
+ setSelectedWorkflow(workflow);
+ setIsShowCancelComfirm(true);
+ }}
+ >
+ Delete
+ </Button>
+ </Td>
+ </Tr>
+ ))}
+ </Tbody>
+ </Table>
+ </TableContainer>
+ <Flex mt={4} alignItems="center">
+ {isLoading ? (
+ <Spinner colorScheme="blue" size="sm" />
+ ) : (
+ <Text fontSize="sm" color="#909090">
+ {total}
+ {` workflow${total > 1 ? 's' : ''} in total, `}
+ {`page ${pageIndex} of ${Math.ceil(total / pageSize)}`}
+ </Text>
+ )}
+ <Flex flex={1} justifyContent="flex-end" align="center">
+ <Button
+ mr={2}
+ size="sm"
+ leftIcon={<ChevronLeftIcon />}
+ colorScheme="blue"
+ variant="outline"
+ disabled={pageIndex < 2}
+ onClick={() => setPageIndex(pageIndex - 1)}
+ >
+ Prev
+ </Button>
+ <Button
+ size="sm"
+ rightIcon={<ChevronRightIcon />}
+ colorScheme="blue"
+ variant="outline"
+ disabled={pageIndex >= Math.ceil(total / pageSize)}
+ onClick={() => setPageIndex(pageIndex + 1)}
+ >
+ Next
+ </Button>
+ </Flex>
+ </Flex>
+ </Flex>
+
+ <AlertDialog
+ leastDestructiveRef={cancelRef}
+ isOpen={isShowCancelConfirm}
+ onClose={() => setIsShowCancelComfirm(false)}
+ >
+ <AlertDialogOverlay>
+ <AlertDialogContent>
+ <AlertDialogHeader fontSize="lg" fontWeight="bold">
+ <Flex alignItems="center">
+ <WarningTwoIcon mr={2} boxSize={6} color="orange" />
+ <Text fontSize="xl" as="b">
+ Confirm
+ </Text>
+ </Flex>
+ </AlertDialogHeader>
+
+ <AlertDialogBody>
+ Are you sure to delete
+ {' '}
+ <Text fontSize="sm" as="b">
+ {selectedWorkflow?.workflow_name}
+ </Text>
+ ?
+ <Box />
+ </AlertDialogBody>
+
+ <AlertDialogFooter>
+ <Button
+ ref={cancelRef}
+ onClick={() => {
+ setIsShowCancelComfirm(false);
+ setSelectedWorkflow(null);
+ }}
+ >
+ No
+ </Button>
+ <Button colorScheme="blue" onClick={() => onDelete()} ml={3}>
+ Delete
+ </Button>
+ </AlertDialogFooter>
+ </AlertDialogContent>
+ </AlertDialogOverlay>
+ </AlertDialog>
+
+ <Details
+ visible={isShowDetails}
+ data={selectedWorkflow}
+ onSaved={() => {
+ setIsShowDetails(false);
+ setRefreshFlag(+new Date());
+ }}
+ onClose={() => {
+ setIsShowDetails(false);
+ setSelectedWorkflow(null);
+ }}
+ />
+
+ <Create
+ visible={isShowCreate}
+ onSucceed={() => {
+ setIsShowCreate(false);
+ setPageIndex(1);
+ setRefreshFlag(+new Date());
+ }}
+ onClose={() => setIsShowCreate(false)}
+ />
+ </>
+ );
+};
+
+export default Workflows;
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..718d6fe
--- /dev/null
+++ b/public/favicon.ico
Binary files differ
diff --git a/static/images/logo.png b/static/images/logo.png
new file mode 100644
index 0000000..e854551
--- /dev/null
+++ b/static/images/logo.png
Binary files differ
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..99710e8
--- /dev/null
+++ b/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"]
+}